diff --git a/Application/Application.csproj b/Application/Application.csproj index 96740c3fd..234f718bf 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -48,6 +48,8 @@ + + true diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index 35ff09ba9..60455d50b 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -1,7 +1,7 @@ using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Misc; -using IW4MAdmin.Application.RconParsers; +using IW4MAdmin.Application.RConParsers; using SharedLibraryCore; using SharedLibraryCore.Commands; using SharedLibraryCore.Configuration; @@ -220,15 +220,15 @@ namespace IW4MAdmin.Application public async Task UpdateServerStates() { // store the server hash code and task for it - var runningUpdateTasks = new Dictionary(); + var runningUpdateTasks = new Dictionary(); while (!_tokenSource.IsCancellationRequested) { // select the server ids that have completed the update task var serverTasksToRemove = runningUpdateTasks - .Where(ut => ut.Value.Status == TaskStatus.RanToCompletion || - ut.Value.Status == TaskStatus.Canceled || - ut.Value.Status == TaskStatus.Faulted) + .Where(ut => ut.Value.task.Status == TaskStatus.RanToCompletion || + ut.Value.task.Status == TaskStatus.Canceled || // we want to cancel if a task takes longer than 5 minutes + ut.Value.task.Status == TaskStatus.Faulted || DateTime.Now - ut.Value.startTime > TimeSpan.FromMinutes(5)) .Select(ut => ut.Key) .ToList(); @@ -239,9 +239,14 @@ namespace IW4MAdmin.Application IsInitialized = true; } - // remove the update tasks as they have completd - foreach (long serverId in serverTasksToRemove) + // remove the update tasks as they have completed + foreach (var serverId in serverTasksToRemove) { + if (!runningUpdateTasks[serverId].tokenSource.Token.IsCancellationRequested) + { + runningUpdateTasks[serverId].tokenSource.Cancel(); + } + runningUpdateTasks.Remove(serverId); } @@ -249,11 +254,12 @@ namespace IW4MAdmin.Application var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList(); foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint))) { - runningUpdateTasks.Add(server.EndPoint, Task.Run(async () => + var tokenSource = new CancellationTokenSource(); + runningUpdateTasks.Add(server.EndPoint, (Task.Run(async () => { try { - await server.ProcessUpdatesAsync(_tokenSource.Token); + await server.ProcessUpdatesAsync(_tokenSource.Token).WithWaitCancellation(runningUpdateTasks[server.EndPoint].tokenSource.Token); } catch (Exception e) @@ -268,7 +274,7 @@ namespace IW4MAdmin.Application { server.IsInitialized = true; } - })); + }, tokenSource.Token), tokenSource, DateTime.Now)); } try diff --git a/Application/EventParsers/BaseEventParser.cs b/Application/EventParsers/BaseEventParser.cs index ddb3578c1..285c80d02 100644 --- a/Application/EventParsers/BaseEventParser.cs +++ b/Application/EventParsers/BaseEventParser.cs @@ -17,6 +17,8 @@ namespace IW4MAdmin.Application.EventParsers private readonly Dictionary)> _customEventRegistrations; private readonly ILogger _logger; private readonly ApplicationConfiguration _appConfig; + private readonly Dictionary _regexMap; + private readonly Dictionary _eventTypeMap; public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig) { @@ -78,7 +80,28 @@ namespace IW4MAdmin.Application.EventParsers Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12); Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13); + Configuration.MapChange.Pattern = @".*InitGame.*"; + Configuration.MapEnd.Pattern = @".*(?:ExitLevel|ShutdownGame).*"; + Configuration.Time.Pattern = @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )"; + + _regexMap = new Dictionary + { + {Configuration.Say, GameEvent.EventType.Say}, + {Configuration.Kill, GameEvent.EventType.Kill}, + {Configuration.MapChange, GameEvent.EventType.MapChange}, + {Configuration.MapEnd, GameEvent.EventType.MapEnd} + }; + + _eventTypeMap = new Dictionary + { + {"say", GameEvent.EventType.Say}, + {"sayteam", GameEvent.EventType.Say}, + {"K", GameEvent.EventType.Kill}, + {"D", GameEvent.EventType.Damage}, + {"J", GameEvent.EventType.PreConnect}, + {"Q", GameEvent.EventType.PreDisconnect}, + }; } public IEventParserConfiguration Configuration { get; set; } @@ -91,6 +114,28 @@ namespace IW4MAdmin.Application.EventParsers public string Name { get; set; } = "Call of Duty"; + private (GameEvent.EventType type, string eventKey) GetEventTypeFromLine(string logLine) + { + var lineSplit = logLine.Split(';'); + if (lineSplit.Length > 1) + { + var type = lineSplit[0]; + return _eventTypeMap.ContainsKey(type) ? (_eventTypeMap[type], type): (GameEvent.EventType.Unknown, null); + } + + foreach (var (key, value) in _regexMap) + { + var result = key.PatternMatcher.Match(logLine); + if (result.Success) + { + return (value, null); + } + } + + return (GameEvent.EventType.Unknown, null); + } + + public virtual GameEvent GenerateGameEvent(string logLine) { var timeMatch = Configuration.Time.PatternMatcher.Match(logLine); @@ -104,7 +149,7 @@ namespace IW4MAdmin.Application.EventParsers .Values .Skip(2) // this converts the timestamp into seconds passed - .Select((_value, index) => long.Parse(_value.ToString()) * (index == 0 ? 60 : 1)) + .Select((value, index) => long.Parse(value.ToString()) * (index == 0 ? 60 : 1)) .Sum(); } @@ -114,33 +159,34 @@ namespace IW4MAdmin.Application.EventParsers } // we want to strip the time from the log line - logLine = logLine.Substring(timeMatch.Values.First().Length); + logLine = logLine.Substring(timeMatch.Values.First().Length).Trim(); } - string[] lineSplit = logLine.Split(';'); - string eventType = lineSplit[0]; + var eventParseResult = GetEventTypeFromLine(logLine); + var eventType = eventParseResult.type; + + _logger.LogDebug(logLine); - if (eventType == "say" || eventType == "sayteam") + if (eventType == GameEvent.EventType.Say) { var matchResult = Configuration.Say.PatternMatcher.Match(logLine); if (matchResult.Success) { - string message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]] - .ToString() + var message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]] .Replace("\x15", "") .Trim(); if (message.Length > 0) { - string originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString(); - string originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(); + var originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]]; + var originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]]; - long originId = originIdString.IsBotGuid() ? + var originId = originIdString.IsBotGuid() ? originName.GenerateGuidFromString() : originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); - int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); + var clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix)) { @@ -172,26 +218,26 @@ namespace IW4MAdmin.Application.EventParsers } } - if (eventType == "K") + if (eventType == GameEvent.EventType.Kill) { var match = Configuration.Kill.PatternMatcher.Match(logLine); if (match.Success) { - string originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString(); - string targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString(); - string originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(); - string targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]].ToString(); + var originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]]; + var targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]]; + var originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]]; + var targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]]; - long originId = originIdString.IsBotGuid() ? + var originId = originIdString.IsBotGuid() ? originName.GenerateGuidFromString() : originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); - long targetId = targetIdString.IsBotGuid() ? + var targetId = targetIdString.IsBotGuid() ? targetName.GenerateGuidFromString() : targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); - int originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); - int targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]); + var originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); + var targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]); return new GameEvent() { @@ -206,26 +252,26 @@ namespace IW4MAdmin.Application.EventParsers } } - if (eventType == "D") + if (eventType == GameEvent.EventType.Damage) { var match = Configuration.Damage.PatternMatcher.Match(logLine); if (match.Success) { - string originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString(); - string targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString(); - string originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(); - string targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]].ToString(); + var originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]]; + var targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]]; + var originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]]; + var targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]]; - long originId = originIdString.IsBotGuid() ? + var originId = originIdString.IsBotGuid() ? originName.GenerateGuidFromString() : originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); - long targetId = targetIdString.IsBotGuid() ? + var targetId = targetIdString.IsBotGuid() ? targetName.GenerateGuidFromString() : targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); - int originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); - int targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]); + var originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); + var targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]); return new GameEvent() { @@ -240,16 +286,16 @@ namespace IW4MAdmin.Application.EventParsers } } - if (eventType == "J") + if (eventType == GameEvent.EventType.PreConnect) { var match = Configuration.Join.PatternMatcher.Match(logLine); if (match.Success) { - string originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString(); - string originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(); + var originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]]; + var originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]]; - long networkId = originIdString.IsBotGuid() ? + var networkId = originIdString.IsBotGuid() ? originName.GenerateGuidFromString() : originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); @@ -261,10 +307,10 @@ namespace IW4MAdmin.Application.EventParsers { CurrentAlias = new EFAlias() { - Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine(), + Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine(), }, NetworkId = networkId, - ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()), + ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]), State = EFClient.ClientState.Connecting, }, Extra = originIdString, @@ -276,16 +322,16 @@ namespace IW4MAdmin.Application.EventParsers } } - if (eventType == "Q") + if (eventType == GameEvent.EventType.PreDisconnect) { var match = Configuration.Quit.PatternMatcher.Match(logLine); if (match.Success) { - string originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString(); - string originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(); + var originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]]; + var originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]]; - long networkId = originIdString.IsBotGuid() ? + var networkId = originIdString.IsBotGuid() ? originName.GenerateGuidFromString() : originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); @@ -297,10 +343,10 @@ namespace IW4MAdmin.Application.EventParsers { CurrentAlias = new EFAlias() { - Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine() + Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine() }, NetworkId = networkId, - ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()), + ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]), State = EFClient.ClientState.Disconnecting }, RequiredEntity = GameEvent.EventRequiredEntity.None, @@ -311,7 +357,7 @@ namespace IW4MAdmin.Application.EventParsers } } - if (eventType.Contains("ExitLevel") || eventType.Contains("ShutdownGame")) + if (eventType == GameEvent.EventType.MapEnd) { return new GameEvent() { @@ -325,9 +371,9 @@ namespace IW4MAdmin.Application.EventParsers }; } - if (eventType.Contains("InitGame")) + if (eventType == GameEvent.EventType.MapChange) { - string dump = eventType.Replace("InitGame: ", ""); + var dump = logLine.Replace("InitGame: ", ""); return new GameEvent() { @@ -342,26 +388,37 @@ namespace IW4MAdmin.Application.EventParsers }; } - if (_customEventRegistrations.ContainsKey(eventType)) + if (eventParseResult.eventKey == null || !_customEventRegistrations.ContainsKey(eventParseResult.eventKey)) { - var eventModifier = _customEventRegistrations[eventType]; - - try + return new GameEvent() { - return eventModifier.Item2(logLine, Configuration, new GameEvent() - { - Type = GameEvent.EventType.Other, - Data = logLine, - Subtype = eventModifier.Item1, - GameTime = gameTime, - Source = GameEvent.EventSource.Log - }); - } + Type = GameEvent.EventType.Unknown, + Data = logLine, + Origin = Utilities.IW4MAdminClient(), + Target = Utilities.IW4MAdminClient(), + RequiredEntity = GameEvent.EventRequiredEntity.None, + GameTime = gameTime, + Source = GameEvent.EventSource.Log + }; + } - catch (Exception e) + var eventModifier = _customEventRegistrations[eventParseResult.eventKey]; + + try + { + return eventModifier.Item2(logLine, Configuration, new GameEvent() { - _logger.LogError(e, $"Could not handle custom event generation"); - } + Type = GameEvent.EventType.Other, + Data = logLine, + Subtype = eventModifier.Item1, + GameTime = gameTime, + Source = GameEvent.EventSource.Log + }); + } + + catch (Exception e) + { + _logger.LogError(e, "Could not handle custom event generation"); } return new GameEvent() diff --git a/Application/EventParsers/DynamicEventParserConfiguration.cs b/Application/EventParsers/DynamicEventParserConfiguration.cs index 026c275ba..59873bf14 100644 --- a/Application/EventParsers/DynamicEventParserConfiguration.cs +++ b/Application/EventParsers/DynamicEventParserConfiguration.cs @@ -1,5 +1,6 @@ using SharedLibraryCore.Interfaces; using System.Globalization; +using SharedLibraryCore; namespace IW4MAdmin.Application.EventParsers { @@ -17,6 +18,8 @@ namespace IW4MAdmin.Application.EventParsers public ParserRegex Damage { get; set; } public ParserRegex Action { get; set; } public ParserRegex Time { get; set; } + public ParserRegex MapChange { get; set; } + public ParserRegex MapEnd { get; set; } public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber; public DynamicEventParserConfiguration(IParserRegexFactory parserRegexFactory) @@ -28,6 +31,8 @@ namespace IW4MAdmin.Application.EventParsers Damage = parserRegexFactory.CreateParserRegex(); Action = parserRegexFactory.CreateParserRegex(); Time = parserRegexFactory.CreateParserRegex(); + MapChange = parserRegexFactory.CreateParserRegex(); + MapEnd = parserRegexFactory.CreateParserRegex(); } } } diff --git a/Application/Factories/RConConnectionFactory.cs b/Application/Factories/RConConnectionFactory.cs index b65d2b4b4..399820814 100644 --- a/Application/Factories/RConConnectionFactory.cs +++ b/Application/Factories/RConConnectionFactory.cs @@ -1,6 +1,10 @@ -using IW4MAdmin.Application.RCon; +using System; using SharedLibraryCore.Interfaces; using System.Text; +using Integrations.Cod; +using Integrations.Source; +using Integrations.Source.Interfaces; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace IW4MAdmin.Application.Factories @@ -10,16 +14,16 @@ namespace IW4MAdmin.Application.Factories /// internal class RConConnectionFactory : IRConConnectionFactory { - private static readonly Encoding gameEncoding = Encoding.GetEncoding("windows-1252"); - private readonly ILogger _logger; - + private static readonly Encoding GameEncoding = Encoding.GetEncoding("windows-1252"); + private readonly IServiceProvider _serviceProvider; + /// /// Base constructor /// /// - public RConConnectionFactory(ILogger logger) + public RConConnectionFactory(IServiceProvider serviceProvider) { - _logger = logger; + _serviceProvider = serviceProvider; } /// @@ -29,9 +33,16 @@ namespace IW4MAdmin.Application.Factories /// port of the server /// rcon password of the server /// - public IRConConnection CreateConnection(string ipAddress, int port, string password) + public IRConConnection CreateConnection(string ipAddress, int port, string password, string rconEngine) { - return new RConConnection(ipAddress, port, password, _logger, gameEncoding); + return rconEngine switch + { + "COD" => new CodRConConnection(ipAddress, port, password, + _serviceProvider.GetRequiredService>(), GameEncoding), + "Source" => new SourceRConConnection(_serviceProvider.GetRequiredService>(), + _serviceProvider.GetRequiredService(), ipAddress, port, password), + _ => throw new ArgumentException($"No supported RCon engine available for '{rconEngine}'") + }; } } -} +} \ No newline at end of file diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 4d826b0ca..a430117ae 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -74,6 +74,8 @@ namespace IW4MAdmin client = await Manager.GetClientService().Create(clientFromLog); } + client.CopyAdditionalProperties(clientFromLog); + // this is only a temporary version until the IPAddress is transmitted client.CurrentAlias = new EFAlias() { @@ -739,11 +741,11 @@ namespace IW4MAdmin /// array index 2 = updated clients /// /// - async Task[]> PollPlayersAsync() + async Task[]> PollPlayersAsync() { var currentClients = GetClientsAsList(); var statusResponse = (await this.GetStatusAsync()); - var polledClients = statusResponse.Item1.AsEnumerable(); + var polledClients = statusResponse.Clients.AsEnumerable(); if (Manager.GetApplicationSettings().Configuration().IgnoreBots) { @@ -753,10 +755,12 @@ namespace IW4MAdmin var connectingClients = polledClients.Except(currentClients); var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients); - UpdateMap(statusResponse.Item2); - UpdateGametype(statusResponse.Item3); + UpdateMap(statusResponse.Map); + UpdateGametype(statusResponse.GameType); + UpdateHostname(statusResponse.Hostname); + UpdateMaxPlayers(statusResponse.MaxClients); - return new List[] + return new [] { connectingClients.ToList(), disconnectingClients.ToList(), @@ -803,6 +807,36 @@ namespace IW4MAdmin } } + private void UpdateHostname(string hostname) + { + if (string.IsNullOrEmpty(hostname) || Hostname == hostname) + { + return; + } + + using(LogContext.PushProperty("Server", ToString())) + { + ServerLogger.LogDebug("Updating hostname to {HostName}", hostname); + } + + Hostname = hostname; + } + + private void UpdateMaxPlayers(int? maxPlayers) + { + if (maxPlayers == null || maxPlayers == MaxClients) + { + return; + } + + using(LogContext.PushProperty("Server", ToString())) + { + ServerLogger.LogDebug("Updating max clients to {MaxPlayers}", maxPlayers); + } + + MaxClients = maxPlayers.Value; + } + private async Task ShutdownInternal() { foreach (var client in GetClientsAsList()) @@ -1011,6 +1045,7 @@ namespace IW4MAdmin RconParser = RconParser ?? Manager.AdditionalRConParsers[0]; EventParser = EventParser ?? Manager.AdditionalEventParsers[0]; + RemoteConnection = RConConnectionFactory.CreateConnection(IP, Port, Password, RconParser.RConEngine); RemoteConnection.SetConfiguration(RconParser); var version = await this.GetMappedDvarValueOrDefaultAsync("version"); @@ -1172,7 +1207,7 @@ namespace IW4MAdmin public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl) { - var logUri = new Uri(logPath); + var logUri = new Uri(logPath, UriKind.Absolute); if (string.IsNullOrEmpty(gameLogServerUrl)) { @@ -1287,8 +1322,12 @@ namespace IW4MAdmin Manager.AddEvent(e); + var temporalClientId = targetClient.GetAdditionalProperty("ConnectionClientId"); + var parsedClientId = string.IsNullOrEmpty(temporalClientId) ? (int?)null : int.Parse(temporalClientId); + var clientNumber = parsedClientId ?? targetClient.ClientNumber; + var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, - targetClient.ClientNumber, + clientNumber, _messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty, previousPenalty)); @@ -1319,8 +1358,12 @@ namespace IW4MAdmin if (targetClient.IsIngame) { + var temporalClientId = targetClient.GetAdditionalProperty("ConnectionClientId"); + var parsedClientId = string.IsNullOrEmpty(temporalClientId) ? (int?)null : int.Parse(temporalClientId); + var clientNumber = parsedClientId ?? targetClient.ClientNumber; + var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, - targetClient.ClientNumber, + clientNumber, _messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty)); ServerLogger.LogDebug("Executing tempban kick command for {targetClient}", targetClient.ToString()); await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick); @@ -1353,8 +1396,13 @@ namespace IW4MAdmin if (targetClient.IsIngame) { ServerLogger.LogDebug("Attempting to kicking newly banned client {targetClient}", targetClient.ToString()); + + var temporalClientId = targetClient.GetAdditionalProperty("ConnectionClientId"); + var parsedClientId = string.IsNullOrEmpty(temporalClientId) ? (int?)null : int.Parse(temporalClientId); + var clientNumber = parsedClientId ?? targetClient.ClientNumber; + var formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, - targetClient.ClientNumber, + clientNumber, _messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty)); await targetClient.CurrentServer.ExecuteCommandAsync(formattedString); } diff --git a/Application/Main.cs b/Application/Main.cs index caa4187e4..ca3b16d51 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -24,6 +24,7 @@ using System.Threading; using System.Threading.Tasks; using Data.Abstractions; using Data.Helpers; +using Integrations.Source.Extensions; using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Localization; using Microsoft.Extensions.Logging; @@ -417,6 +418,8 @@ namespace IW4MAdmin.Application serviceCollection.AddSingleton(); } + serviceCollection.AddSource(); + return serviceCollection; } diff --git a/Application/RconParsers/BaseRConParser.cs b/Application/RConParsers/BaseRConParser.cs similarity index 83% rename from Application/RconParsers/BaseRConParser.cs rename to Application/RConParsers/BaseRConParser.cs index da5c3badc..086df0f5e 100644 --- a/Application/RconParsers/BaseRConParser.cs +++ b/Application/RConParsers/BaseRConParser.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging; using static SharedLibraryCore.Server; using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace IW4MAdmin.Application.RconParsers +namespace IW4MAdmin.Application.RConParsers { public class BaseRConParser : IRConParser { @@ -56,6 +56,7 @@ namespace IW4MAdmin.Application.RconParsers Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5); + Configuration.Dvar.AddMapping(ParserRegex.GroupType.AdditionalGroup, int.MaxValue); Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *"; Configuration.GametypeStatus.Pattern = ""; @@ -73,6 +74,7 @@ namespace IW4MAdmin.Application.RconParsers public Game GameName { get; set; } = Game.COD; public bool CanGenerateLogPath { get; set; } = true; public string Name { get; set; } = "Call of Duty"; + public string RConEngine { get; set; } = "COD"; public async Task ExecuteCommandAsync(IRConConnection connection, string command) { @@ -128,7 +130,7 @@ namespace IW4MAdmin.Application.RconParsers return new Dvar() { - Name = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarName]].Value, + Name = dvarName, Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)), DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)), LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)), @@ -136,53 +138,55 @@ namespace IW4MAdmin.Application.RconParsers }; } - public virtual async Task<(List, string, string)> GetStatusAsync(IRConConnection connection) + public virtual async Task GetStatusAsync(IRConConnection connection) { - string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS); + var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS); _logger.LogDebug("Status Response {response}", string.Join(Environment.NewLine, response)); - return (ClientsFromStatus(response), MapFromStatus(response), GameTypeFromStatus(response)); + return new StatusResponse + { + Clients = ClientsFromStatus(response).ToArray(), + Map = GetValueFromStatus(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus.Pattern), + GameType = GetValueFromStatus(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus.Pattern), + Hostname = GetValueFromStatus(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus.Pattern), + MaxClients = GetValueFromStatus(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus.Pattern) + }; } - private string MapFromStatus(string[] response) + private T GetValueFromStatus(IEnumerable response, ParserRegex.GroupType groupType, string groupPattern) { - string map = null; + if (string.IsNullOrEmpty(groupPattern)) + { + return default; + } + + string value = null; foreach (var line in response) { - var regex = Regex.Match(line, Configuration.MapStatus.Pattern); + var regex = Regex.Match(line, groupPattern); if (regex.Success) { - map = regex.Groups[Configuration.MapStatus.GroupMapping[ParserRegex.GroupType.RConStatusMap]].ToString(); + value = regex.Groups[Configuration.MapStatus.GroupMapping[groupType]].ToString(); } } - return map; - } - - private string GameTypeFromStatus(string[] response) - { - if (string.IsNullOrWhiteSpace(Configuration.GametypeStatus.Pattern)) + if (value == null) { - return null; + return default; } - string gametype = null; - foreach (var line in response) + if (typeof(T) == typeof(int?)) { - var regex = Regex.Match(line, Configuration.GametypeStatus.Pattern); - if (regex.Success) - { - gametype = regex.Groups[Configuration.GametypeStatus.GroupMapping[ParserRegex.GroupType.RConStatusGametype]].ToString(); - } + return (T)Convert.ChangeType(int.Parse(value), Nullable.GetUnderlyingType(typeof(T))); } - - return gametype; + + return (T)Convert.ChangeType(value, typeof(T)); } public async Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue) { string dvarString = (dvarValue is string str) ? $"{dvarName} \"{str}\"" - : $"{dvarName} {dvarValue.ToString()}"; + : $"{dvarName} {dvarValue}"; return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0; } @@ -257,6 +261,11 @@ namespace IW4MAdmin.Application.RconParsers }; client.SetAdditionalProperty("BotGuid", networkIdString); + var additionalGroupIndex = Configuration.Status.GroupMapping[ParserRegex.GroupType.AdditionalGroup]; + if (match.Values.Length > additionalGroupIndex) + { + client.SetAdditionalProperty("ConnectionClientId", match.Values[additionalGroupIndex]); + } StatusPlayers.Add(client); } diff --git a/Application/RconParsers/DynamicRConParser.cs b/Application/RConParsers/DynamicRConParser.cs similarity index 79% rename from Application/RconParsers/DynamicRConParser.cs rename to Application/RConParsers/DynamicRConParser.cs index f642a5b95..051eae6b6 100644 --- a/Application/RconParsers/DynamicRConParser.cs +++ b/Application/RConParsers/DynamicRConParser.cs @@ -1,13 +1,13 @@ using Microsoft.Extensions.Logging; using SharedLibraryCore.Interfaces; -namespace IW4MAdmin.Application.RconParsers +namespace IW4MAdmin.Application.RConParsers { /// /// empty implementation of the IW4RConParser /// allows script plugins to generate dynamic RCon parsers /// - sealed internal class DynamicRConParser : BaseRConParser + internal sealed class DynamicRConParser : BaseRConParser { public DynamicRConParser(ILogger logger, IParserRegexFactory parserRegexFactory) : base(logger, parserRegexFactory) { diff --git a/Application/RconParsers/DynamicRConParserConfiguration.cs b/Application/RConParsers/DynamicRConParserConfiguration.cs similarity index 85% rename from Application/RconParsers/DynamicRConParserConfiguration.cs rename to Application/RConParsers/DynamicRConParserConfiguration.cs index 05b5de611..f70f69fa8 100644 --- a/Application/RconParsers/DynamicRConParserConfiguration.cs +++ b/Application/RConParsers/DynamicRConParserConfiguration.cs @@ -4,7 +4,7 @@ using SharedLibraryCore.RCon; using System.Collections.Generic; using System.Globalization; -namespace IW4MAdmin.Application.RconParsers +namespace IW4MAdmin.Application.RConParsers { /// /// generic implementation of the IRConParserConfiguration @@ -16,6 +16,8 @@ namespace IW4MAdmin.Application.RconParsers public ParserRegex Status { get; set; } public ParserRegex MapStatus { get; set; } public ParserRegex GametypeStatus { get; set; } + public ParserRegex HostnameStatus { get; set; } + public ParserRegex MaxPlayersStatus { get; set; } public ParserRegex Dvar { get; set; } public ParserRegex StatusHeader { get; set; } public string ServerNotRunningResponse { get; set; } @@ -34,6 +36,8 @@ namespace IW4MAdmin.Application.RconParsers GametypeStatus = parserRegexFactory.CreateParserRegex(); Dvar = parserRegexFactory.CreateParserRegex(); StatusHeader = parserRegexFactory.CreateParserRegex(); + HostnameStatus = parserRegexFactory.CreateParserRegex(); + MaxPlayersStatus = parserRegexFactory.CreateParserRegex(); } } } diff --git a/Application/RConParsers/StatusResponse.cs b/Application/RConParsers/StatusResponse.cs new file mode 100644 index 000000000..bd0a52b95 --- /dev/null +++ b/Application/RConParsers/StatusResponse.cs @@ -0,0 +1,15 @@ +using SharedLibraryCore.Database.Models; +using SharedLibraryCore.Interfaces; + +namespace IW4MAdmin.Application.RConParsers +{ + /// + public class StatusResponse : IStatusResponse + { + public string Map { get; set; } + public string GameType { get; set; } + public string Hostname { get; set; } + public int? MaxClients { get; set; } + public EFClient[] Clients { get; set; } + } +} \ No newline at end of file diff --git a/Data/Data.csproj b/Data/Data.csproj index 671d685a0..6697f1ce8 100644 --- a/Data/Data.csproj +++ b/Data/Data.csproj @@ -8,6 +8,7 @@ RaidMax.IW4MAdmin.Data RaidMax.IW4MAdmin.Data + 1.0.1 diff --git a/Data/Models/Reference.cs b/Data/Models/Reference.cs index 3c8757880..60063e43e 100644 --- a/Data/Models/Reference.cs +++ b/Data/Models/Reference.cs @@ -14,7 +14,8 @@ T5 = 6, T6 = 7, T7 = 8, - SHG1 = 9 + SHG1 = 9, + CSGO = 10 } } } \ No newline at end of file diff --git a/Data/Models/SharedEntity.cs b/Data/Models/SharedEntity.cs index bd5271ce3..c7761ebb2 100644 --- a/Data/Models/SharedEntity.cs +++ b/Data/Models/SharedEntity.cs @@ -5,7 +5,7 @@ namespace Data.Models { public class SharedEntity : IPropertyExtender { - private readonly ConcurrentDictionary _additionalProperties; + private ConcurrentDictionary _additionalProperties; /// /// indicates if the entity is active @@ -33,5 +33,10 @@ namespace Data.Models _additionalProperties.TryAdd(name, value); } } + + public void CopyAdditionalProperties(SharedEntity source) + { + _additionalProperties = source._additionalProperties; + } } } diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index 92fd906e6..9f2f3205d 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -47,6 +47,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js Plugins\ScriptPlugins\ParserPlutoniumT4.js = Plugins\ScriptPlugins\ParserPlutoniumT4.js Plugins\ScriptPlugins\ParserS1x.js = Plugins\ScriptPlugins\ParserS1x.js + Plugins\ScriptPlugins\ParserCSGO.js = Plugins\ScriptPlugins\ParserCSGO.js + Plugins\ScriptPlugins\ParserCSGOSM.js = Plugins\ScriptPlugins\ParserCSGOSM.js EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}" @@ -59,6 +61,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationTests", "Tests\A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj", "{81689023-E55E-48ED-B7A8-53F4E21BBF2D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Integrations", "Integrations", "{A2AE33B4-0830-426A-9E11-951DAB12BE5B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integrations.Cod", "Integrations\Cod\Integrations.Cod.csproj", "{A9348433-58C1-4B9C-8BB7-088B02529D9D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integrations.Source", "Integrations\Source\Integrations.Source.csproj", "{9512295B-3045-40E0-9B7E-2409F2173E9D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -362,6 +370,54 @@ Global {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Release|x86.Build.0 = Release|Any CPU {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU {81689023-E55E-48ED-B7A8-53F4E21BBF2D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x64.Build.0 = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x86.ActiveCfg = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x86.Build.0 = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.Build.0 = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|x64.ActiveCfg = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|x64.Build.0 = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|x86.ActiveCfg = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|x86.Build.0 = Debug|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|Any CPU.Build.0 = Release|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x64.ActiveCfg = Release|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x64.Build.0 = Release|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x86.ActiveCfg = Release|Any CPU + {A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x86.Build.0 = Release|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x64.ActiveCfg = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x64.Build.0 = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x86.ActiveCfg = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x86.Build.0 = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.Build.0 = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|x64.ActiveCfg = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|x64.Build.0 = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|x86.ActiveCfg = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|x86.Build.0 = Debug|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|Any CPU.Build.0 = Release|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x64.ActiveCfg = Release|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x64.Build.0 = Release|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x86.ActiveCfg = Release|Any CPU + {9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -376,6 +432,8 @@ Global {F5815359-CFC7-44B4-9A3B-C04BACAD5836} = {26E8B310-269E-46D4-A612-24601F16065F} {00A1FED2-2254-4AF7-A5DB-2357FA7C88CD} = {26E8B310-269E-46D4-A612-24601F16065F} {581FA7AF-FEF6-483C-A7D0-2D13EF50801B} = {3065279E-17F0-4CE0-AF5B-014E04263D77} + {A9348433-58C1-4B9C-8BB7-088B02529D9D} = {A2AE33B4-0830-426A-9E11-951DAB12BE5B} + {9512295B-3045-40E0-9B7E-2409F2173E9D} = {A2AE33B4-0830-426A-9E11-951DAB12BE5B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87} diff --git a/Application/RCon/RConConnection.cs b/Integrations/Cod/CodRConConnection.cs similarity index 98% rename from Application/RCon/RConConnection.cs rename to Integrations/Cod/CodRConConnection.cs index 8e0e8725c..5192b551a 100644 --- a/Application/RCon/RConConnection.cs +++ b/Integrations/Cod/CodRConConnection.cs @@ -1,8 +1,4 @@ -using SharedLibraryCore; -using SharedLibraryCore.Exceptions; -using SharedLibraryCore.Interfaces; -using SharedLibraryCore.RCon; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -14,25 +10,29 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Serilog.Context; +using SharedLibraryCore; +using SharedLibraryCore.Exceptions; +using SharedLibraryCore.Interfaces; +using SharedLibraryCore.RCon; using ILogger = Microsoft.Extensions.Logging.ILogger; -namespace IW4MAdmin.Application.RCon +namespace Integrations.Cod { /// /// implementation of IRConConnection /// - public class RConConnection : IRConConnection + public class CodRConConnection : IRConConnection { static readonly ConcurrentDictionary ActiveQueries = new ConcurrentDictionary(); - public IPEndPoint Endpoint { get; private set; } - public string RConPassword { get; private set; } + public IPEndPoint Endpoint { get; } + public string RConPassword { get; } private IRConParser parser; private IRConParserConfiguration config; private readonly ILogger _log; private readonly Encoding _gameEncoding; - public RConConnection(string ipAddress, int port, string password, ILogger log, Encoding gameEncoding) + public CodRConConnection(string ipAddress, int port, string password, ILogger log, Encoding gameEncoding) { Endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), port); _gameEncoding = gameEncoding; @@ -468,4 +468,4 @@ namespace IW4MAdmin.Application.RCon ActiveQueries[this.Endpoint].OnSentData.Set(); } } -} +} \ No newline at end of file diff --git a/Application/RCon/ConnectionState.cs b/Integrations/Cod/ConnectionState.cs similarity index 96% rename from Application/RCon/ConnectionState.cs rename to Integrations/Cod/ConnectionState.cs index b5401938e..129111aa6 100644 --- a/Application/RCon/ConnectionState.cs +++ b/Integrations/Cod/ConnectionState.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Net.Sockets; using System.Threading; -namespace IW4MAdmin.Application.RCon +namespace Integrations.Cod { /// /// used to keep track of the udp connection state @@ -28,4 +28,4 @@ namespace IW4MAdmin.Application.RCon public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new SocketAsyncEventArgs(); public DateTime LastQuery { get; set; } = DateTime.Now; } -} +} \ No newline at end of file diff --git a/Integrations/Cod/Integrations.Cod.csproj b/Integrations/Cod/Integrations.Cod.csproj new file mode 100644 index 000000000..26f66bae2 --- /dev/null +++ b/Integrations/Cod/Integrations.Cod.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + Integrations.Cod + Integrations.Cod + + + + + + + diff --git a/Integrations/Source/Extensions/IntegrationServicesExtensions.cs b/Integrations/Source/Extensions/IntegrationServicesExtensions.cs new file mode 100644 index 000000000..4cc24f1c9 --- /dev/null +++ b/Integrations/Source/Extensions/IntegrationServicesExtensions.cs @@ -0,0 +1,15 @@ +using Integrations.Source.Interfaces; +using Microsoft.Extensions.DependencyInjection; + +namespace Integrations.Source.Extensions +{ + public static class IntegrationServicesExtensions + { + public static IServiceCollection AddSource(this IServiceCollection services) + { + services.AddSingleton(); + + return services; + } + } +} \ No newline at end of file diff --git a/Integrations/Source/Integrations.Source.csproj b/Integrations/Source/Integrations.Source.csproj new file mode 100644 index 000000000..9835107f9 --- /dev/null +++ b/Integrations/Source/Integrations.Source.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp3.1 + Integrations.Source + Integrations.Source + + + + + + + + + + + diff --git a/Integrations/Source/Interfaces/IRConClientFactory.cs b/Integrations/Source/Interfaces/IRConClientFactory.cs new file mode 100644 index 000000000..92b565046 --- /dev/null +++ b/Integrations/Source/Interfaces/IRConClientFactory.cs @@ -0,0 +1,9 @@ +using RconSharp; + +namespace Integrations.Source.Interfaces +{ + public interface IRConClientFactory + { + RconClient CreateClient(string hostname, int port); + } +} \ No newline at end of file diff --git a/Integrations/Source/RConClientFactory.cs b/Integrations/Source/RConClientFactory.cs new file mode 100644 index 000000000..35e5347b6 --- /dev/null +++ b/Integrations/Source/RConClientFactory.cs @@ -0,0 +1,13 @@ +using Integrations.Source.Interfaces; +using RconSharp; + +namespace Integrations.Source +{ + public class RConClientFactory : IRConClientFactory + { + public RconClient CreateClient(string hostname, int port) + { + return RconClient.Create(hostname, port); + } + } +} \ No newline at end of file diff --git a/Integrations/Source/SourceRConConnection.cs b/Integrations/Source/SourceRConConnection.cs new file mode 100644 index 000000000..5b7fe2323 --- /dev/null +++ b/Integrations/Source/SourceRConConnection.cs @@ -0,0 +1,105 @@ +using System.Linq; +using System.Net.Sockets; +using System.Threading.Tasks; +using Integrations.Source.Interfaces; +using Microsoft.Extensions.Logging; +using RconSharp; +using Serilog.Context; +using SharedLibraryCore; +using SharedLibraryCore.Exceptions; +using SharedLibraryCore.Interfaces; +using SharedLibraryCore.RCon; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Integrations.Source +{ + public class SourceRConConnection : IRConConnection + { + private readonly ILogger _logger; + private readonly string _password; + private readonly string _hostname; + private readonly int _port; + private readonly IRConClientFactory _rconClientFactory; + + private RconClient _rconClient; + + public SourceRConConnection(ILogger logger, IRConClientFactory rconClientFactory, + string hostname, int port, string password) + { + _rconClient = rconClientFactory.CreateClient(hostname, port); + _rconClientFactory = rconClientFactory; + _password = password; + _hostname = hostname; + _port = port; + _logger = logger; + } + + public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") + { + await _rconClient.ConnectAsync(); + + bool authenticated; + + try + { + authenticated = await _rconClient.AuthenticateAsync(_password); + } + catch (SocketException ex) + { + // occurs when the server comes back from hibernation + // this is probably a bug in the library + if (ex.ErrorCode == 10053 || ex.ErrorCode == 10054) + { + using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) + { + _logger.LogWarning(ex, + "Server appears to resumed from hibernation, so we are using a new socket"); + } + + _rconClient = _rconClientFactory.CreateClient(_hostname, _port); + } + + using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) + { + _logger.LogError("Could not login to server"); + } + + throw new NetworkException("Could not authenticate with server"); + } + + if (!authenticated) + { + using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) + { + _logger.LogError("Could not login to server"); + } + + throw new ServerException("Could not authenticate to server with provided password"); + } + + if (type == StaticHelpers.QueryType.COMMAND_STATUS) + { + parameters = "status"; + } + + using (LogContext.PushProperty("Server", $"{_hostname}:{_port}")) + { + _logger.LogDebug("Sending query {Type} with parameters {Parameters}", type, parameters); + } + + var response = await _rconClient.ExecuteCommandAsync(parameters.StripColors(), true); + + using (LogContext.PushProperty("Server", $"{_rconClient.Host}:{_rconClient.Port}")) + { + _logger.LogDebug("Received RCon response {Response}", response); + } + + var split = response.TrimEnd('\n').Split('\n'); + return split.Take(split.Length - 1).ToArray(); + } + + public void SetConfiguration(IRConParser config) + { + } + } +} \ No newline at end of file diff --git a/Plugins/ScriptPlugins/ParserCSGO.js b/Plugins/ScriptPlugins/ParserCSGO.js new file mode 100644 index 000000000..53f96b85e --- /dev/null +++ b/Plugins/ScriptPlugins/ParserCSGO.js @@ -0,0 +1,100 @@ +let rconParser; +let eventParser; + +const plugin = { + author: 'RaidMax', + version: 0.1, + name: 'CS:GO Parser', + engine: 'Source', + isParser: true, + + onEventAsync: function (gameEvent, server) { + }, + + onLoadAsync: function (manager) { + rconParser = manager.GenerateDynamicRConParser(this.engine); + eventParser = manager.GenerateDynamicEventParser(this.engine); + rconParser.RConEngine = this.engine; + + rconParser.Configuration.StatusHeader.Pattern = 'userid +name +uniqueid +connected +ping +loss +state +rate +adr'; + + rconParser.Configuration.MapStatus.Pattern = '^map *: +(.+)$'; + rconParser.Configuration.MapStatus.AddMapping(111, 1); + + rconParser.Configuration.HostnameStatus.Pattern = '^hostname: +(.+)$'; + rconParser.Configuration.MapStatus.AddMapping(113, 1); + + rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d humans, \\d bots \\((\\d+).+'; + rconParser.Configuration.MapStatus.AddMapping(114, 1); + + rconParser.Configuration.Dvar.Pattern = '^"(.+)" = (?:"(.+)" (?:\\( def\\. "(.*)" \\))|"(.+)" +(.+)) +- (.*)$'; + rconParser.Configuration.Dvar.AddMapping(106, 1); + rconParser.Configuration.Dvar.AddMapping(107, 2); + rconParser.Configuration.Dvar.AddMapping(108, 3); + rconParser.Configuration.Dvar.AddMapping(109, 3); + + rconParser.Configuration.Status.Pattern = '^#\\s*(\\d+) (\\d+) "(.+)" (\\S+) (\\d+:\\d+) (\\d+) (\\S+) (\\S+) (\\d+) (\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+)$'; + rconParser.Configuration.Status.AddMapping(100, 2); + rconParser.Configuration.Status.AddMapping(101, 7); + rconParser.Configuration.Status.AddMapping(102, 6); + rconParser.Configuration.Status.AddMapping(103, 4) + rconParser.Configuration.Status.AddMapping(104, 3); + rconParser.Configuration.Status.AddMapping(105, 10); + rconParser.Configuration.Status.AddMapping(200, 1); + + rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1'); + rconParser.Configuration.DefaultDvarValues.Add('version', this.engine); + rconParser.Configuration.DefaultDvarValues.Add('fs_basepath', ''); + rconParser.Configuration.DefaultDvarValues.Add('fs_basegame', ''); + rconParser.Configuration.DefaultDvarValues.Add('g_log', ''); + rconParser.Configuration.DefaultDvarValues.Add('net_ip', 'localhost'); + + rconParser.Configuration.OverrideDvarNameMapping.Add('sv_hostname', 'hostname'); + rconParser.Configuration.OverrideDvarNameMapping.Add('mapname', 'host_map'); + rconParser.Configuration.OverrideDvarNameMapping.Add('sv_maxclients', 'maxplayers'); + rconParser.Configuration.OverrideDvarNameMapping.Add('g_gametype', 'game_type'); // todo: will need gamemode too + rconParser.Configuration.OverrideDvarNameMapping.Add('fs_game', 'game_mode'); + rconParser.Configuration.OverrideDvarNameMapping.Add('g_password', 'sv_password'); + + rconParser.Configuration.NoticeLineSeparator = '. '; + rconParser.CanGenerateLogPath = false; + + rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; + rconParser.Configuration.CommandPrefixes.Kick = 'kickid {0} "{1}"'; + rconParser.Configuration.CommandPrefixes.Ban = 'kickid {0} "{1}"'; + rconParser.Configuration.CommandPrefixes.TempBan = 'kickid {0} "{1}"'; + rconParser.Configuration.CommandPrefixes.Say = 'say {0}'; + rconParser.Configuration.CommandPrefixes.Tell = 'say [{0}] {1}'; // no tell exists in vanilla + + eventParser.Configuration.Say.Pattern = '^"(.+)<(\\d+)><(.+)><(.*?)>" say "(.*)"$'; + eventParser.Configuration.Say.AddMapping(5, 1); + eventParser.Configuration.Say.AddMapping(3, 2); + eventParser.Configuration.Say.AddMapping(1, 3); + eventParser.Configuration.Say.AddMapping(7, 4); + eventParser.Configuration.Say.AddMapping(13, 5); + + eventParser.Configuration.Kill.Pattern = '"(.+)<(\\d+)><(.+)><(.*)>" \\[-?\\d+ -?\\d+ -?\\d+\\] killed "(.+)<(\\d+)><(.+)><(.*)>" \\[-?\\d+ -?\\d+ -?\\d+\\] with "(\\S*)"(.*)$'; + eventParser.Configuration.Kill.AddMapping(5, 1); + eventParser.Configuration.Kill.AddMapping(3, 2); + eventParser.Configuration.Kill.AddMapping(1, 3); + eventParser.Configuration.Kill.AddMapping(7, 4); + eventParser.Configuration.Kill.AddMapping(6, 5); + eventParser.Configuration.Kill.AddMapping(4, 6); + eventParser.Configuration.Kill.AddMapping(2, 7); + eventParser.Configuration.Kill.AddMapping(8, 8); + eventParser.Configuration.Kill.AddMapping(9, 9); + + eventParser.Configuration.Time.Pattern = '^L [01]\\d/[0-3]\\d/\\d+ - [0-2]\\d:[0-5]\\d:[0-5]\\d:'; + + rconParser.Version = 'CSGO'; + rconParser.GameName = 10; // CSGO + eventParser.Version = 'CSGO'; + eventParser.GameName = 10; // CSGO + }, + + onUnloadAsync: function () { + }, + + onTickAsync: function (server) { + } +}; \ No newline at end of file diff --git a/Plugins/ScriptPlugins/ParserCSGOSM.js b/Plugins/ScriptPlugins/ParserCSGOSM.js new file mode 100644 index 000000000..2a97bd2ca --- /dev/null +++ b/Plugins/ScriptPlugins/ParserCSGOSM.js @@ -0,0 +1,100 @@ +let rconParser; +let eventParser; + +const plugin = { + author: 'RaidMax', + version: 0.1, + name: 'CS:GO (SourceMod) Parser', + engine: 'Source', + isParser: true, + + onEventAsync: function (gameEvent, server) { + }, + + onLoadAsync: function (manager) { + rconParser = manager.GenerateDynamicRConParser(this.engine); + eventParser = manager.GenerateDynamicEventParser(this.engine); + rconParser.RConEngine = this.engine; + + rconParser.Configuration.StatusHeader.Pattern = 'userid +name +uniqueid +connected +ping +loss +state +rate +adr'; + + rconParser.Configuration.MapStatus.Pattern = '^map *: +(.+)$'; + rconParser.Configuration.MapStatus.AddMapping(111, 1); + + rconParser.Configuration.HostnameStatus.Pattern = '^hostname: +(.+)$'; + rconParser.Configuration.MapStatus.AddMapping(113, 1); + + rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d humans, \\d bots \\((\\d+).+'; + rconParser.Configuration.MapStatus.AddMapping(114, 1); + + rconParser.Configuration.Dvar.Pattern = '^"(.+)" = (?:"(.+)" (?:\\( def\\. "(.*)" \\))|"(.+)" +(.+)) +- (.*)$'; + rconParser.Configuration.Dvar.AddMapping(106, 1); + rconParser.Configuration.Dvar.AddMapping(107, 2); + rconParser.Configuration.Dvar.AddMapping(108, 3); + rconParser.Configuration.Dvar.AddMapping(109, 3); + + rconParser.Configuration.Status.Pattern = '^#\\s*(\\d+) (\\d+) "(.+)" (\\S+) (\\d+:\\d+) (\\d+) (\\S+) (\\S+) (\\d+) (\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+)$'; + rconParser.Configuration.Status.AddMapping(100, 2); + rconParser.Configuration.Status.AddMapping(101, 7); + rconParser.Configuration.Status.AddMapping(102, 6); + rconParser.Configuration.Status.AddMapping(103, 4) + rconParser.Configuration.Status.AddMapping(104, 3); + rconParser.Configuration.Status.AddMapping(105, 10); + rconParser.Configuration.Status.AddMapping(200, 1); + + rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1'); + rconParser.Configuration.DefaultDvarValues.Add('version', this.engine); + rconParser.Configuration.DefaultDvarValues.Add('fs_basepath', ''); + rconParser.Configuration.DefaultDvarValues.Add('fs_basegame', ''); + rconParser.Configuration.DefaultDvarValues.Add('g_log', ''); + rconParser.Configuration.DefaultDvarValues.Add('net_ip', 'localhost'); + + rconParser.Configuration.OverrideDvarNameMapping.Add('sv_hostname', 'hostname'); + rconParser.Configuration.OverrideDvarNameMapping.Add('mapname', 'host_map'); + rconParser.Configuration.OverrideDvarNameMapping.Add('sv_maxclients', 'maxplayers'); + rconParser.Configuration.OverrideDvarNameMapping.Add('g_gametype', 'game_type'); + rconParser.Configuration.OverrideDvarNameMapping.Add('fs_game', 'game_mode'); + rconParser.Configuration.OverrideDvarNameMapping.Add('g_password', 'sv_password'); + + rconParser.Configuration.NoticeLineSeparator = '. '; + rconParser.CanGenerateLogPath = false; + + rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; + rconParser.Configuration.CommandPrefixes.Kick = 'sm_kick #{0} {1}'; + rconParser.Configuration.CommandPrefixes.Ban = 'sm_kick #{0} {1}'; + rconParser.Configuration.CommandPrefixes.TempBan = 'sm_kick #{0} {1}'; + rconParser.Configuration.CommandPrefixes.Say = 'sm_say {0}'; + rconParser.Configuration.CommandPrefixes.Tell = 'sm_psay #{0} {1}'; + + eventParser.Configuration.Say.Pattern = '^"(.+)<(\\d+)><(.+)><(.*?)>" say "(.*)"$'; + eventParser.Configuration.Say.AddMapping(5, 1); + eventParser.Configuration.Say.AddMapping(3, 2); + eventParser.Configuration.Say.AddMapping(1, 3); + eventParser.Configuration.Say.AddMapping(7, 4); + eventParser.Configuration.Say.AddMapping(13, 5); + + eventParser.Configuration.Kill.Pattern = '"(.+)<(\\d+)><(.+)><(.*)>" \\[-?\\d+ -?\\d+ -?\\d+\\] killed "(.+)<(\\d+)><(.+)><(.*)>" \\[-?\\d+ -?\\d+ -?\\d+\\] with "(\\S*)"(.*)$'; + eventParser.Configuration.Kill.AddMapping(5, 1); + eventParser.Configuration.Kill.AddMapping(3, 2); + eventParser.Configuration.Kill.AddMapping(1, 3); + eventParser.Configuration.Kill.AddMapping(7, 4); + eventParser.Configuration.Kill.AddMapping(6, 5); + eventParser.Configuration.Kill.AddMapping(4, 6); + eventParser.Configuration.Kill.AddMapping(2, 7); + eventParser.Configuration.Kill.AddMapping(8, 8); + eventParser.Configuration.Kill.AddMapping(9, 9); + + eventParser.Configuration.Time.Pattern = '^L [01]\\d/[0-3]\\d/\\d+ - [0-2]\\d:[0-5]\\d:[0-5]\\d:'; + + rconParser.Version = 'CSGOSM'; + rconParser.GameName = 10; // CSGO + eventParser.Version = 'CSGOSM'; + eventParser.GameName = 10; // CSGO + }, + + onUnloadAsync: function () { + }, + + onTickAsync: function (server) { + } +}; \ No newline at end of file diff --git a/Plugins/Stats/Client/HitCalculator.cs b/Plugins/Stats/Client/HitCalculator.cs index 8b23a0a47..02bf1c12e 100644 --- a/Plugins/Stats/Client/HitCalculator.cs +++ b/Plugins/Stats/Client/HitCalculator.cs @@ -176,6 +176,12 @@ namespace IW4MAdmin.Plugins.Stats.Client foreach (var hitInfo in new[] {attackerHitInfo, victimHitInfo}) { + if (hitInfo.MeansOfDeath == null || hitInfo.Location == null || hitInfo.Weapon == null) + { + _logger.LogDebug("Skipping hit because it does not contain the required data"); + continue; + } + try { await _onTransaction.WaitAsync(); diff --git a/Plugins/Stats/Client/HitInfoBuilder.cs b/Plugins/Stats/Client/HitInfoBuilder.cs index e203b3b48..698eef11c 100644 --- a/Plugins/Stats/Client/HitInfoBuilder.cs +++ b/Plugins/Stats/Client/HitInfoBuilder.cs @@ -15,13 +15,13 @@ namespace Stats.Client private readonly IWeaponNameParser _weaponNameParser; private readonly ILogger _logger; private const int MaximumDamage = 1000; - + public HitInfoBuilder(ILogger logger, IWeaponNameParser weaponNameParser) { _weaponNameParser = weaponNameParser; _logger = logger; } - + public HitInfo Build(string[] log, int entityId, bool isSelf, bool isVictim, Server.Game gameName) { var eventType = log[(uint) ParserRegex.GroupType.EventType].First(); @@ -50,11 +50,19 @@ namespace Stats.Client EntityId = entityId, IsVictim = isVictim, HitType = hitType, - Damage = Math.Min(MaximumDamage, int.Parse(log[(uint) ParserRegex.GroupType.Damage])), - Location = log[(uint) ParserRegex.GroupType.HitLocation], - Weapon = _weaponNameParser.Parse(log[(uint) ParserRegex.GroupType.Weapon], gameName), - MeansOfDeath = log[(uint)ParserRegex.GroupType.MeansOfDeath], - Game = (Reference.Game)gameName + Damage = Math.Min(MaximumDamage, + log.Length > (uint) ParserRegex.GroupType.Damage + ? int.Parse(log[(uint) ParserRegex.GroupType.Damage]) + : 0), + Location = log.Length > (uint) ParserRegex.GroupType.HitLocation + ? log[(uint) ParserRegex.GroupType.HitLocation] + : "Unknown", + Weapon = log.Length == 10 ? _weaponNameParser.Parse(log[8], gameName) + : _weaponNameParser.Parse(log[(uint) ParserRegex.GroupType.Weapon], gameName), + MeansOfDeath = log.Length > (uint) ParserRegex.GroupType.MeansOfDeath + ? log[(uint) ParserRegex.GroupType.MeansOfDeath] + : "Unknown", + Game = (Reference.Game) gameName }; //_logger.LogDebug("Generated new hitInfo {@hitInfo}", hitInfo); diff --git a/Plugins/Stats/Client/WeaponNameParser.cs b/Plugins/Stats/Client/WeaponNameParser.cs index d65f0a432..a559660c8 100644 --- a/Plugins/Stats/Client/WeaponNameParser.cs +++ b/Plugins/Stats/Client/WeaponNameParser.cs @@ -6,6 +6,7 @@ using System.Linq; using IW4MAdmin.Plugins.Stats.Config; using SharedLibraryCore; using SharedLibraryCore.Interfaces; +using Stats.Config; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Stats.Client @@ -24,22 +25,16 @@ namespace Stats.Client public WeaponInfo Parse(string weaponName, Server.Game gameName) { var configForGame = _config.WeaponNameParserConfigurations - ?.FirstOrDefault(config => config.Game == gameName); - - if (configForGame == null) + ?.FirstOrDefault(config => config.Game == gameName) ?? new WeaponNameParserConfiguration() { - _logger.LogWarning("No weapon parser config available for game {game}", gameName); - return new WeaponInfo() - { - Name = "Unknown" - }; - } - + Game = gameName + }; + var splitWeaponName = weaponName.Split(configForGame.Delimiters); if (!splitWeaponName.Any()) { - _logger.LogError("Could not parse weapon name {weapon}", weaponName); + _logger.LogError("Could not parse weapon name {Weapon}", weaponName); return new WeaponInfo() { diff --git a/SharedLibraryCore/Helpers/ParserRegex.cs b/SharedLibraryCore/Helpers/ParserRegex.cs index 379d19b8c..2e083c99e 100644 --- a/SharedLibraryCore/Helpers/ParserRegex.cs +++ b/SharedLibraryCore/Helpers/ParserRegex.cs @@ -11,20 +11,20 @@ namespace SharedLibraryCore.Interfaces /// public enum GroupType { - EventType, - OriginNetworkId, - TargetNetworkId, - OriginClientNumber, - TargetClientNumber, - OriginName, - TargetName, - OriginTeam, - TargetTeam, - Weapon, - Damage, - MeansOfDeath, - HitLocation, - Message, + EventType = 0, + OriginNetworkId = 1, + TargetNetworkId = 2, + OriginClientNumber = 3, + TargetClientNumber = 4, + OriginName = 5, + TargetName = 6, + OriginTeam = 7, + TargetTeam = 8, + Weapon = 9, + Damage = 10, + MeansOfDeath = 11, + HitLocation = 12, + Message = 13, RConClientNumber = 100, RConScore = 101, RConPing = 102, @@ -38,6 +38,8 @@ namespace SharedLibraryCore.Interfaces RConDvarDomain = 110, RConStatusMap = 111, RConStatusGametype = 112, + RConStatusHostname = 113, + RConStatusMaxPlayers = 114, AdditionalGroup = 200 } diff --git a/SharedLibraryCore/Interfaces/IEventParserConfiguration.cs b/SharedLibraryCore/Interfaces/IEventParserConfiguration.cs index a6f5870de..43975b7fa 100644 --- a/SharedLibraryCore/Interfaces/IEventParserConfiguration.cs +++ b/SharedLibraryCore/Interfaces/IEventParserConfiguration.cs @@ -45,6 +45,16 @@ namespace SharedLibraryCore.Interfaces /// ParserRegex Time { get; set; } + /// + /// stores the regex information for the map change game log + /// + ParserRegex MapChange { get; } + + /// + /// stores the regex information for the map end game log + /// + ParserRegex MapEnd { get; } + /// /// indicates the format expected for parsed guids /// diff --git a/SharedLibraryCore/Interfaces/IRConConnectionFactory.cs b/SharedLibraryCore/Interfaces/IRConConnectionFactory.cs index ae06457bf..025a5a5a7 100644 --- a/SharedLibraryCore/Interfaces/IRConConnectionFactory.cs +++ b/SharedLibraryCore/Interfaces/IRConConnectionFactory.cs @@ -11,7 +11,8 @@ /// ip address of the server /// port of the server /// password of the server + /// engine to create the rcon connection to /// instance of rcon connection - IRConConnection CreateConnection(string ipAddress, int port, string password); + IRConConnection CreateConnection(string ipAddress, int port, string password, string rconEngine); } } diff --git a/SharedLibraryCore/Interfaces/IRConParser.cs b/SharedLibraryCore/Interfaces/IRConParser.cs index 8a03ed01f..7fd8046c3 100644 --- a/SharedLibraryCore/Interfaces/IRConParser.cs +++ b/SharedLibraryCore/Interfaces/IRConParser.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; -using SharedLibraryCore.Database.Models; using static SharedLibraryCore.Server; namespace SharedLibraryCore.Interfaces @@ -39,8 +37,8 @@ namespace SharedLibraryCore.Interfaces /// get the list of connected clients from status response /// /// RCon connection to use - /// list of clients, current map, and current gametype - Task<(List, string, string)> GetStatusAsync(IRConConnection connection); + /// + Task GetStatusAsync(IRConConnection connection); /// /// stores the RCon configuration @@ -50,23 +48,29 @@ namespace SharedLibraryCore.Interfaces /// /// stores the game/client specific version (usually the value of the "version" DVAR) /// - string Version { get; set; } + string Version { get; } /// /// specifies the game name (usually the internal studio iteration ie: IW4, T5 etc...) /// - Game GameName { get; set; } + Game GameName { get; } /// /// indicates if the game supports generating a log path from DVAR retrieval /// of fs_game, fs_basepath, g_log /// - bool CanGenerateLogPath { get; set; } + bool CanGenerateLogPath { get; } /// /// specifies the name of the parser /// - string Name { get; set; } + string Name { get; } + + /// + /// specifies the type of rcon engine + /// eg: COD, Source + /// + string RConEngine { get; } /// /// retrieves the value of given dvar key if it exists in the override dict diff --git a/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs b/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs index d53c8f923..706e6a85a 100644 --- a/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs +++ b/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs @@ -9,59 +9,69 @@ namespace SharedLibraryCore.Interfaces /// /// stores the command format for console commands /// - CommandPrefix CommandPrefixes { get; set; } + CommandPrefix CommandPrefixes { get; } /// /// stores the regex info for parsing get status response /// - ParserRegex Status { get; set; } + ParserRegex Status { get; } /// /// stores regex info for parsing the map line from rcon status response /// - ParserRegex MapStatus { get; set; } + ParserRegex MapStatus { get; } /// /// stores regex info for parsing the gametype line from rcon status response /// - ParserRegex GametypeStatus { get; set; } + ParserRegex GametypeStatus { get; } + + /// + /// stores regex info for parsing hostname line from rcon status response + /// + ParserRegex HostnameStatus { get; } + + /// + /// stores regex info for parsing max players line from rcon status response + /// + ParserRegex MaxPlayersStatus { get; } /// /// stores the regex info for parsing get DVAR responses /// - ParserRegex Dvar { get; set; } + ParserRegex Dvar { get; } /// /// stores the regex info for parsing the header of a status response /// - ParserRegex StatusHeader { get; set; } + ParserRegex StatusHeader { get; } /// /// Specifies the expected response message from rcon when the server is not running /// - string ServerNotRunningResponse { get; set; } + string ServerNotRunningResponse { get; } /// /// indicates if the application should wait for response from server /// when executing a command /// - bool WaitForResponse { get; set; } + bool WaitForResponse { get; } /// /// indicates the format expected for parsed guids /// - NumberStyles GuidNumberStyle { get; set; } + NumberStyles GuidNumberStyle { get; } /// /// specifies simple mappings for dvar names in scenarios where the needed /// information is not stored in a traditional dvar name /// - IDictionary OverrideDvarNameMapping { get; set; } + IDictionary OverrideDvarNameMapping { get; } /// /// specifies the default dvar values for games that don't support certain dvars /// - IDictionary DefaultDvarValues { get; set; } + IDictionary DefaultDvarValues { get; } /// /// specifies how many lines can be used for ingame notice @@ -71,11 +81,11 @@ namespace SharedLibraryCore.Interfaces /// /// specifies how many characters can be displayed per notice line /// - int NoticeMaxCharactersPerLine { get; set; } + int NoticeMaxCharactersPerLine { get; } /// /// specifies the characters used to split a line /// - string NoticeLineSeparator { get; set; } + string NoticeLineSeparator { get; } } } diff --git a/SharedLibraryCore/Interfaces/IStatusResponse.cs b/SharedLibraryCore/Interfaces/IStatusResponse.cs new file mode 100644 index 000000000..623126c17 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IStatusResponse.cs @@ -0,0 +1,35 @@ +using SharedLibraryCore.Database.Models; + +namespace SharedLibraryCore.Interfaces +{ + /// + /// describes the collection of data returned from a status query + /// + public interface IStatusResponse + { + /// + /// name of the map + /// + string Map { get; } + + /// + /// gametype/mode + /// + string GameType { get; } + + /// + /// server name + /// + string Hostname { get; } + + /// + /// max number of players + /// + int? MaxClients { get; } + + /// + /// active clients + /// + EFClient[] Clients { get; } + } +} \ No newline at end of file diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 9a500aef3..96672ab10 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -29,7 +29,8 @@ namespace SharedLibraryCore T5 = 6, T6 = 7, T7 = 8, - SHG1 = 9 + SHG1 = 9, + CSGO = 10 } public Server(ILogger logger, SharedLibraryCore.Interfaces.ILogger deprecatedLogger, @@ -42,7 +43,6 @@ namespace SharedLibraryCore Manager = mgr; Logger = deprecatedLogger; ServerConfig = config; - RemoteConnection = rconConnectionFactory.CreateConnection(IP, Port, Password); EventProcessing = new SemaphoreSlim(1, 1); Clients = new List(new EFClient[64]); Reports = new List(); @@ -52,6 +52,7 @@ namespace SharedLibraryCore CustomSayEnabled = Manager.GetApplicationSettings().Configuration().EnableCustomSayName; CustomSayName = Manager.GetApplicationSettings().Configuration().CustomSayName; this.gameLogReaderFactory = gameLogReaderFactory; + RConConnectionFactory = rconConnectionFactory; ServerLogger = logger; InitializeTokens(); InitializeAutoMessages(); @@ -158,24 +159,28 @@ namespace SharedLibraryCore /// Send a message to a particular players /// /// Message to send - /// EFClient to send message to - protected async Task Tell(string message, EFClient target) + /// EFClient to send message to + protected async Task Tell(string message, EFClient targetClient) { if (!Utilities.IsDevelopment) { + var temporalClientId = targetClient.GetAdditionalProperty("ConnectionClientId"); + var parsedClientId = string.IsNullOrEmpty(temporalClientId) ? (int?)null : int.Parse(temporalClientId); + var clientNumber = parsedClientId ?? targetClient.ClientNumber; + var formattedMessage = string.Format(RconParser.Configuration.CommandPrefixes.Tell, - target.ClientNumber, + clientNumber, $"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{message.FixIW4ForwardSlash()}"); - if (target.ClientNumber > -1 && message.Length > 0 && target.Level != EFClient.Permission.Console) + if (targetClient.ClientNumber > -1 && message.Length > 0 && targetClient.Level != EFClient.Permission.Console) await this.ExecuteCommandAsync(formattedMessage); } else { - ServerLogger.LogDebug("Tell[{clientNumber}]->{message}", target.ClientNumber, message.StripColors()); + ServerLogger.LogDebug("Tell[{clientNumber}]->{message}", targetClient.ClientNumber, message.StripColors()); } - if (target.Level == EFClient.Permission.Console) + if (targetClient.Level == EFClient.Permission.Console) { Console.ForegroundColor = ConsoleColor.Green; using (LogContext.PushProperty("Server", ToString())) @@ -340,6 +345,7 @@ namespace SharedLibraryCore protected DateTime LastPoll; protected ManualResetEventSlim OnRemoteCommandResponse; protected IGameLogReaderFactory gameLogReaderFactory; + protected IRConConnectionFactory RConConnectionFactory; // only here for performance private readonly bool CustomSayEnabled; diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 39eed5aec..285e9d758 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -44,7 +44,7 @@ - + diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index 2ce37529c..901cd125d 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -322,6 +322,8 @@ namespace SharedLibraryCore /// public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, long? fallback = null) { + // added for source games that provide the steam ID + str = str.Replace("STEAM_1", "").Replace(":", ""); str = str.Substring(0, Math.Min(str.Length, 19)); var parsableAsNumber = Regex.Match(str, @"([A-F]|[a-f]|[0-9])+").Value; @@ -732,7 +734,7 @@ namespace SharedLibraryCore return await server.RconParser.ExecuteCommandAsync(server.RemoteConnection, commandName); } - public static Task<(List, string, string)> GetStatusAsync(this Server server) + public static Task GetStatusAsync(this Server server) { return server.RconParser.GetStatusAsync(server.RemoteConnection); } diff --git a/WebfrontCore/Views/Client/Statistics/Advanced.cshtml b/WebfrontCore/Views/Client/Statistics/Advanced.cshtml index 6ae182828..d49de81c3 100644 --- a/WebfrontCore/Views/Client/Statistics/Advanced.cshtml +++ b/WebfrontCore/Views/Client/Statistics/Advanced.cshtml @@ -56,7 +56,7 @@ } var weapons = Model.ByWeapon - .Where(hit => hit.DamageInflicted > 0) + .Where(hit => hit.DamageInflicted > 0 || (hit.DamageInflicted == 0 && hit.HitCount > 0)) .GroupBy(hit => new {hit.WeaponId}) .Select(group => {