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 =>
{