diff --git a/.gitignore b/.gitignore index 8f7e6f1cc..8b804f4d3 100644 --- a/.gitignore +++ b/.gitignore @@ -242,3 +242,4 @@ launchSettings.json /WebfrontCore/wwwroot/font /Plugins/Tests/TestSourceFiles /Tests/ApplicationTests/Files/GameEvents.json +/Tests/ApplicationTests/Files/replay.json diff --git a/Application/Application.csproj b/Application/Application.csproj index 4f37f0940..1951099e9 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -10,7 +10,7 @@ Forever None IW4MAdmin IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated servers - 2019 + 2020 https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE https://raidmax.org/IW4MAdmin https://github.com/RaidMax/IW4M-Admin diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index 9729dc426..6ace023c0 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -31,7 +31,7 @@ namespace IW4MAdmin.Application private readonly ConcurrentBag _servers; public List Servers => _servers.OrderByDescending(s => s.ClientNum).ToList(); public ILogger Logger => GetLogger(0); - public bool Running { get; private set; } + public bool IsRunning { get; private set; } public bool IsInitialized { get; private set; } public DateTime StartTime { get; private set; } public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString(); @@ -50,7 +50,6 @@ namespace IW4MAdmin.Application readonly AliasService AliasSvc; readonly PenaltyService PenaltySvc; public IConfigurationHandler ConfigHandler; - GameEventHandler Handler; readonly IPageList PageList; private readonly Dictionary _loggers = new Dictionary(); private readonly MetaService _metaService; @@ -62,11 +61,13 @@ namespace IW4MAdmin.Application private readonly IGameServerInstanceFactory _serverInstanceFactory; private readonly IParserRegexFactory _parserRegexFactory; private readonly IEnumerable _customParserEvents; + private readonly IEventHandler _eventHandler; public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable commands, ITranslationLookup translationLookup, IConfigurationHandler commandConfiguration, IConfigurationHandler appConfigHandler, IGameServerInstanceFactory serverInstanceFactory, - IEnumerable plugins, IParserRegexFactory parserRegexFactory, IEnumerable customParserEvents) + IEnumerable plugins, IParserRegexFactory parserRegexFactory, IEnumerable customParserEvents, + IEventHandler eventHandler) { MiddlewareActionHandler = actionHandler; _servers = new ConcurrentBag(); @@ -90,6 +91,7 @@ namespace IW4MAdmin.Application _serverInstanceFactory = serverInstanceFactory; _parserRegexFactory = parserRegexFactory; _customParserEvents = customParserEvents; + _eventHandler = eventHandler; Plugins = plugins; } @@ -255,7 +257,7 @@ namespace IW4MAdmin.Application public async Task Init() { - Running = true; + IsRunning = true; ExternalIPAddress = await Utilities.GetExternalIP(); #region PLUGINS @@ -589,9 +591,6 @@ namespace IW4MAdmin.Application async Task Init(ServerConfiguration Conf) { - // setup the event handler after the class is initialized - Handler = new GameEventHandler(this); - try { // todo: this might not always be an IW4MServer @@ -610,7 +609,7 @@ namespace IW4MAdmin.Application Owner = ServerInstance }; - Handler.AddEvent(e); + AddEvent(e); successServers++; } @@ -726,7 +725,7 @@ namespace IW4MAdmin.Application public void Stop() { _tokenSource.Cancel(); - Running = false; + IsRunning = false; } public void Restart() @@ -782,9 +781,9 @@ namespace IW4MAdmin.Application return ConfigHandler; } - public IEventHandler GetEventHandler() + public void AddEvent(GameEvent gameEvent) { - return Handler; + _eventHandler.HandleEvent(this, gameEvent); } public IPageList GetPageList() diff --git a/Application/EventParsers/BaseEventParser.cs b/Application/EventParsers/BaseEventParser.cs index 7550a3113..d07ddd5bb 100644 --- a/Application/EventParsers/BaseEventParser.cs +++ b/Application/EventParsers/BaseEventParser.cs @@ -23,26 +23,26 @@ namespace IW4MAdmin.Application.EventParsers GameDirectory = "main", }; - Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32});([0-9]+);(.+);(.*)$"; + Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(.+);(.*)$"; Configuration.Say.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Say.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2); Configuration.Say.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3); Configuration.Say.AddMapping(ParserRegex.GroupType.OriginName, 4); Configuration.Say.AddMapping(ParserRegex.GroupType.Message, 5); - Configuration.Quit.Pattern = @"^(Q);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);([0-9]+);(.*)$"; + Configuration.Quit.Pattern = @"^(Q);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(.*)$"; Configuration.Quit.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2); Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3); Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginName, 4); - Configuration.Join.Pattern = @"^(J);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);([0-9]+);(.*)$"; + Configuration.Join.Pattern = @"^(J);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(.*)$"; Configuration.Join.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Join.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2); Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3); Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4); - Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$"; + Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$"; Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2); Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3); @@ -57,7 +57,7 @@ namespace IW4MAdmin.Application.EventParsers Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12); Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13); - Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$"; + Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$"; Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2); Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3); @@ -118,7 +118,13 @@ namespace IW4MAdmin.Application.EventParsers if (message.Length > 0) { - long originId = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle); + string originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString(); + string originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(); + + long originId = originIdString.IsBotGuid() ? + originName.GenerateGuidFromString() : + originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); + int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); // todo: these need to defined outside of here @@ -132,7 +138,8 @@ namespace IW4MAdmin.Application.EventParsers Message = message, Extra = logLine, RequiredEntity = GameEvent.EventRequiredEntity.Origin, - GameTime = gameTime + GameTime = gameTime, + Source = GameEvent.EventSource.Log }; } @@ -144,7 +151,8 @@ namespace IW4MAdmin.Application.EventParsers Message = message, Extra = logLine, RequiredEntity = GameEvent.EventRequiredEntity.Origin, - GameTime = gameTime + GameTime = gameTime, + Source = GameEvent.EventSource.Log }; } } @@ -156,8 +164,18 @@ namespace IW4MAdmin.Application.EventParsers if (match.Success) { - long originId = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1); - long targetId = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1); + 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(); + + long originId = originIdString.IsBotGuid() ? + originName.GenerateGuidFromString() : + originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); + long 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]]); @@ -168,7 +186,8 @@ namespace IW4MAdmin.Application.EventParsers Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber }, Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber }, RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target, - GameTime = gameTime + GameTime = gameTime, + Source = GameEvent.EventSource.Log }; } } @@ -179,8 +198,18 @@ namespace IW4MAdmin.Application.EventParsers if (match.Success) { - long originId = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1); - long targetId = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1); + 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(); + + long originId = originIdString.IsBotGuid() ? + originName.GenerateGuidFromString() : + originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); + long 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]]); @@ -191,7 +220,8 @@ namespace IW4MAdmin.Application.EventParsers Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber }, Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber }, RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target, - GameTime = gameTime + GameTime = gameTime, + Source = GameEvent.EventSource.Log }; } } @@ -202,6 +232,13 @@ namespace IW4MAdmin.Application.EventParsers 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(); + + long networkId = originIdString.IsBotGuid() ? + originName.GenerateGuidFromString() : + originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); + return new GameEvent() { Type = GameEvent.EventType.PreConnect, @@ -212,13 +249,14 @@ namespace IW4MAdmin.Application.EventParsers { Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine(), }, - NetworkId = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle), + NetworkId = networkId, ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()), State = EFClient.ClientState.Connecting, }, RequiredEntity = GameEvent.EventRequiredEntity.None, IsBlocking = true, - GameTime = gameTime + GameTime = gameTime, + Source = GameEvent.EventSource.Log }; } } @@ -229,6 +267,13 @@ namespace IW4MAdmin.Application.EventParsers 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(); + + long networkId = originIdString.IsBotGuid() ? + originName.GenerateGuidFromString() : + originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); + return new GameEvent() { Type = GameEvent.EventType.PreDisconnect, @@ -239,13 +284,14 @@ namespace IW4MAdmin.Application.EventParsers { Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine() }, - NetworkId = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle), + NetworkId = networkId, ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()), State = EFClient.ClientState.Disconnecting }, RequiredEntity = GameEvent.EventRequiredEntity.None, IsBlocking = true, - GameTime = gameTime + GameTime = gameTime, + Source = GameEvent.EventSource.Log }; } } @@ -259,7 +305,8 @@ namespace IW4MAdmin.Application.EventParsers Origin = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(), RequiredEntity = GameEvent.EventRequiredEntity.None, - GameTime = gameTime + GameTime = gameTime, + Source = GameEvent.EventSource.Log }; } @@ -275,7 +322,8 @@ namespace IW4MAdmin.Application.EventParsers Target = Utilities.IW4MAdminClient(), Extra = dump.DictionaryFromKeyValue(), RequiredEntity = GameEvent.EventRequiredEntity.None, - GameTime = gameTime + GameTime = gameTime, + Source = GameEvent.EventSource.Log }; } @@ -290,7 +338,8 @@ namespace IW4MAdmin.Application.EventParsers Type = GameEvent.EventType.Other, Data = logLine, Subtype = eventModifier.Item1, - GameTime = gameTime + GameTime = gameTime, + Source = GameEvent.EventSource.Log }); } @@ -307,7 +356,8 @@ namespace IW4MAdmin.Application.EventParsers Origin = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(), RequiredEntity = GameEvent.EventRequiredEntity.None, - GameTime = gameTime + GameTime = gameTime, + Source = GameEvent.EventSource.Log }; } diff --git a/Application/Factories/GameLogReaderFactory.cs b/Application/Factories/GameLogReaderFactory.cs new file mode 100644 index 000000000..53854e8b0 --- /dev/null +++ b/Application/Factories/GameLogReaderFactory.cs @@ -0,0 +1,33 @@ +using IW4MAdmin.Application.IO; +using Microsoft.Extensions.DependencyInjection; +using SharedLibraryCore.Interfaces; +using System; + +namespace IW4MAdmin.Application.Factories +{ + public class GameLogReaderFactory : IGameLogReaderFactory + { + private readonly IServiceProvider _serviceProvider; + + public GameLogReaderFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public IGameLogReader CreateGameLogReader(Uri[] logUris, IEventParser eventParser) + { + var baseUri = logUris[0]; + if (baseUri.Scheme == Uri.UriSchemeHttp) + { + return new GameLogReaderHttp(logUris, eventParser, _serviceProvider.GetRequiredService()); + } + + else if (baseUri.Scheme == Uri.UriSchemeFile) + { + return new GameLogReader(baseUri.LocalPath, eventParser, _serviceProvider.GetRequiredService()); + } + + throw new NotImplementedException($"No log reader implemented for Uri scheme \"{baseUri.Scheme}\""); + } + } +} diff --git a/Application/Factories/GameServerInstanceFactory.cs b/Application/Factories/GameServerInstanceFactory.cs index 3f4f9e7b8..afab231a0 100644 --- a/Application/Factories/GameServerInstanceFactory.cs +++ b/Application/Factories/GameServerInstanceFactory.cs @@ -12,16 +12,18 @@ namespace IW4MAdmin.Application.Factories { private readonly ITranslationLookup _translationLookup; private readonly IRConConnectionFactory _rconConnectionFactory; + private readonly IGameLogReaderFactory _gameLogReaderFactory; /// /// base constructor /// /// /// - public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory) + public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory) { _translationLookup = translationLookup; _rconConnectionFactory = rconConnectionFactory; + _gameLogReaderFactory = gameLogReaderFactory; } /// @@ -32,7 +34,7 @@ namespace IW4MAdmin.Application.Factories /// public Server CreateServer(ServerConfiguration config, IManager manager) { - return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory); + return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory, _gameLogReaderFactory); } } } diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs index 8a8d25ea4..f43b646f6 100644 --- a/Application/GameEventHandler.cs +++ b/Application/GameEventHandler.cs @@ -1,19 +1,19 @@ using IW4MAdmin.Application.Misc; +using Newtonsoft.Json; using SharedLibraryCore; using SharedLibraryCore.Events; using SharedLibraryCore.Interfaces; using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace IW4MAdmin.Application { - class GameEventHandler : IEventHandler + public class GameEventHandler : IEventHandler { - private const int MAX_CONCURRENT_EVENTS = 10; - private readonly ApplicationManager _manager; - private readonly SemaphoreSlim _processingEvents; + private readonly EventLog _eventLog; private static readonly GameEvent.EventType[] overrideEvents = new[] { GameEvent.EventType.Connect, @@ -22,39 +22,12 @@ namespace IW4MAdmin.Application GameEvent.EventType.Stop }; - public GameEventHandler(IManager mgr) + public GameEventHandler() { - _manager = (ApplicationManager)mgr; - _processingEvents = new SemaphoreSlim(MAX_CONCURRENT_EVENTS, MAX_CONCURRENT_EVENTS); + _eventLog = new EventLog(); } - private Task GameEventHandler_GameEventAdded(object sender, GameEventArgs args) - { - try - { - // this is not elegant and there's probably a much better way to do it, but it works for now - _processingEvents.Wait(); - EventApi.OnGameEvent(sender, args); - return _manager.ExecuteEvent(args.Event); - } - - catch - { - - } - - finally - { - if (_processingEvents.CurrentCount < MAX_CONCURRENT_EVENTS) - { - _processingEvents.Release(); - } - } - - return Task.CompletedTask; - } - - public void AddEvent(GameEvent gameEvent) + public void HandleEvent(IManager manager, GameEvent gameEvent) { #if DEBUG ThreadPool.GetMaxThreads(out int workerThreads, out int n); @@ -62,12 +35,23 @@ namespace IW4MAdmin.Application gameEvent.Owner.Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks"); #endif - if (_manager.Running || overrideEvents.Contains(gameEvent.Type)) + if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type)) { #if DEBUG gameEvent.Owner.Logger.WriteDebug($"Adding event with id {gameEvent.Id}"); #endif - Task.Run(() => GameEventHandler_GameEventAdded(this, new GameEventArgs(null, false, gameEvent))); + + EventApi.OnGameEvent(gameEvent); + Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent)); + + /*if (!_eventLog.ContainsKey(gameEvent.Owner.EndPoint)) + { + _eventLog.Add(gameEvent.Owner.EndPoint,new List()); + } + _eventLog[gameEvent.Owner.EndPoint].Add(gameEvent); + string serializedEvents = JsonConvert.SerializeObject(_eventLog, EventLog.BuildVcrSerializationSettings()); + System.IO.File.WriteAllText("output.json", serializedEvents);*/ + //Task.Run(() => GameEventHandler_GameEventAdded(this, new GameEventArgs(null, false, gameEvent))); } #if DEBUG else diff --git a/Application/IO/GameLogEventDetection.cs b/Application/IO/GameLogEventDetection.cs index 5055aad7c..be9468a89 100644 --- a/Application/IO/GameLogEventDetection.cs +++ b/Application/IO/GameLogEventDetection.cs @@ -13,17 +13,9 @@ namespace IW4MAdmin.Application.IO private readonly IGameLogReader _reader; private readonly bool _ignoreBots; - class EventState + public GameLogEventDetection(Server server, Uri[] gameLogUris, IGameLogReaderFactory gameLogReaderFactory) { - public ILogger Log { get; set; } - public string ServerId { get; set; } - } - - public GameLogEventDetection(Server server, string gameLogPath, Uri gameLogServerUri, IGameLogReader reader = null) - { - _reader = gameLogServerUri != null - ? reader ?? new GameLogReaderHttp(gameLogServerUri, gameLogPath, server.EventParser) - : reader ?? new GameLogReader(gameLogPath, server.EventParser); + _reader = gameLogReaderFactory.CreateGameLogReader(gameLogUris, server.EventParser); _server = server; _ignoreBots = server?.Manager.GetApplicationSettings().Configuration().IgnoreBots ?? false; } @@ -70,7 +62,7 @@ namespace IW4MAdmin.Application.IO return; } - var events = await _reader.ReadEventsFromLog(_server, fileDiff, previousFileSize); + var events = await _reader.ReadEventsFromLog(fileDiff, previousFileSize); foreach (var gameEvent in events) { @@ -84,9 +76,9 @@ namespace IW4MAdmin.Application.IO // we don't want to add the event if ignoreBots is on and the event comes from a bot if (!_ignoreBots || (_ignoreBots && !((gameEvent.Origin?.IsBot ?? false) || (gameEvent.Target?.IsBot ?? false)))) { - if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != 1) + if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != Utilities.WORLD_ID) { - gameEvent.Origin = _server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId); + gameEvent.Origin = _server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId);; } if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target) @@ -104,7 +96,7 @@ namespace IW4MAdmin.Application.IO gameEvent.Target.CurrentServer = _server; } - _server.Manager.GetEventHandler().AddEvent(gameEvent); + _server.Manager.AddEvent(gameEvent); } } diff --git a/Application/IO/GameLogReader.cs b/Application/IO/GameLogReader.cs index 07a398632..764de90a7 100644 --- a/Application/IO/GameLogReader.cs +++ b/Application/IO/GameLogReader.cs @@ -13,18 +13,20 @@ namespace IW4MAdmin.Application.IO { private readonly IEventParser _parser; private readonly string _logFile; + private readonly ILogger _logger; public long Length => new FileInfo(_logFile).Length; public int UpdateInterval => 300; - public GameLogReader(string logFile, IEventParser parser) + public GameLogReader(string logFile, IEventParser parser, ILogger logger) { _logFile = logFile; _parser = parser; + _logger = logger; } - public async Task> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition) + public async Task> ReadEventsFromLog(long fileSizeDiff, long startPosition) { // allocate the bytes for the new log lines List logLines = new List(); @@ -34,7 +36,7 @@ namespace IW4MAdmin.Application.IO { byte[] buff = new byte[fileSizeDiff]; fs.Seek(startPosition, SeekOrigin.Begin); - await fs.ReadAsync(buff, 0, (int)fileSizeDiff, server.Manager.CancellationToken); + await fs.ReadAsync(buff, 0, (int)fileSizeDiff); var stringBuilder = new StringBuilder(); char[] charBuff = Utilities.EncodingType.GetChars(buff); @@ -71,9 +73,9 @@ namespace IW4MAdmin.Application.IO catch (Exception e) { - server.Logger.WriteWarning("Could not properly parse event line"); - server.Logger.WriteDebug(e.Message); - server.Logger.WriteDebug(eventLine); + _logger.WriteWarning("Could not properly parse event line"); + _logger.WriteDebug(e.Message); + _logger.WriteDebug(eventLine); } } diff --git a/Application/IO/GameLogReaderHttp.cs b/Application/IO/GameLogReaderHttp.cs index 8f64f3562..822861d0f 100644 --- a/Application/IO/GameLogReaderHttp.cs +++ b/Application/IO/GameLogReaderHttp.cs @@ -5,43 +5,42 @@ using SharedLibraryCore.Interfaces; using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http; using System.Threading.Tasks; -using static SharedLibraryCore.Utilities; namespace IW4MAdmin.Application.IO { /// - /// provides capibility of reading log files over HTTP + /// provides capability of reading log files over HTTP /// class GameLogReaderHttp : IGameLogReader { private readonly IEventParser _eventParser; private readonly IGameLogServer _logServerApi; - readonly string logPath; + private readonly ILogger _logger; + private readonly string _safeLogPath; private string lastKey = "next"; - public GameLogReaderHttp(Uri gameLogServerUri, string logPath, IEventParser parser) + public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger logger) { - this.logPath = logPath.ToBase64UrlSafeString(); _eventParser = parser; - _logServerApi = RestClient.For(gameLogServerUri); + _logServerApi = RestClient.For(gameLogServerUris[0].ToString()); + _safeLogPath = gameLogServerUris[1].LocalPath.ToBase64UrlSafeString(); + _logger = logger; } public long Length => -1; public int UpdateInterval => 500; - public async Task> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition) + public async Task> ReadEventsFromLog(long fileSizeDiff, long startPosition) { var events = new List(); - string b64Path = logPath; - var response = await _logServerApi.Log(b64Path, lastKey); + var response = await _logServerApi.Log(_safeLogPath, lastKey); lastKey = response.NextKey; if (!response.Success && string.IsNullOrEmpty(lastKey)) { - server.Logger.WriteError($"Could not get log server info of {logPath}/{b64Path} ({server.LogPath})"); + _logger.WriteError($"Could not get log server info of {_safeLogPath}"); return events; } @@ -63,9 +62,9 @@ namespace IW4MAdmin.Application.IO catch (Exception e) { - server.Logger.WriteError("Could not properly parse event line from http"); - server.Logger.WriteDebug(e.Message); - server.Logger.WriteDebug(eventLine); + _logger.WriteError("Could not properly parse event line from http"); + _logger.WriteDebug(e.Message); + _logger.WriteDebug(eventLine); } } } diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index b54bf3dd8..0da506839 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -31,7 +32,7 @@ namespace IW4MAdmin public int Id { get; private set; } public IW4MServer(IManager mgr, ServerConfiguration cfg, ITranslationLookup lookup, - IRConConnectionFactory connectionFactory) : base(mgr, connectionFactory, cfg) + IRConConnectionFactory connectionFactory, IGameLogReaderFactory gameLogReaderFactory) : base(cfg, mgr, connectionFactory, gameLogReaderFactory) { _translationLookup = lookup; } @@ -77,7 +78,7 @@ namespace IW4MAdmin Type = GameEvent.EventType.Connect }; - Manager.GetEventHandler().AddEvent(e); + Manager.AddEvent(e); return client; } @@ -104,7 +105,7 @@ namespace IW4MAdmin Type = GameEvent.EventType.Disconnect }; - Manager.GetEventHandler().AddEvent(e); + Manager.AddEvent(e); #if DEBUG == true } #endif @@ -176,6 +177,8 @@ namespace IW4MAdmin await command.ExecuteAsync(E); } + + var pluginTasks = Manager.Plugins.Where(_plugin => _plugin.Name != "Login").Select(async _plugin => { try @@ -186,7 +189,11 @@ namespace IW4MAdmin return; } - await _plugin.OnEventAsync(E, this); + using (var tokenSource = new CancellationTokenSource()) + { + tokenSource.CancelAfter(Utilities.DefaultCommandTimeout); + await (_plugin.OnEventAsync(E, this)).WithWaitCancellation(tokenSource.Token); + } } catch (Exception Except) { @@ -195,7 +202,7 @@ namespace IW4MAdmin } }); - Parallel.ForEach(pluginTasks, async (_task) => await _task); + await Task.WhenAny(pluginTasks); } catch (Exception e) @@ -314,7 +321,8 @@ namespace IW4MAdmin // this happens for some reason rarely where the client spots get out of order // possible a connect/reconnect game event before we get to process it here // it appears that new games decide to switch client slots between maps (even if the clients aren't disconnecting) - else if (existingClient != null && existingClient.ClientNumber != E.Origin.ClientNumber) + // bots can have duplicate names which causes conflicting GUIDs + else if (existingClient != null && existingClient.ClientNumber != E.Origin.ClientNumber && !E.Origin.IsBot) { Logger.WriteWarning($"client {E.Origin} is trying to connect in client slot {E.Origin.ClientNumber}, but they are already registed in client slot {existingClient.ClientNumber}, swapping..."); // we need to remove them so the client spots can swap @@ -727,7 +735,7 @@ namespace IW4MAdmin Origin = client }; - Manager.GetEventHandler().AddEvent(e); + Manager.AddEvent(e); await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token); } @@ -771,10 +779,11 @@ namespace IW4MAdmin { Type = GameEvent.EventType.PreDisconnect, Origin = disconnectingClient, - Owner = this + Owner = this, + Source = GameEvent.EventSource.Status }; - Manager.GetEventHandler().AddEvent(e); + Manager.AddEvent(e); await e.WaitAsync(Utilities.DefaultCommandTimeout, Manager.CancellationToken); } @@ -793,10 +802,11 @@ namespace IW4MAdmin Type = GameEvent.EventType.PreConnect, Origin = client, Owner = this, - IsBlocking = true + IsBlocking = true, + Source = GameEvent.EventSource.Status }; - Manager.GetEventHandler().AddEvent(e); + Manager.AddEvent(e); await e.WaitAsync(Utilities.DefaultCommandTimeout, Manager.CancellationToken); } @@ -811,7 +821,7 @@ namespace IW4MAdmin Owner = this }; - Manager.GetEventHandler().AddEvent(e); + Manager.AddEvent(e); } if (ConnectionErrors > 0) @@ -824,7 +834,7 @@ namespace IW4MAdmin Target = Utilities.IW4MAdminClient(this) }; - Manager.GetEventHandler().AddEvent(_event); + Manager.AddEvent(_event); } ConnectionErrors = 0; @@ -846,7 +856,7 @@ namespace IW4MAdmin Data = ConnectionErrors.ToString() }; - Manager.GetEventHandler().AddEvent(_event); + Manager.AddEvent(_event); } return true; } @@ -1071,7 +1081,7 @@ namespace IW4MAdmin } } - LogEvent = new GameLogEventDetection(this, LogPath, ServerConfig.GameLogServerUrl); + LogEvent = new GameLogEventDetection(this, GenerateUriForLog(LogPath, ServerConfig.GameLogServerUrl?.AbsoluteUri), gameLogReaderFactory); Logger.WriteInfo($"Log file is {LogPath}"); _ = Task.Run(() => LogEvent.PollForChanges()); @@ -1080,6 +1090,21 @@ namespace IW4MAdmin #endif } + public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl) + { + var logUri = new Uri(logPath); + + if (string.IsNullOrEmpty(gameLogServerUrl)) + { + return new[] { logUri }; + } + + else + { + return new[] { new Uri(gameLogServerUrl), logUri }; + } + } + public static string GenerateLogPath(LogPathGeneratorInfo logInfo) { string logPath; @@ -1179,7 +1204,7 @@ namespace IW4MAdmin Owner = this }; - Manager.GetEventHandler().AddEvent(e); + Manager.AddEvent(e); string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7"); await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick); diff --git a/Application/Main.cs b/Application/Main.cs index 4369d2c6d..a89ffe6ff 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -30,7 +30,7 @@ namespace IW4MAdmin.Application /// entrypoint of the application /// /// - public static async Task Main() + public static async Task Main(string[] args) { AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory); @@ -45,7 +45,7 @@ namespace IW4MAdmin.Application Console.WriteLine($" Version {Utilities.GetVersionAsString()}"); Console.WriteLine("====================================================="); - await LaunchAsync(); + await LaunchAsync(args); } /// @@ -64,7 +64,7 @@ namespace IW4MAdmin.Application /// task that initializes application and starts the application monitoring and runtime tasks /// /// - private static async Task LaunchAsync() + private static async Task LaunchAsync(string[] args) { restart: ITranslationLookup translationLookup = null; @@ -74,7 +74,7 @@ namespace IW4MAdmin.Application ConfigurationMigration.MoveConfigFolder10518(null); ConfigurationMigration.CheckDirectories(); - var services = ConfigureServices(); + var services = ConfigureServices(args); serviceProvider = services.BuildServiceProvider(); ServerManager = (ApplicationManager)serviceProvider.GetRequiredService(); translationLookup = serviceProvider.GetRequiredService(); @@ -252,7 +252,7 @@ namespace IW4MAdmin.Application Owner = ServerManager.Servers[0] }; - ServerManager.GetEventHandler().AddEvent(E); + ServerManager.AddEvent(E); await E.WaitAsync(Utilities.DefaultCommandTimeout, ServerManager.CancellationToken); Console.Write('>'); } @@ -266,7 +266,7 @@ namespace IW4MAdmin.Application /// /// Configures the dependency injection services /// - private static IServiceCollection ConfigureServices() + private static IServiceCollection ConfigureServices(string[] args) { var defaultLogger = new Logger("IW4MAdmin-Manager"); var pluginImporter = new PluginImporter(defaultLogger); @@ -285,6 +285,7 @@ namespace IW4MAdmin.Application .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddTransient() .AddSingleton(_serviceProvider => @@ -295,6 +296,15 @@ namespace IW4MAdmin.Application }) .AddSingleton(); + if (args.Contains("serialevents")) + { + serviceCollection.AddSingleton(); + } + else + { + serviceCollection.AddSingleton(); + } + // register the native commands foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes() .Where(_command => _command.BaseType == typeof(Command))) diff --git a/Application/Misc/EventLog.cs b/Application/Misc/EventLog.cs new file mode 100644 index 000000000..6a8ef608e --- /dev/null +++ b/Application/Misc/EventLog.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; +using SharedLibraryCore; +using System; +using System.Collections.Generic; +using System.Text; + +namespace IW4MAdmin.Application.Misc +{ + public class EventLog : Dictionary> + { + private static JsonSerializerSettings serializationSettings; + + public static JsonSerializerSettings BuildVcrSerializationSettings() + { + if (serializationSettings == null) + { + serializationSettings = new JsonSerializerSettings() { Formatting = Formatting.Indented, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; + serializationSettings.Converters.Add(new IPAddressConverter()); + serializationSettings.Converters.Add(new IPEndPointConverter()); + serializationSettings.Converters.Add(new GameEventConverter()); + serializationSettings.Converters.Add(new ClientEntityConverter()); + } + + return serializationSettings; + } + } +} diff --git a/Application/Misc/SerializationHelpers.cs b/Application/Misc/SerializationHelpers.cs new file mode 100644 index 000000000..d2753dafa --- /dev/null +++ b/Application/Misc/SerializationHelpers.cs @@ -0,0 +1,149 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SharedLibraryCore; +using SharedLibraryCore.Database.Models; +using System; +using System.Net; +using static SharedLibraryCore.Database.Models.EFClient; +using static SharedLibraryCore.GameEvent; + +namespace IW4MAdmin.Application.Misc +{ + class IPAddressConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return (objectType == typeof(IPAddress)); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return IPAddress.Parse((string)reader.Value); + } + } + + class IPEndPointConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return (objectType == typeof(IPEndPoint)); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + IPEndPoint ep = (IPEndPoint)value; + JObject jo = new JObject(); + jo.Add("Address", JToken.FromObject(ep.Address, serializer)); + jo.Add("Port", ep.Port); + jo.WriteTo(writer); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + IPAddress address = jo["Address"].ToObject(serializer); + int port = (int)jo["Port"]; + return new IPEndPoint(address, port); + } + } + + class ClientEntityConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(EFClient); + + public override object ReadJson(JsonReader reader, Type objectType,object existingValue, JsonSerializer serializer) + { + if (reader.Value == null) + { + return null; + } + + var jsonObject = JObject.Load(reader); + + return new EFClient + { + NetworkId = (long)jsonObject["NetworkId"], + ClientNumber = (int)jsonObject["ClientNumber"], + State = Enum.Parse(jsonObject["state"].ToString()), + CurrentAlias = new EFAlias() + { + IPAddress = (int?)jsonObject["IPAddress"], + Name = jsonObject["Name"].ToString() + } + }; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var client = value as EFClient; + var jsonObject = new JObject + { + { "NetworkId", client.NetworkId }, + { "ClientNumber", client.ClientNumber }, + { "IPAddress", client.CurrentAlias?.IPAddress }, + { "Name", client.CurrentAlias?.Name }, + { "State", (int)client.State } + }; + + jsonObject.WriteTo(writer); + } + } + + class GameEventConverter : JsonConverter + { + public override bool CanConvert(Type objectType) =>objectType == typeof(GameEvent); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jsonObject = JObject.Load(reader); + + return new GameEvent + { + Type = Enum.Parse(jsonObject["Type"].ToString()), + Subtype = jsonObject["Subtype"]?.ToString(), + Source = Enum.Parse(jsonObject["Source"].ToString()), + RequiredEntity = Enum.Parse(jsonObject["RequiredEntity"].ToString()), + Data = jsonObject["Data"].ToString(), + Message = jsonObject["Message"].ToString(), + GameTime = (int?)jsonObject["GameTime"], + Origin = jsonObject["Origin"]?.ToObject(serializer), + Target = jsonObject["Target"]?.ToObject(serializer), + ImpersonationOrigin = jsonObject["ImpersonationOrigin"]?.ToObject(serializer), + IsRemote = (bool)jsonObject["IsRemote"], + Extra = null, // fix + Time = (DateTime)jsonObject["Time"], + IsBlocking = (bool)jsonObject["IsBlocking"] + }; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var gameEvent = value as GameEvent; + + var jsonObject = new JObject + { + { "Type", (int)gameEvent.Type }, + { "Subtype", gameEvent.Subtype }, + { "Source", (int)gameEvent.Source }, + { "RequiredEntity", (int)gameEvent.RequiredEntity }, + { "Data", gameEvent.Data }, + { "Message", gameEvent.Message }, + { "GameTime", gameEvent.GameTime }, + { "Origin", gameEvent.Origin != null ? JToken.FromObject(gameEvent.Origin, serializer) : null }, + { "Target", gameEvent.Target != null ? JToken.FromObject(gameEvent.Target, serializer) : null }, + { "ImpersonationOrigin", gameEvent.ImpersonationOrigin != null ? JToken.FromObject(gameEvent.ImpersonationOrigin, serializer) : null}, + { "IsRemote", gameEvent.IsRemote }, + { "Extra", gameEvent.Extra?.ToString() }, + { "Time", gameEvent.Time }, + { "IsBlocking", gameEvent.IsBlocking } + }; + + jsonObject.WriteTo(writer); + } + } +} diff --git a/Application/RCon/RConConnection.cs b/Application/RCon/RConConnection.cs index 3ced4b19a..ca131f4c8 100644 --- a/Application/RCon/RConConnection.cs +++ b/Application/RCon/RConConnection.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace IW4MAdmin.Application.RCon @@ -193,7 +194,8 @@ namespace IW4MAdmin.Application.RCon throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString())); } - string[] headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : config.CommandPrefixes.RConResponse); + string responseHeaderMatch = Regex.Match(responseString, config.CommandPrefixes.RConResponse).Value; + string[] headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : responseHeaderMatch); if (headerSplit.Length != 2) { diff --git a/Application/RconParsers/BaseRConParser.cs b/Application/RconParsers/BaseRConParser.cs index d2554cdb4..d170bef78 100644 --- a/Application/RconParsers/BaseRConParser.cs +++ b/Application/RconParsers/BaseRConParser.cs @@ -179,9 +179,15 @@ namespace IW4MAdmin.Application.RconParsers } long networkId; + string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); + try { - networkId = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].ConvertGuidToLong(Configuration.GuidNumberStyle); + string networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]]; + + networkId = networkIdString.IsBotGuid() ? + name.GenerateGuidFromString() : + networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); } catch (FormatException) @@ -189,7 +195,6 @@ namespace IW4MAdmin.Application.RconParsers continue; } - string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); int? ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP(); var client = new EFClient() @@ -206,13 +211,13 @@ namespace IW4MAdmin.Application.RconParsers State = EFClient.ClientState.Connecting }; -#if DEBUG - if (client.NetworkId < 1000 && client.NetworkId > 0) - { - client.IPAddress = 2147483646; - client.Ping = 0; - } -#endif +//#if DEBUG +// if (client.NetworkId < 1000 && client.NetworkId > 0) +// { +// client.IPAddress = 2147483646; +// client.Ping = 0; +// } +//#endif StatusPlayers.Add(client); } diff --git a/Application/SerialGameEventHandler.cs b/Application/SerialGameEventHandler.cs new file mode 100644 index 000000000..5eea99101 --- /dev/null +++ b/Application/SerialGameEventHandler.cs @@ -0,0 +1,41 @@ +using SharedLibraryCore; +using SharedLibraryCore.Events; +using SharedLibraryCore.Interfaces; +using System; +using System.Linq; + +namespace IW4MAdmin.Application +{ + class SerialGameEventHandler : IEventHandler + { + private delegate void GameEventAddedEventHandler(object sender, GameEventArgs args); + private event GameEventAddedEventHandler GameEventAdded; + + private static readonly GameEvent.EventType[] overrideEvents = new[] + { + GameEvent.EventType.Connect, + GameEvent.EventType.Disconnect, + GameEvent.EventType.Quit, + GameEvent.EventType.Stop + }; + + public SerialGameEventHandler() + { + GameEventAdded += GameEventHandler_GameEventAdded; + } + + private async void GameEventHandler_GameEventAdded(object sender, GameEventArgs args) + { + await (sender as IManager).ExecuteEvent(args.Event); + EventApi.OnGameEvent(args.Event); + } + + public void HandleEvent(IManager manager, GameEvent gameEvent) + { + if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type)) + { + GameEventAdded?.Invoke(manager, new GameEventArgs(null, false, gameEvent)); + } + } + } +} diff --git a/Plugins/ScriptPlugins/ParserPIW5.js b/Plugins/ScriptPlugins/ParserPIW5.js index 45e699648..a638e6add 100644 --- a/Plugins/ScriptPlugins/ParserPIW5.js +++ b/Plugins/ScriptPlugins/ParserPIW5.js @@ -3,7 +3,7 @@ var eventParser; var plugin = { author: 'RaidMax', - version: 0.2, + version: 0.3, name: 'Plutonium IW5 Parser', isParser: true, @@ -28,7 +28,7 @@ var plugin = { rconParser.Configuration.CanGenerateLogPath = true; rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *'; - rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$'; + rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+|0) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$'; rconParser.Configuration.Status.AddMapping(100, 1); rconParser.Configuration.Status.AddMapping(101, 2); rconParser.Configuration.Status.AddMapping(102, 3); diff --git a/Plugins/ScriptPlugins/ParserPT6.js b/Plugins/ScriptPlugins/ParserPT6.js index b1aa28320..51eb460c5 100644 --- a/Plugins/ScriptPlugins/ParserPT6.js +++ b/Plugins/ScriptPlugins/ParserPT6.js @@ -3,7 +3,7 @@ var eventParser; var plugin = { author: 'RaidMax, Xerxes', - version: 0.7, + version: 0.8, name: 'Plutonium T6 Parser', isParser: true, @@ -27,7 +27,7 @@ var plugin = { rconParser.Configuration.WaitForResponse = false; rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *'; - rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$'; + rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+|0) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$'; rconParser.Configuration.Status.AddMapping(100, 1); rconParser.Configuration.Status.AddMapping(101, 2); rconParser.Configuration.Status.AddMapping(102, 3); diff --git a/Plugins/ScriptPlugins/ParserTeknoMW3.js b/Plugins/ScriptPlugins/ParserTeknoMW3.js index 793c8fff0..3c7dcfa87 100644 --- a/Plugins/ScriptPlugins/ParserTeknoMW3.js +++ b/Plugins/ScriptPlugins/ParserTeknoMW3.js @@ -3,7 +3,7 @@ var eventParser; var plugin = { author: 'RaidMax', - version: 0.3, + version: 0.4, name: 'Tekno MW3 Parser', isParser: true, @@ -14,11 +14,11 @@ var plugin = { rconParser = manager.GenerateDynamicRConParser(this.name); eventParser = manager.GenerateDynamicEventParser(this.name); - rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[A-Z]|[0-9]){16,32})\t +(.{0,16}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+\\:-?\\d{1,5}|loopback) *$'; + rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[A-Z]|[0-9]){16,32}|0)\t +(.{0,16}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+\\:-?\\d{1,5}|loopback) *$'; rconParser.Configuration.Status.AddMapping(104, 5); // RConName rconParser.Configuration.Status.AddMapping(103, 4); // RConNetworkId rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; - rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff'; + rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff(print)?'; rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}'; rconParser.Configuration.CommandPrefixes.Say = 'say {0}'; rconParser.Configuration.CommandPrefixes.Kick = 'dropclient {0} "{1}"'; diff --git a/Plugins/Stats/Events/Script.cs b/Plugins/Stats/Events/Script.cs index 8fa09d374..b4eeb2e45 100644 --- a/Plugins/Stats/Events/Script.cs +++ b/Plugins/Stats/Events/Script.cs @@ -24,6 +24,12 @@ namespace IW4MAdmin.Plugins.Stats.Events return (EVENT_SCRIPTKILL, EVENT_SCRIPTKILL, (string eventLine, IEventParserConfiguration config, GameEvent autoEvent) => { string[] lineSplit = eventLine.Split(";"); + + if (lineSplit[1].IsBotGuid() || lineSplit[2].IsBotGuid()) + { + return autoEvent; + } + long originId = lineSplit[1].ConvertGuidToLong(config.GuidNumberStyle, 1); long targetId = lineSplit[2].ConvertGuidToLong(config.GuidNumberStyle, 1); @@ -48,6 +54,12 @@ namespace IW4MAdmin.Plugins.Stats.Events return (EVENT_SCRIPTDAMAGE, EVENT_SCRIPTDAMAGE, (string eventLine, IEventParserConfiguration config, GameEvent autoEvent) => { string[] lineSplit = eventLine.Split(";"); + + if (lineSplit[1].IsBotGuid() || lineSplit[2].IsBotGuid()) + { + return autoEvent; + } + long originId = lineSplit[1].ConvertGuidToLong(config.GuidNumberStyle, 1); long targetId = lineSplit[2].ConvertGuidToLong(config.GuidNumberStyle, 1); @@ -71,6 +83,12 @@ namespace IW4MAdmin.Plugins.Stats.Events return (EVENT_JOINTEAM, EVENT_JOINTEAM, (string eventLine, IEventParserConfiguration config, GameEvent autoEvent) => { string[] lineSplit = eventLine.Split(";"); + + if (lineSplit[1].IsBotGuid() || lineSplit[2].IsBotGuid()) + { + return autoEvent; + } + long originId = lineSplit[1].ConvertGuidToLong(config.GuidNumberStyle, 1); long targetId = lineSplit[2].ConvertGuidToLong(config.GuidNumberStyle, 1); diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index 6aeb76eae..733439bf9 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -530,7 +530,7 @@ namespace IW4MAdmin.Plugins.Stats /// /// /// - private bool IsWorldDamage(EFClient origin) => origin?.NetworkId == 1; + private bool IsWorldDamage(EFClient origin) => origin?.NetworkId == Utilities.WORLD_ID || origin?.ClientId == Utilities.WORLD_ID; /// /// Indicates if we should try to use anticheat even if sv_customcallbacks is not defined diff --git a/Plugins/Stats/Stats.csproj b/Plugins/Stats/Stats.csproj index 7b680892f..52ff5583f 100644 --- a/Plugins/Stats/Stats.csproj +++ b/Plugins/Stats/Stats.csproj @@ -16,7 +16,7 @@ - + diff --git a/Plugins/Tests/ClientTests.cs b/Plugins/Tests/ClientTests.cs index e72f0dfab..79770a33b 100644 --- a/Plugins/Tests/ClientTests.cs +++ b/Plugins/Tests/ClientTests.cs @@ -72,7 +72,7 @@ namespace Tests } }; - _manager.GetEventHandler().AddEvent(e); + _manager.AddEvent(e); e.Complete(); e = new GameEvent() @@ -91,7 +91,7 @@ namespace Tests } }; - _manager.GetEventHandler().AddEvent(e); + _manager.AddEvent(e); e.Complete(); e = new GameEvent() @@ -110,7 +110,7 @@ namespace Tests } }; - _manager.GetEventHandler().AddEvent(e); + _manager.AddEvent(e); e.Complete(); } diff --git a/Plugins/Tests/PluginTests.cs b/Plugins/Tests/PluginTests.cs index 980467141..ffc5600d3 100644 --- a/Plugins/Tests/PluginTests.cs +++ b/Plugins/Tests/PluginTests.cs @@ -30,7 +30,7 @@ namespace Tests Owner = Manager.GetServers()[0] }; - Manager.GetEventHandler().AddEvent(e); + Manager.AddEvent(e); e.Complete(); var client = Manager.GetServers()[0].Clients[0]; @@ -43,7 +43,7 @@ namespace Tests Owner = e.Owner }; - Manager.GetEventHandler().AddEvent(e); + Manager.AddEvent(e); e.Complete(); Assert.True(client.Warnings == 1, "client wasn't warned for objectional language"); diff --git a/SharedLibraryCore/Commands/RunAsCommand.cs b/SharedLibraryCore/Commands/RunAsCommand.cs index 67e7ac477..c20a8d9d4 100644 --- a/SharedLibraryCore/Commands/RunAsCommand.cs +++ b/SharedLibraryCore/Commands/RunAsCommand.cs @@ -49,7 +49,7 @@ namespace SharedLibraryCore.Commands Data = cmd, Owner = E.Owner }; - E.Owner.Manager.GetEventHandler().AddEvent(impersonatedCommandEvent); + E.Owner.Manager.AddEvent(impersonatedCommandEvent); var result = await impersonatedCommandEvent.WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken); var response = E.Owner.CommandResult.Where(c => c.ClientId == E.Target.ClientId).ToList(); diff --git a/SharedLibraryCore/Events/EventAPI.cs b/SharedLibraryCore/Events/EventAPI.cs index 102f5fe98..720b4a4e7 100644 --- a/SharedLibraryCore/Events/EventAPI.cs +++ b/SharedLibraryCore/Events/EventAPI.cs @@ -23,9 +23,9 @@ namespace SharedLibraryCore.Events return eventList; } - public static void OnGameEvent(object sender, GameEventArgs eventState) + public static void OnGameEvent(GameEvent gameEvent) { - var E = eventState.Event; + var E = gameEvent; // don't want to clog up the api with unknown events if (E.Type == GameEvent.EventType.Unknown) return; diff --git a/SharedLibraryCore/Events/GameEvent.cs b/SharedLibraryCore/Events/GameEvent.cs index 490edd74d..40586d389 100644 --- a/SharedLibraryCore/Events/GameEvent.cs +++ b/SharedLibraryCore/Events/GameEvent.cs @@ -194,6 +194,14 @@ namespace SharedLibraryCore Target = 4 } + public enum EventSource + { + Unspecified, + Log, + Status, + Internal + } + static long NextEventId; static long GetNextEventId() { @@ -214,6 +222,7 @@ namespace SharedLibraryCore } public EventType Type; + public EventSource Source { get; set; } /// /// suptype of the event for more detailed classification /// @@ -229,7 +238,7 @@ namespace SharedLibraryCore public EFClient Target; public EFClient ImpersonationOrigin { get; set; } public Server Owner; - public bool IsRemote { get; set; } = false; + public bool IsRemote { get; set; } public object Extra { get; set; } private readonly ManualResetEvent _eventFinishedWaiter; public DateTime Time { get; set; } diff --git a/SharedLibraryCore/Interfaces/IEventHandler.cs b/SharedLibraryCore/Interfaces/IEventHandler.cs index c92c5afd7..b8437f5e5 100644 --- a/SharedLibraryCore/Interfaces/IEventHandler.cs +++ b/SharedLibraryCore/Interfaces/IEventHandler.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace SharedLibraryCore.Interfaces +namespace SharedLibraryCore.Interfaces { /// - /// This class handle games events (from log, manual events, etc) + /// handles games events (from log, manual events, etc) /// public interface IEventHandler { /// /// Add a game event event to the queue to be processed /// - /// Game event - void AddEvent(GameEvent gameEvent); + /// application manager instance + /// game event + void HandleEvent(IManager manager, GameEvent gameEvent); } } diff --git a/SharedLibraryCore/Interfaces/IGameLogReader.cs b/SharedLibraryCore/Interfaces/IGameLogReader.cs index 25bf59707..155c1851b 100644 --- a/SharedLibraryCore/Interfaces/IGameLogReader.cs +++ b/SharedLibraryCore/Interfaces/IGameLogReader.cs @@ -4,18 +4,17 @@ using System.Threading.Tasks; namespace SharedLibraryCore.Interfaces { /// - /// represents the abtraction of game log reading + /// represents the abstraction of game log reading /// public interface IGameLogReader { /// /// get new events that have occured since the last poll /// - /// /// /// /// - Task> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition); + Task> ReadEventsFromLog(long fileSizeDiff, long startPosition); /// /// how long the log file is diff --git a/SharedLibraryCore/Interfaces/IGameLogReaderFactory.cs b/SharedLibraryCore/Interfaces/IGameLogReaderFactory.cs new file mode 100644 index 000000000..54eb716bf --- /dev/null +++ b/SharedLibraryCore/Interfaces/IGameLogReaderFactory.cs @@ -0,0 +1,18 @@ +using System; + +namespace SharedLibraryCore.Interfaces +{ + /// + /// factory interface to create game log readers based on the log file uri + /// + public interface IGameLogReaderFactory + { + /// + /// generates a new game log reader based on the provided Uri + /// + /// collection of log uri used to generate the log reader + /// event parser for the log reader + /// + IGameLogReader CreateGameLogReader(Uri[] logUris, IEventParser eventParser); + } +} diff --git a/SharedLibraryCore/Interfaces/IManager.cs b/SharedLibraryCore/Interfaces/IManager.cs index f058a3985..337a3a1b9 100644 --- a/SharedLibraryCore/Interfaces/IManager.cs +++ b/SharedLibraryCore/Interfaces/IManager.cs @@ -24,11 +24,6 @@ namespace SharedLibraryCore.Interfaces AliasService GetAliasService(); PenaltyService GetPenaltyService(); /// - /// Get the event handlers - /// - /// EventHandler for the manager - IEventHandler GetEventHandler(); - /// /// enumerates the registered plugin instances /// IEnumerable Plugins { get; } @@ -68,7 +63,12 @@ namespace SharedLibraryCore.Interfaces string ExternalIPAddress { get; } CancellationToken CancellationToken { get; } bool IsRestartRequested { get; } - //OnServerEventEventHandler OnServerEvent { get; set; } + bool IsRunning { get; } Task ExecuteEvent(GameEvent gameEvent); + /// + /// queues an event for processing + /// + /// event to be processed + void AddEvent(GameEvent gameEvent); } } diff --git a/SharedLibraryCore/PartialEntities/EFClient.cs b/SharedLibraryCore/PartialEntities/EFClient.cs index c8c0e58fc..e9a87cfe3 100644 --- a/SharedLibraryCore/PartialEntities/EFClient.cs +++ b/SharedLibraryCore/PartialEntities/EFClient.cs @@ -142,7 +142,7 @@ namespace SharedLibraryCore.Database.Models Data = message }; - CurrentServer?.Manager.GetEventHandler().AddEvent(e); + CurrentServer?.Manager.AddEvent(e); return e; } @@ -174,7 +174,7 @@ namespace SharedLibraryCore.Database.Models Warnings++; } - sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); + sender.CurrentServer.Manager.AddEvent(e); return e; } @@ -202,7 +202,7 @@ namespace SharedLibraryCore.Database.Models Warnings = 0; - sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); + sender.CurrentServer.Manager.AddEvent(e); return e; } @@ -243,7 +243,7 @@ namespace SharedLibraryCore.Database.Models } sender.SetAdditionalProperty("_reportCount", reportCount + 1); - sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); + sender.CurrentServer.Manager.AddEvent(e); return e; } @@ -276,7 +276,7 @@ namespace SharedLibraryCore.Database.Models e.FailReason = GameEvent.EventFailReason.Invalid; } - sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); + sender.CurrentServer.Manager.AddEvent(e); return e; } @@ -308,7 +308,7 @@ namespace SharedLibraryCore.Database.Models e.FailReason = GameEvent.EventFailReason.Invalid; } - sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); + sender.CurrentServer.Manager.AddEvent(e); return e; } @@ -336,7 +336,7 @@ namespace SharedLibraryCore.Database.Models } State = ClientState.Disconnecting; - sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); + sender.CurrentServer.Manager.AddEvent(e); return e; } @@ -366,7 +366,7 @@ namespace SharedLibraryCore.Database.Models } State = ClientState.Disconnecting; - sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); + sender.CurrentServer.Manager.AddEvent(e); return e; } @@ -400,7 +400,7 @@ namespace SharedLibraryCore.Database.Models } State = ClientState.Disconnecting; - sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); + sender.CurrentServer.Manager.AddEvent(e); return e; } @@ -428,7 +428,7 @@ namespace SharedLibraryCore.Database.Models e.FailReason = GameEvent.EventFailReason.Permission; } - sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); + sender.CurrentServer.Manager.AddEvent(e); return e; } @@ -464,7 +464,7 @@ namespace SharedLibraryCore.Database.Models Level = newPermission; } - sender.CurrentServer.Manager.GetEventHandler().AddEvent(e); + sender.CurrentServer.Manager.AddEvent(e); return e; } @@ -565,7 +565,7 @@ namespace SharedLibraryCore.Database.Models Owner = CurrentServer, }; - CurrentServer.Manager.GetEventHandler().AddEvent(e); + CurrentServer.Manager.AddEvent(e); } } @@ -655,7 +655,7 @@ namespace SharedLibraryCore.Database.Models [NotMapped] public int Score { get; set; } [NotMapped] - public bool IsBot => NetworkId == -1; + public bool IsBot => NetworkId == Name.GenerateGuidFromString(); [NotMapped] public ClientState State { get; set; } @@ -694,7 +694,7 @@ namespace SharedLibraryCore.Database.Models public override bool Equals(object obj) { - return ((EFClient)obj).NetworkId == this.NetworkId; + return obj.GetType() == typeof(EFClient) && ((EFClient)obj).NetworkId == this.NetworkId; } public override int GetHashCode() diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 687151c3a..6e675bd67 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -28,7 +28,7 @@ namespace SharedLibraryCore T7 = 8 } - public Server(IManager mgr, IRConConnectionFactory rconConnectionFactory, ServerConfiguration config) + public Server(ServerConfiguration config, IManager mgr, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory) { Password = config.Password; IP = config.IPAddress; @@ -46,6 +46,7 @@ namespace SharedLibraryCore NextMessage = 0; CustomSayEnabled = Manager.GetApplicationSettings().Configuration().EnableCustomSayName; CustomSayName = Manager.GetApplicationSettings().Configuration().CustomSayName; + this.gameLogReaderFactory = gameLogReaderFactory; InitializeTokens(); InitializeAutoMessages(); } @@ -134,7 +135,7 @@ namespace SharedLibraryCore Origin = sender, }; - Manager.GetEventHandler().AddEvent(e); + Manager.AddEvent(e); return e; } @@ -296,7 +297,7 @@ namespace SharedLibraryCore { get { - return Clients.Where(p => p != null && !p.IsBot).Count(); + return Clients.Where(p => p != null/* && !p.IsBot*/).Count(); } } public int MaxClients { get; protected set; } @@ -325,6 +326,7 @@ namespace SharedLibraryCore protected TimeSpan LastMessage; protected DateTime LastPoll; protected ManualResetEventSlim OnRemoteCommandResponse; + protected IGameLogReaderFactory gameLogReaderFactory; // only here for performance private readonly bool CustomSayEnabled; diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index a96af91f7..0c9e3d18c 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -6,7 +6,7 @@ RaidMax.IW4MAdmin.SharedLibraryCore - 2.2.11 + 2.2.12 RaidMax Forever None Debug;Release;Prerelease @@ -20,8 +20,8 @@ true MIT Shared Library for IW4MAdmin - 2.2.11.0 - 2.2.11.0 + 2.2.12.0 + 2.2.12.0 diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs index 80afde181..b76460dbe 100644 --- a/SharedLibraryCore/Utilities.cs +++ b/SharedLibraryCore/Utilities.cs @@ -16,6 +16,7 @@ using System.Net; using System.Reflection; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFPenalty; @@ -50,6 +51,10 @@ namespace SharedLibraryCore AdministeredPenalties = new List() }; } + /// + /// fallback id for world events + /// + public const long WORLD_ID = -1; public static string HttpRequest(string location, string header, string headerValue) { @@ -295,39 +300,46 @@ namespace SharedLibraryCore } } + /// + /// converts a string to numerical guid + /// + /// source string for guid + /// how to parse the guid + /// value to use if string is empty + /// public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, long? fallback = null) { str = str.Substring(0, Math.Min(str.Length, 19)); - var bot = Regex.Match(str, @"bot[0-9]+").Value; + var parsableAsNumber = Regex.Match(str, @"([A-F]|[a-f]|[0-9])+").Value; if (string.IsNullOrWhiteSpace(str) && fallback.HasValue) { return fallback.Value; } - long id = 0; - - if (numberStyle == NumberStyles.Integer) + long id; + if (!string.IsNullOrEmpty(parsableAsNumber)) { - long.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out id); - - if (id < 0) + if (numberStyle == NumberStyles.Integer) { - id = (uint)id; + long.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out id); + + if (id < 0) + { + id = (uint)id; + } + } + + else + { + long.TryParse(str.Length > 16 ? str.Substring(0, 16) : str, numberStyle, CultureInfo.InvariantCulture, out id); } } else { - long.TryParse(str.Length > 16 ? str.Substring(0, 16) : str, numberStyle, CultureInfo.InvariantCulture, out id); - } - - if (!string.IsNullOrEmpty(bot)) - { - id = -1; -#if DEBUG - id = str.Sum(_c => _c); -#endif + // this is a special case for when a real guid is not provided, so we generated it from another source + id = str.GenerateGuidFromString(); } if (id == 0) @@ -338,6 +350,23 @@ namespace SharedLibraryCore return id; } + /// + /// determines if the guid provided appears to be a bot guid + /// + /// value of the guid + /// true if is bot guid, otherwise false + public static bool IsBotGuid(this string guid) + { + return guid.Contains("bot") || guid == "0"; + } + + /// + /// generates a numerical hashcode from a string value + /// + /// value string + /// + public static long GenerateGuidFromString(this string value) => string.IsNullOrEmpty(value) ? -1 : HashCode.Combine(value.StripColors()); + public static int? ConvertToIP(this string str) { bool success = IPAddress.TryParse(str, out IPAddress ip); @@ -900,6 +929,24 @@ namespace SharedLibraryCore return false; } + /// + /// https://www.planetgeek.ch/2016/12/08/async-method-without-cancellation-support-do-it-my-way/ + /// + public static async Task WithWaitCancellation(this Task task, + CancellationToken cancellationToken) + { + Task completedTask = await Task.WhenAny(task, Task.Delay(Timeout.Infinite, cancellationToken)); + if (completedTask == task) + { + await task; + } + else + { + cancellationToken.ThrowIfCancellationRequested(); + throw new InvalidOperationException("Infinite delay task completed."); + } + } + public static bool ShouldHideLevel(this Permission perm) => perm == Permission.Flagged; /// diff --git a/Tests/ApplicationTests/ApplicationTests.csproj b/Tests/ApplicationTests/ApplicationTests.csproj index ba2cfe133..1c0b41a26 100644 --- a/Tests/ApplicationTests/ApplicationTests.csproj +++ b/Tests/ApplicationTests/ApplicationTests.csproj @@ -23,6 +23,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/Tests/ApplicationTests/CommandTests.cs b/Tests/ApplicationTests/CommandTests.cs index f6bb9ec5b..88b6bbd4d 100644 --- a/Tests/ApplicationTests/CommandTests.cs +++ b/Tests/ApplicationTests/CommandTests.cs @@ -37,12 +37,10 @@ namespace ApplicationTests cmdConfig = new CommandConfiguration(); serviceProvider = new ServiceCollection() - .BuildBase() + .BuildBase(new MockEventHandler(true)) .BuildServiceProvider(); - mockEventHandler = new MockEventHandler(true); - A.CallTo(() => serviceProvider.GetRequiredService().GetEventHandler()) - .Returns(mockEventHandler); + mockEventHandler = serviceProvider.GetRequiredService(); var mgr = serviceProvider.GetRequiredService(); transLookup = serviceProvider.GetRequiredService(); @@ -54,7 +52,9 @@ namespace ApplicationTests new NonImpersonatableCommand(cmdConfig, transLookup) }); - //Utilities.DefaultCommandTimeout = new TimeSpan(0, 0, 2); + A.CallTo(() => mgr.AddEvent(A.Ignored)) + .Invokes((fakeCall) => mockEventHandler.HandleEvent(mgr, fakeCall.Arguments[0] as GameEvent)); + } #region RUNAS diff --git a/Tests/ApplicationTests/DepedencyInjectionExtensions.cs b/Tests/ApplicationTests/DependencyInjectionExtensions.cs similarity index 63% rename from Tests/ApplicationTests/DepedencyInjectionExtensions.cs rename to Tests/ApplicationTests/DependencyInjectionExtensions.cs index daafc2bb9..2784955fa 100644 --- a/Tests/ApplicationTests/DepedencyInjectionExtensions.cs +++ b/Tests/ApplicationTests/DependencyInjectionExtensions.cs @@ -9,10 +9,22 @@ using SharedLibraryCore.Services; namespace ApplicationTests { - static class DepedencyInjectionExtensions + static class DependencyInjectionExtensions { - public static IServiceCollection BuildBase(this IServiceCollection serviceCollection) + public static IServiceCollection BuildBase(this IServiceCollection serviceCollection, IEventHandler eventHandler = null) { + + if (eventHandler == null) + { + eventHandler = new MockEventHandler(); + serviceCollection.AddSingleton(eventHandler as MockEventHandler); + } + + else if (eventHandler is MockEventHandler mockEventHandler) + { + serviceCollection.AddSingleton(mockEventHandler); + } + var manager = A.Fake(); var logger = A.Fake(); @@ -27,10 +39,13 @@ namespace ApplicationTests .AddSingleton(A.Fake()) .AddSingleton(A.Fake()) .AddSingleton(A.Fake()) - .AddSingleton(A.Fake()); + .AddSingleton() + .AddSingleton(A.Fake()) + .AddSingleton(A.Fake()) + .AddSingleton(eventHandler); serviceCollection.AddSingleton(_sp => new IW4MServer(_sp.GetRequiredService(), ConfigurationGenerators.CreateServerConfiguration(), - _sp.GetRequiredService(), _sp.GetRequiredService()) + _sp.GetRequiredService(), _sp.GetRequiredService(), _sp.GetRequiredService()) { RconParser = _sp.GetRequiredService() }); diff --git a/Tests/ApplicationTests/Fixtures/DataFileLoader.cs b/Tests/ApplicationTests/Fixtures/DataFileLoader.cs new file mode 100644 index 000000000..ccd9a2705 --- /dev/null +++ b/Tests/ApplicationTests/Fixtures/DataFileLoader.cs @@ -0,0 +1,16 @@ +using IW4MAdmin.Application.Misc; +using Newtonsoft.Json; +using System.IO; +using System.Threading.Tasks; + +namespace ApplicationTests.Fixtures +{ + class DataFileLoader + { + public async Task Load(string fileName) + { + string data = await File.ReadAllTextAsync($"{fileName}.json"); + return JsonConvert.DeserializeObject(data, EventLog.BuildVcrSerializationSettings()); + } + } +} diff --git a/Tests/ApplicationTests/GameEventHandlerTests.cs b/Tests/ApplicationTests/GameEventHandlerTests.cs new file mode 100644 index 000000000..f0f42afb0 --- /dev/null +++ b/Tests/ApplicationTests/GameEventHandlerTests.cs @@ -0,0 +1,17 @@ +using IW4MAdmin; +using IW4MAdmin.Application; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using SharedLibraryCore; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace ApplicationTests +{ + [TestFixture] + public class GameEventHandlerTests + { + } +} diff --git a/Tests/ApplicationTests/IOTests.cs b/Tests/ApplicationTests/IOTests.cs index f4cd32382..9c09767a2 100644 --- a/Tests/ApplicationTests/IOTests.cs +++ b/Tests/ApplicationTests/IOTests.cs @@ -1,5 +1,7 @@ using FakeItEasy; +using IW4MAdmin; using IW4MAdmin.Application.IO; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using SharedLibraryCore; using SharedLibraryCore.Interfaces; @@ -12,11 +14,24 @@ namespace ApplicationTests public class IOTests { + private IServiceProvider serviceProvider; + + [SetUp] + public void Setup() + { + serviceProvider = new ServiceCollection().BuildBase().BuildServiceProvider(); + } + [Test] public async Task GameLogEventDetection_WorksAfterFileSizeReset() { var reader = A.Fake(); - var detect = new GameLogEventDetection(null, "", A.Fake(), reader); + var factory = A.Fake(); + + A.CallTo(() => factory.CreateGameLogReader(A.Ignored, A.Ignored)) + .Returns(reader); + + var detect = new GameLogEventDetection(serviceProvider.GetService(), new Uri[] { new Uri("C:\\test.log") }, factory); A.CallTo(() => reader.Length) .Returns(100) @@ -35,7 +50,7 @@ namespace ApplicationTests await detect.UpdateLogEvents(); } - A.CallTo(() => reader.ReadEventsFromLog(A.Ignored, A.Ignored, A.Ignored)) + A.CallTo(() => reader.ReadEventsFromLog(A.Ignored, A.Ignored)) .MustHaveHappenedTwiceExactly(); } } diff --git a/Tests/ApplicationTests/IW4MServerTests.cs b/Tests/ApplicationTests/IW4MServerTests.cs index b0cc1d50f..899be42f0 100644 --- a/Tests/ApplicationTests/IW4MServerTests.cs +++ b/Tests/ApplicationTests/IW4MServerTests.cs @@ -36,6 +36,7 @@ namespace ApplicationTests fakeManager = serviceProvider.GetRequiredService(); fakeRConConnection = serviceProvider.GetRequiredService(); fakeRConParser = serviceProvider.GetRequiredService(); + mockEventHandler = serviceProvider.GetRequiredService(); var rconConnectionFactory = serviceProvider.GetRequiredService(); @@ -45,10 +46,9 @@ namespace ApplicationTests A.CallTo(() => fakeRConParser.Configuration) .Returns(ConfigurationGenerators.CreateRConParserConfiguration(serviceProvider.GetRequiredService())); - - mockEventHandler = new MockEventHandler(); - A.CallTo(() => fakeManager.GetEventHandler()) - .Returns(mockEventHandler); + A.CallTo(() => fakeManager.AddEvent(A.Ignored)) + .Invokes((fakeCall) => mockEventHandler.HandleEvent(fakeManager, fakeCall.Arguments[0] as GameEvent)); + } #region LOG diff --git a/Tests/ApplicationTests/Mocks/EventHandler.cs b/Tests/ApplicationTests/Mocks/EventHandler.cs index ec1caea20..e61a9a206 100644 --- a/Tests/ApplicationTests/Mocks/EventHandler.cs +++ b/Tests/ApplicationTests/Mocks/EventHandler.cs @@ -14,7 +14,7 @@ namespace ApplicationTests.Mocks _autoExecute = autoExecute; } - public void AddEvent(GameEvent gameEvent) + public void HandleEvent(IManager manager, GameEvent gameEvent) { Events.Add(gameEvent); diff --git a/Tests/ApplicationTests/Mocks/VcrEventReader.cs b/Tests/ApplicationTests/Mocks/VcrEventReader.cs new file mode 100644 index 000000000..5176f6fdb --- /dev/null +++ b/Tests/ApplicationTests/Mocks/VcrEventReader.cs @@ -0,0 +1,21 @@ +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace ApplicationTests.Mocks +{ + class VcrEventReader : IGameLogReader + { + public long Length => throw new NotImplementedException(); + + public int UpdateInterval => throw new NotImplementedException(); + + public Task> ReadEventsFromLog(long fileSizeDiff, long startPosition) + { + throw new NotImplementedException(); + } + } +} diff --git a/Tests/ApplicationTests/PluginTests.cs b/Tests/ApplicationTests/PluginTests.cs index f9ea27b6b..f4f5ac017 100644 --- a/Tests/ApplicationTests/PluginTests.cs +++ b/Tests/ApplicationTests/PluginTests.cs @@ -31,9 +31,7 @@ namespace ApplicationTests { serviceProvider = new ServiceCollection().BuildBase().BuildServiceProvider(); fakeManager = serviceProvider.GetRequiredService(); - mockEventHandler = new MockEventHandler(); - A.CallTo(() => fakeManager.GetEventHandler()) - .Returns(mockEventHandler); + mockEventHandler = serviceProvider.GetRequiredService(); var rconConnectionFactory = serviceProvider.GetRequiredService(); @@ -42,6 +40,9 @@ namespace ApplicationTests A.CallTo(() => serviceProvider.GetRequiredService().Configuration) .Returns(ConfigurationGenerators.CreateRConParserConfiguration(serviceProvider.GetRequiredService())); + + A.CallTo(() => fakeManager.AddEvent(A.Ignored)) + .Invokes((fakeCall) => mockEventHandler.HandleEvent(fakeManager, fakeCall.Arguments[0] as GameEvent)); } [Test] diff --git a/Tests/ApplicationTests/ServerTests.cs b/Tests/ApplicationTests/ServerTests.cs index e37437870..70dfb0c23 100644 --- a/Tests/ApplicationTests/ServerTests.cs +++ b/Tests/ApplicationTests/ServerTests.cs @@ -33,7 +33,7 @@ namespace ApplicationTests var mgr = A.Fake(); var server = new IW4MServer(mgr, new SharedLibraryCore.Configuration.ServerConfiguration() { IPAddress = "127.0.0.1", Port = 28960 }, - A.Fake(), A.Fake()); + A.Fake(), A.Fake(), A.Fake()); var parser = new BaseEventParser(A.Fake(), A.Fake()); parser.Configuration.GuidNumberStyle = System.Globalization.NumberStyles.Integer; @@ -59,7 +59,7 @@ namespace ApplicationTests var server = new IW4MServer(mgr, new SharedLibraryCore.Configuration.ServerConfiguration() { IPAddress = "127.0.0.1", Port = 28960 }, - A.Fake(), A.Fake()); + A.Fake(), A.Fake(), A.Fake()); var parser = new BaseEventParser(A.Fake(), A.Fake()); parser.Configuration.GuidNumberStyle = System.Globalization.NumberStyles.Integer; diff --git a/Tests/ApplicationTests/StatsTests.cs b/Tests/ApplicationTests/StatsTests.cs index d9759e2fe..a2296ed00 100644 --- a/Tests/ApplicationTests/StatsTests.cs +++ b/Tests/ApplicationTests/StatsTests.cs @@ -63,7 +63,7 @@ namespace ApplicationTests var server = new IW4MServer(mgr, new SharedLibraryCore.Configuration.ServerConfiguration() { IPAddress = "127.0.0.1", Port = 28960 }, A.Fake(), - A.Fake()); + A.Fake(), A.Fake()); var parser = new BaseEventParser(A.Fake(), A.Fake()); parser.Configuration.GuidNumberStyle = System.Globalization.NumberStyles.Integer; diff --git a/Tests/ApplicationTests/VcrTests.cs b/Tests/ApplicationTests/VcrTests.cs new file mode 100644 index 000000000..d6639d7f4 --- /dev/null +++ b/Tests/ApplicationTests/VcrTests.cs @@ -0,0 +1,43 @@ +using ApplicationTests.Fixtures; +using IW4MAdmin; +using IW4MAdmin.Application; +using IW4MAdmin.Application.Misc; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ApplicationTests +{ + [TestFixture] + public class VcrTests + { + private IServiceProvider serviceProvider; + + [SetUp] + public void Setup() + { + serviceProvider = new ServiceCollection().BuildBase() + .BuildServiceProvider(); + } + + [Test] + [TestCase("replay")] + public async Task ReplayEvents(string source) + { + var sourceData = await serviceProvider + .GetRequiredService() + .Load(source); + + var server = serviceProvider.GetRequiredService(); + + foreach (var gameEvent in sourceData.Values.First()) + { + await server.ExecuteEvent(gameEvent); + } + } + } +} diff --git a/WebfrontCore/Controllers/ConsoleController.cs b/WebfrontCore/Controllers/ConsoleController.cs index 59ff81926..15547ef35 100644 --- a/WebfrontCore/Controllers/ConsoleController.cs +++ b/WebfrontCore/Controllers/ConsoleController.cs @@ -56,7 +56,7 @@ namespace WebfrontCore.Controllers IsRemote = true }; - Manager.GetEventHandler().AddEvent(remoteEvent); + Manager.AddEvent(remoteEvent); List response = null; try