diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index efe53f30f..bb0c6a07d 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -58,11 +58,12 @@ namespace IW4MAdmin.Application private readonly Dictionary> _operationLookup = new Dictionary>(); private readonly ITranslationLookup _translationLookup; private readonly IConfigurationHandler _commandConfiguration; - private readonly IPluginImporter _pluginImporter; + private readonly IGameServerInstanceFactory _serverInstanceFactory; public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable commands, ITranslationLookup translationLookup, IConfigurationHandler commandConfiguration, - IConfigurationHandler appConfigHandler, IPluginImporter pluginImporter) + IConfigurationHandler appConfigHandler, IGameServerInstanceFactory serverInstanceFactory, + IEnumerable plugins) { MiddlewareActionHandler = actionHandler; _servers = new ConcurrentBag(); @@ -73,8 +74,8 @@ namespace IW4MAdmin.Application ConfigHandler = appConfigHandler; StartTime = DateTime.UtcNow; PageList = new PageList(); - AdditionalEventParsers = new List(); - AdditionalRConParsers = new List(); + AdditionalEventParsers = new List() { new BaseEventParser() }; + AdditionalRConParsers = new List() { new BaseRConParser() }; TokenAuthenticator = new TokenAuthentication(); _metaService = new MetaService(); _tokenSource = new CancellationTokenSource(); @@ -82,9 +83,12 @@ namespace IW4MAdmin.Application _commands = commands.ToList(); _translationLookup = translationLookup; _commandConfiguration = commandConfiguration; - _pluginImporter = pluginImporter; + _serverInstanceFactory = serverInstanceFactory; + Plugins = plugins; } + public IEnumerable Plugins { get; } + public async Task ExecuteEvent(GameEvent newEvent) { #if DEBUG == true @@ -249,7 +253,7 @@ namespace IW4MAdmin.Application ExternalIPAddress = await Utilities.GetExternalIP(); #region PLUGINS - foreach (var plugin in _pluginImporter.ActivePlugins) + foreach (var plugin in Plugins) { try { @@ -567,7 +571,8 @@ namespace IW4MAdmin.Application try { - var ServerInstance = new IW4MServer(this, Conf, _translationLookup, _pluginImporter); + // todo: this might not always be an IW4MServer + var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer; await ServerInstance.Initialize(); _servers.Add(ServerInstance); @@ -759,11 +764,6 @@ namespace IW4MAdmin.Application return Handler; } - public IList GetPluginAssemblies() - { - return _pluginImporter.PluginAssemblies.Union(_pluginImporter.Assemblies).ToList(); - } - public IPageList GetPageList() { return PageList; diff --git a/Application/EventParsers/BaseEventParser.cs b/Application/EventParsers/BaseEventParser.cs index cb73d54ab..765b7bb74 100644 --- a/Application/EventParsers/BaseEventParser.cs +++ b/Application/EventParsers/BaseEventParser.cs @@ -9,7 +9,7 @@ using static SharedLibraryCore.Server; namespace IW4MAdmin.Application.EventParsers { - class BaseEventParser : IEventParser + public class BaseEventParser : IEventParser { public BaseEventParser() { @@ -37,7 +37,7 @@ namespace IW4MAdmin.Application.EventParsers 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-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.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2); Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3); @@ -52,7 +52,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-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.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2); Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3); diff --git a/Application/Factories/ConfigurationHandlerFactory.cs b/Application/Factories/ConfigurationHandlerFactory.cs new file mode 100644 index 000000000..6f2c8ec16 --- /dev/null +++ b/Application/Factories/ConfigurationHandlerFactory.cs @@ -0,0 +1,23 @@ +using IW4MAdmin.Application.Misc; +using SharedLibraryCore.Interfaces; + +namespace IW4MAdmin.Application.Factories +{ + /// + /// implementation of IConfigurationHandlerFactory + /// provides base functionality to create configuration handlers + /// + public class ConfigurationHandlerFactory : IConfigurationHandlerFactory + { + /// + /// creates a base configuration handler + /// + /// base configuration type + /// name of the config file + /// + public IConfigurationHandler GetConfigurationHandler(string name) where T : IBaseConfiguration + { + return new BaseConfigurationHandler(name); + } + } +} diff --git a/Application/Factories/GameServerInstanceFactory.cs b/Application/Factories/GameServerInstanceFactory.cs new file mode 100644 index 000000000..3f4f9e7b8 --- /dev/null +++ b/Application/Factories/GameServerInstanceFactory.cs @@ -0,0 +1,38 @@ +using SharedLibraryCore; +using SharedLibraryCore.Configuration; +using SharedLibraryCore.Interfaces; +using System.Collections; + +namespace IW4MAdmin.Application.Factories +{ + /// + /// implementation of IGameServerInstanceFactory + /// + internal class GameServerInstanceFactory : IGameServerInstanceFactory + { + private readonly ITranslationLookup _translationLookup; + private readonly IRConConnectionFactory _rconConnectionFactory; + + /// + /// base constructor + /// + /// + /// + public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory) + { + _translationLookup = translationLookup; + _rconConnectionFactory = rconConnectionFactory; + } + + /// + /// creates an IW4MServer instance + /// + /// server configuration + /// application manager + /// + public Server CreateServer(ServerConfiguration config, IManager manager) + { + return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory); + } + } +} diff --git a/Application/Factories/RConConnectionFactory.cs b/Application/Factories/RConConnectionFactory.cs new file mode 100644 index 000000000..1dd0d2f88 --- /dev/null +++ b/Application/Factories/RConConnectionFactory.cs @@ -0,0 +1,36 @@ +using IW4MAdmin.Application.RCon; +using SharedLibraryCore.Interfaces; +using System.Text; + +namespace IW4MAdmin.Application.Factories +{ + /// + /// implementation of IRConConnectionFactory + /// + internal class RConConnectionFactory : IRConConnectionFactory + { + private static readonly Encoding gameEncoding = Encoding.GetEncoding("windows-1252"); + private readonly ILogger _logger; + + /// + /// Base constructor + /// + /// + public RConConnectionFactory(ILogger logger) + { + _logger = logger; + } + + /// + /// creates a new rcon connection instance + /// + /// ip address of the server + /// port of the server + /// rcon password of the server + /// + public IRConConnection CreateConnection(string ipAddress, int port, string password) + { + return new RConConnection(ipAddress, port, password, _logger, gameEncoding); + } + } +} diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs index 8322c1e87..21ecc8884 100644 --- a/Application/IW4MServer.cs +++ b/Application/IW4MServer.cs @@ -27,15 +27,14 @@ namespace IW4MAdmin private GameLogEventDetection LogEvent; private readonly ITranslationLookup _translationLookup; private const int REPORT_FLAG_COUNT = 4; - private readonly IPluginImporter _pluginImporter; private int lastGameTime = 0; public int Id { get; private set; } - public IW4MServer(IManager mgr, ServerConfiguration cfg, ITranslationLookup lookup, IPluginImporter pluginImporter) : base(mgr, cfg) + public IW4MServer(IManager mgr, ServerConfiguration cfg, ITranslationLookup lookup, + IRConConnectionFactory connectionFactory) : base(mgr, connectionFactory, cfg) { _translationLookup = lookup; - _pluginImporter = pluginImporter; } override public async Task OnClientConnected(EFClient clientFromLog) @@ -66,6 +65,7 @@ namespace IW4MAdmin client.Score = clientFromLog.Score; client.Ping = clientFromLog.Ping; client.CurrentServer = this; + client.State = ClientState.Connecting; Clients[client.ClientNumber] = client; #if DEBUG == true @@ -153,7 +153,7 @@ namespace IW4MAdmin } } - foreach (var plugin in _pluginImporter.ActivePlugins) + foreach (var plugin in Manager.Plugins) { try { @@ -182,6 +182,11 @@ namespace IW4MAdmin catch (Exception e) { lastException = e; + + if (E.Origin != null) + { + E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]); + } } finally @@ -695,7 +700,7 @@ namespace IW4MAdmin await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token); } - foreach (var plugin in _pluginImporter.ActivePlugins) + foreach (var plugin in Manager.Plugins) { await plugin.OnUnloadAsync(); } diff --git a/Application/Main.cs b/Application/Main.cs index b96961fa6..2e1c7d541 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -1,4 +1,6 @@ -using IW4MAdmin.Application.Helpers; +using IW4MAdmin.Application.Factories; +using IW4MAdmin.Application.Helpers; +using IW4MAdmin.Application.IO; using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.Misc; using Microsoft.Extensions.DependencyInjection; @@ -8,7 +10,6 @@ using SharedLibraryCore.Exceptions; using SharedLibraryCore.Helpers; using SharedLibraryCore.Interfaces; using System; -using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; @@ -266,27 +267,21 @@ namespace IW4MAdmin.Application /// private static IServiceCollection ConfigureServices() { + var defaultLogger = new Logger("IW4MAdmin-Manager"); + var pluginImporter = new PluginImporter(defaultLogger); + var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(_serviceProvider => serviceCollection) .AddSingleton(new BaseConfigurationHandler("IW4MAdminSettings") as IConfigurationHandler) .AddSingleton(new BaseConfigurationHandler("CommandConfiguration") as IConfigurationHandler) .AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService>().Configuration()) .AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService>().Configuration() ?? new CommandConfiguration()) - .AddSingleton(_serviceProvider => new Logger("IW4MAdmin-Manager")) + .AddSingleton(_serviceProvider => defaultLogger) .AddSingleton() .AddSingleton() - .AddTransient(_serviceProvider => - { - var importer = _serviceProvider.GetRequiredService(); - var config = _serviceProvider.GetRequiredService(); - var layout = _serviceProvider.GetRequiredService(); - - // todo: this is disgusting, but I need it until I can figure out a way to dynamically load the plugins without creating an instance. - return importer.CommandTypes. - Union(typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes() - .Where(_command => _command.BaseType == typeof(Command))) - .Select(_cmdType => Activator.CreateInstance(_cmdType, config, layout) as IManagerCommand); - }) + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton(_serviceProvider => { var config = _serviceProvider.GetRequiredService>().Configuration(); @@ -295,6 +290,35 @@ namespace IW4MAdmin.Application }) .AddSingleton(); + // register the native commands + foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes() + .Where(_command => _command.BaseType == typeof(Command))) + { + defaultLogger.WriteInfo($"Registered native command type {commandType.Name}"); + serviceCollection.AddSingleton(typeof(IManagerCommand), commandType); + } + + // register the plugin implementations + var pluginImplementations = pluginImporter.DiscoverAssemblyPluginImplementations(); + foreach (var pluginType in pluginImplementations.Item1) + { + defaultLogger.WriteInfo($"Registered plugin type {pluginType.FullName}"); + serviceCollection.AddSingleton(typeof(IPlugin), pluginType); + } + + // register the plugin commands + foreach (var commandType in pluginImplementations.Item2) + { + defaultLogger.WriteInfo($"Registered plugin command type {commandType.FullName}"); + serviceCollection.AddSingleton(typeof(IManagerCommand), commandType); + } + + // register any script plugins + foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins()) + { + serviceCollection.AddSingleton(scriptPlugin); + } + return serviceCollection; } } diff --git a/Application/Migration/ConfigurationMigration.cs b/Application/Migration/ConfigurationMigration.cs index 487f7e735..d7a421b4f 100644 --- a/Application/Migration/ConfigurationMigration.cs +++ b/Application/Migration/ConfigurationMigration.cs @@ -34,6 +34,11 @@ namespace IW4MAdmin.Application.Migration { Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log")); } + + if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Localization"))) + { + Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Localization")); + } } /// diff --git a/SharedLibraryCore/Helpers/BaseConfigurationHandler.cs b/Application/Misc/BaseConfigurationHandler.cs similarity index 89% rename from SharedLibraryCore/Helpers/BaseConfigurationHandler.cs rename to Application/Misc/BaseConfigurationHandler.cs index 38737c2b1..23d8295b8 100644 --- a/SharedLibraryCore/Helpers/BaseConfigurationHandler.cs +++ b/Application/Misc/BaseConfigurationHandler.cs @@ -1,12 +1,17 @@ using Newtonsoft.Json; +using SharedLibraryCore; using SharedLibraryCore.Exceptions; using SharedLibraryCore.Interfaces; using System; using System.IO; using System.Threading.Tasks; -namespace SharedLibraryCore.Configuration +namespace IW4MAdmin.Application.Misc { + /// + /// default implementation of IConfigurationHandler + /// + /// base configuration type public class BaseConfigurationHandler : IConfigurationHandler where T : IBaseConfiguration { T _configuration; diff --git a/Application/Misc/Logger.cs b/Application/Misc/Logger.cs index e1962db8e..96cbda547 100644 --- a/Application/Misc/Logger.cs +++ b/Application/Misc/Logger.cs @@ -1,12 +1,14 @@ -using SharedLibraryCore; +using IW4MAdmin.Application.IO; +using SharedLibraryCore; using SharedLibraryCore.Interfaces; using System; +using System.Diagnostics; using System.IO; using System.Threading; namespace IW4MAdmin.Application { - class Logger : ILogger + public class Logger : ILogger { enum LogType { @@ -77,9 +79,7 @@ namespace IW4MAdmin.Application { #if DEBUG // lets keep it simple and dispose of everything quickly as logging wont be that much (relatively) - Console.WriteLine(LogLine); - //File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}"); - //Debug.WriteLine(msg); + Console.WriteLine(msg); #else if (type == LogType.Error || type == LogType.Verbose) { diff --git a/Application/Misc/PluginImporter.cs b/Application/Misc/PluginImporter.cs index cb6c73e16..828333976 100644 --- a/Application/Misc/PluginImporter.cs +++ b/Application/Misc/PluginImporter.cs @@ -5,111 +5,84 @@ using System.Reflection; using SharedLibraryCore.Interfaces; using System.Linq; using SharedLibraryCore; +using IW4MAdmin.Application.Misc; namespace IW4MAdmin.Application.Helpers { + /// + /// implementation of IPluginImporter + /// discovers plugins and script plugins + /// public class PluginImporter : IPluginImporter { - public IList CommandTypes { get; private set; } = new List(); - public IList ActivePlugins { get; private set; } = new List(); - public IList PluginAssemblies { get; private set; } = new List(); - public IList Assemblies { get; private set; } = new List(); - + private static readonly string PLUGIN_DIR = "Plugins"; private readonly ILogger _logger; - private readonly ITranslationLookup _translationLookup; - public PluginImporter(ILogger logger, ITranslationLookup translationLookup) + public PluginImporter(ILogger logger) { _logger = logger; - _translationLookup = translationLookup; - - Load(); } /// - /// Loads all the assembly and javascript plugins + /// discovers all the script plugins in the plugins dir /// - private void Load() + /// + public IEnumerable DiscoverScriptPlugins() { - string pluginDir = $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}"; - string[] dllFileNames = null; - string[] scriptFileNames = null; + string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}"; if (Directory.Exists(pluginDir)) { - dllFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.dll"); - scriptFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.js"); - } + string[] scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js"); - else - { - dllFileNames = new string[0]; - scriptFileNames = new string[0]; - } + _logger.WriteInfo($"Discovered {scriptPluginFiles.Length} potential script plugins"); - if (dllFileNames.Length == 0 && - scriptFileNames.Length == 0) - { - _logger.WriteDebug(_translationLookup["PLUGIN_IMPORTER_NOTFOUND"]); - return; - } - - // load up the script plugins - foreach (string fileName in scriptFileNames) - { - var plugin = new ScriptPlugin(fileName); - _logger.WriteDebug($"Loaded script plugin \"{ plugin.Name }\" [{plugin.Version}]"); - ActivePlugins.Add(plugin); - } - - ICollection assemblies = new List(dllFileNames.Length); - foreach (string dllFile in dllFileNames) - { - assemblies.Add(Assembly.LoadFrom(dllFile)); - } - - int LoadedCommands = 0; - foreach (Assembly Plugin in assemblies) - { - if (Plugin != null) + if (scriptPluginFiles.Length > 0) { - Assemblies.Add(Plugin); - Type[] types = Plugin.GetTypes(); - foreach (Type assemblyType in types) + foreach (string fileName in scriptPluginFiles) { - if (assemblyType.IsClass && assemblyType.BaseType == typeof(Command)) - { - CommandTypes.Add(assemblyType); - _logger.WriteDebug($"{_translationLookup["PLUGIN_IMPORTER_REGISTERCMD"]} \"{assemblyType.Name}\""); - LoadedCommands++; - continue; - } - - try - { - if (assemblyType.GetInterface("IPlugin", false) == null) - continue; - - var notifyObject = Activator.CreateInstance(assemblyType); - IPlugin newNotify = (IPlugin)notifyObject; - if (ActivePlugins.FirstOrDefault(x => x.Name == newNotify.Name) == null) - { - ActivePlugins.Add(newNotify); - PluginAssemblies.Add(Plugin); - _logger.WriteDebug($"Loaded plugin \"{newNotify.Name}\" [{newNotify.Version}]"); - } - } - - catch (Exception e) - { - _logger.WriteWarning(_translationLookup["PLUGIN_IMPORTER_ERROR"].FormatExt(Plugin.Location)); - _logger.WriteDebug(e.GetExceptionInfo()); - } + _logger.WriteInfo($"Discovered script plugin {fileName}"); + var plugin = new ScriptPlugin(fileName); + yield return plugin; } } } + } - _logger.WriteInfo($"Loaded {ActivePlugins.Count} plugins and registered {LoadedCommands} plugin commands."); + /// + /// discovers all the C# assembly plugins and commands + /// + /// + public (IEnumerable, IEnumerable) DiscoverAssemblyPluginImplementations() + { + string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}"; + var pluginTypes = Enumerable.Empty(); + var commandTypes = Enumerable.Empty(); + + if (Directory.Exists(pluginDir)) + { + var dllFileNames = Directory.GetFiles(pluginDir, "*.dll"); + _logger.WriteInfo($"Discovered {dllFileNames.Length} potential plugin assemblies"); + + if (dllFileNames.Length > 0) + { + var assemblies = dllFileNames.Select(_name => Assembly.LoadFrom(_name)); + + pluginTypes = assemblies + .SelectMany(_asm => _asm.GetTypes()) + .Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null); + + _logger.WriteInfo($"Discovered {pluginTypes.Count()} plugin implementations"); + + commandTypes = assemblies + .SelectMany(_asm => _asm.GetTypes()) + .Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command)); + + _logger.WriteInfo($"Discovered {commandTypes.Count()} plugin commands"); + } + } + + return (pluginTypes, commandTypes); } } } diff --git a/SharedLibraryCore/ScriptPlugin.cs b/Application/Misc/ScriptPlugin.cs similarity index 97% rename from SharedLibraryCore/ScriptPlugin.cs rename to Application/Misc/ScriptPlugin.cs index 79e49138f..b9b91503f 100644 --- a/SharedLibraryCore/ScriptPlugin.cs +++ b/Application/Misc/ScriptPlugin.cs @@ -1,15 +1,19 @@ using Jint; +using SharedLibraryCore; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Interfaces; -using System; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; -namespace SharedLibraryCore +namespace IW4MAdmin.Application.Misc { + /// + /// implementation of IPlugin + /// used to proxy script plugin requests + /// public class ScriptPlugin : IPlugin { public string Name { get; set; } @@ -45,7 +49,6 @@ namespace SharedLibraryCore _onProcessing.Dispose(); } - public async Task Initialize(IManager manager) { await _onProcessing.WaitAsync(); diff --git a/Application/RCon/ConnectionState.cs b/Application/RCon/ConnectionState.cs new file mode 100644 index 000000000..0dcfcc660 --- /dev/null +++ b/Application/RCon/ConnectionState.cs @@ -0,0 +1,29 @@ +using System; +using System.Net.Sockets; +using System.Threading; + +namespace IW4MAdmin.Application.RCon +{ + /// + /// used to keep track of the udp connection state + /// + internal class ConnectionState + { + ~ConnectionState() + { + OnComplete.Dispose(); + OnSentData.Dispose(); + OnReceivedData.Dispose(); + } + + public int ConnectionAttempts { get; set; } + const int BufferSize = 4096; + public readonly byte[] ReceiveBuffer = new byte[BufferSize]; + public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1); + public readonly ManualResetEventSlim OnSentData = new ManualResetEventSlim(false); + public readonly ManualResetEventSlim OnReceivedData = new ManualResetEventSlim(false); + public SocketAsyncEventArgs SendEventArgs { get; set; } = new SocketAsyncEventArgs(); + public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new SocketAsyncEventArgs(); + public DateTime LastQuery { get; set; } = DateTime.Now; + } +} diff --git a/SharedLibraryCore/RCon/Connection.cs b/Application/RCon/RConConnection.cs similarity index 77% rename from SharedLibraryCore/RCon/Connection.cs rename to Application/RCon/RConConnection.cs index 904e36e03..63616a812 100644 --- a/SharedLibraryCore/RCon/Connection.cs +++ b/Application/RCon/RConConnection.cs @@ -1,58 +1,41 @@ -using SharedLibraryCore.Exceptions; +using SharedLibraryCore; +using SharedLibraryCore.Exceptions; using SharedLibraryCore.Interfaces; +using SharedLibraryCore.RCon; using System; using System.Collections.Concurrent; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; -using System.Threading; using System.Threading.Tasks; -namespace SharedLibraryCore.RCon +namespace IW4MAdmin.Application.RCon { - class ConnectionState - { - ~ConnectionState() - { - OnComplete.Dispose(); - OnSentData.Dispose(); - OnReceivedData.Dispose(); - } - - public int ConnectionAttempts { get; set; } - const int BufferSize = 4096; - public readonly byte[] ReceiveBuffer = new byte[BufferSize]; - public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1); - public readonly ManualResetEventSlim OnSentData = new ManualResetEventSlim(false); - public readonly ManualResetEventSlim OnReceivedData = new ManualResetEventSlim(false); - public SocketAsyncEventArgs SendEventArgs { get; set; } = new SocketAsyncEventArgs(); - public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new SocketAsyncEventArgs(); - public DateTime LastQuery { get; set; } = DateTime.Now; - } - - public class Connection + /// + /// implementation of IRConConnection + /// + public class RConConnection : IRConConnection { static readonly ConcurrentDictionary ActiveQueries = new ConcurrentDictionary(); public IPEndPoint Endpoint { get; private set; } public string RConPassword { get; private set; } - private readonly ILogger Log; - private IRConParserConfiguration Config; - private readonly Encoding defaultEncoding; + private IRConParserConfiguration config; + private readonly ILogger _log; + private readonly Encoding _gameEncoding; - public Connection(string ipAddress, int port, string password, ILogger log, IRConParserConfiguration config) + public RConConnection(string ipAddress, int port, string password, ILogger log, Encoding gameEncoding) { Endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), port); - defaultEncoding = Encoding.GetEncoding("windows-1252"); + _gameEncoding = gameEncoding; RConPassword = password; - Log = log; - Config = config; + _log = log; } public void SetConfiguration(IRConParserConfiguration config) { - Config = config; + this.config = config; } public async Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") @@ -65,7 +48,7 @@ namespace SharedLibraryCore.RCon var connectionState = ActiveQueries[this.Endpoint]; #if DEBUG == true - Log.WriteDebug($"Waiting for semaphore to be released [{this.Endpoint}]"); + _log.WriteDebug($"Waiting for semaphore to be released [{this.Endpoint}]"); #endif // enter the semaphore so only one query is sent at a time per server. await connectionState.OnComplete.WaitAsync(); @@ -80,17 +63,17 @@ namespace SharedLibraryCore.RCon connectionState.LastQuery = DateTime.Now; #if DEBUG == true - Log.WriteDebug($"Semaphore has been released [{this.Endpoint}]"); - Log.WriteDebug($"Query [{this.Endpoint},{type.ToString()},{parameters}]"); + _log.WriteDebug($"Semaphore has been released [{this.Endpoint}]"); + _log.WriteDebug($"Query [{this.Endpoint},{type.ToString()},{parameters}]"); #endif byte[] payload = null; - bool waitForResponse = Config.WaitForResponse; + bool waitForResponse = config.WaitForResponse; string convertEncoding(string text) { byte[] convertedBytes = Utilities.EncodingType.GetBytes(text); - return defaultEncoding.GetString(convertedBytes); + return _gameEncoding.GetString(convertedBytes); } try @@ -102,25 +85,25 @@ namespace SharedLibraryCore.RCon { case StaticHelpers.QueryType.GET_DVAR: waitForResponse |= true; - payload = string.Format(Config.CommandPrefixes.RConGetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray(); + payload = string.Format(config.CommandPrefixes.RConGetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray(); break; case StaticHelpers.QueryType.SET_DVAR: - payload = string.Format(Config.CommandPrefixes.RConSetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray(); + payload = string.Format(config.CommandPrefixes.RConSetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray(); break; case StaticHelpers.QueryType.COMMAND: - payload = string.Format(Config.CommandPrefixes.RConCommand, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray(); + payload = string.Format(config.CommandPrefixes.RConCommand, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray(); break; case StaticHelpers.QueryType.GET_STATUS: waitForResponse |= true; - payload = (Config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray(); + payload = (config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray(); break; case StaticHelpers.QueryType.GET_INFO: waitForResponse |= true; - payload = (Config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray(); + payload = (config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray(); break; case StaticHelpers.QueryType.COMMAND_STATUS: waitForResponse |= true; - payload = string.Format(Config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0").Select(Convert.ToByte).ToArray(); + payload = string.Format(config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0").Select(Convert.ToByte).ToArray(); break; } } @@ -148,7 +131,7 @@ namespace SharedLibraryCore.RCon connectionState.OnReceivedData.Reset(); connectionState.ConnectionAttempts++; #if DEBUG == true - Log.WriteDebug($"Sending {payload.Length} bytes to [{this.Endpoint}] ({connectionState.ConnectionAttempts}/{StaticHelpers.AllowedConnectionFails})"); + _log.WriteDebug($"Sending {payload.Length} bytes to [{this.Endpoint}] ({connectionState.ConnectionAttempts}/{StaticHelpers.AllowedConnectionFails})"); #endif try { @@ -182,7 +165,7 @@ namespace SharedLibraryCore.RCon } } - string responseString = defaultEncoding.GetString(response, 0, response.Length) + '\n'; + string responseString = _gameEncoding.GetString(response, 0, response.Length) + '\n'; // note: not all games respond if the pasword is wrong or not set if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword")) @@ -195,7 +178,7 @@ namespace SharedLibraryCore.RCon throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_NOTSET"]); } - if (responseString.Contains(Config.ServerNotRunningResponse)) + if (responseString.Contains(config.ServerNotRunningResponse)) { throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString())); } @@ -269,7 +252,7 @@ namespace SharedLibraryCore.RCon private void OnDataReceived(object sender, SocketAsyncEventArgs e) { #if DEBUG == true - Log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}"); + _log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}"); #endif ActiveQueries[this.Endpoint].OnReceivedData.Set(); } @@ -277,7 +260,7 @@ namespace SharedLibraryCore.RCon private void OnDataSent(object sender, SocketAsyncEventArgs e) { #if DEBUG == true - Log.WriteDebug($"Sent {e.Buffer?.Length} bytes to {e.ConnectSocket?.RemoteEndPoint?.ToString()}"); + _log.WriteDebug($"Sent {e.Buffer?.Length} bytes to {e.ConnectSocket?.RemoteEndPoint?.ToString()}"); #endif ActiveQueries[this.Endpoint].OnSentData.Set(); } diff --git a/Application/RconParsers/BaseRConParser.cs b/Application/RconParsers/BaseRConParser.cs index b16349cbb..55cd23074 100644 --- a/Application/RconParsers/BaseRConParser.cs +++ b/Application/RconParsers/BaseRConParser.cs @@ -65,13 +65,13 @@ namespace IW4MAdmin.Application.RconParsers public bool CanGenerateLogPath { get; set; } = true; public string Name { get; set; } = "Call of Duty"; - public async Task ExecuteCommandAsync(Connection connection, string command) + public async Task ExecuteCommandAsync(IRConConnection connection, string command) { var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command); return response.Skip(1).ToArray(); } - public async Task> GetDvarAsync(Connection connection, string dvarName) + public async Task> GetDvarAsync(IRConConnection connection, string dvarName) { string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName); string response = string.Join('\n', lineSplit.Skip(1)); @@ -105,7 +105,7 @@ namespace IW4MAdmin.Application.RconParsers }; } - public virtual async Task<(List, string)> GetStatusAsync(Connection connection) + public virtual async Task<(List, string)> GetStatusAsync(IRConConnection connection) { string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS); #if DEBUG @@ -132,7 +132,7 @@ namespace IW4MAdmin.Application.RconParsers return map; } - public async Task SetDvarAsync(Connection connection, string dvarName, object dvarValue) + public async Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue) { return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, $"{dvarName} {dvarValue}")).Length > 0; } diff --git a/GameFiles/IW4x/userraw/_commands.gsc b/GameFiles/IW4x/userraw/scripts/_commands.gsc similarity index 100% rename from GameFiles/IW4x/userraw/_commands.gsc rename to GameFiles/IW4x/userraw/scripts/_commands.gsc diff --git a/GameFiles/IW4x/userraw/_customcallbacks.gsc b/GameFiles/IW4x/userraw/scripts/_customcallbacks.gsc similarity index 100% rename from GameFiles/IW4x/userraw/_customcallbacks.gsc rename to GameFiles/IW4x/userraw/scripts/_customcallbacks.gsc diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index eeebf6fa6..5550abe48 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -6,8 +6,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}" ProjectSection(SolutionItems) = preProject - GameFiles\IW4x\userraw\_commands.gsc = GameFiles\IW4x\userraw\_commands.gsc - GameFiles\IW4x\userraw\_customcallbacks.gsc = GameFiles\IW4x\userraw\_customcallbacks.gsc + GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc + GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc azure-pipelines.yml = azure-pipelines.yml PostPublish.ps1 = PostPublish.ps1 README.md = README.md @@ -57,6 +57,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveRadar", "Plugins\LiveRadar\LiveRadar.csproj", "{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3065279E-17F0-4CE0-AF5B-014E04263D77}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationTests", "Tests\ApplicationTests\ApplicationTests.csproj", "{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -405,6 +409,29 @@ Global {00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x64.Build.0 = Release|Any CPU {00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x86.ActiveCfg = Release|Any CPU {00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x86.Build.0 = Release|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x64.ActiveCfg = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x64.Build.0 = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x86.ActiveCfg = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x86.Build.0 = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x64.ActiveCfg = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x64.Build.0 = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x86.ActiveCfg = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x86.Build.0 = Debug|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Any CPU.Build.0 = Release|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x64.ActiveCfg = Release|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x64.Build.0 = Release|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x86.ActiveCfg = Release|Any CPU + {581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -421,6 +448,7 @@ Global {776B348B-F818-4A0F-A625-D0AF8BAD3E9B} = {A848FCF1-8527-4AA8-A1AA-50D29695C678} {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87} diff --git a/Plugins/AutomessageFeed/AutomessageFeed.csproj b/Plugins/AutomessageFeed/AutomessageFeed.csproj index a4c2f5c6c..bfc986656 100644 --- a/Plugins/AutomessageFeed/AutomessageFeed.csproj +++ b/Plugins/AutomessageFeed/AutomessageFeed.csproj @@ -10,7 +10,7 @@ - + diff --git a/Plugins/AutomessageFeed/Plugin.cs b/Plugins/AutomessageFeed/Plugin.cs index b738ad39f..bb43a00d7 100644 --- a/Plugins/AutomessageFeed/Plugin.cs +++ b/Plugins/AutomessageFeed/Plugin.cs @@ -20,14 +20,19 @@ namespace AutomessageFeed public string Author => "RaidMax"; - private Configuration _configuration; private int _currentFeedItem; + private readonly IConfigurationHandler _configurationHandler; + + public Plugin(IConfigurationHandlerFactory configurationHandlerFactory) + { + _configurationHandler = configurationHandlerFactory.GetConfigurationHandler("AutomessageFeedPluginSettings"); + } private async Task GetNextFeedItem(Server server) { var items = new List(); - using (var reader = XmlReader.Create(_configuration.FeedUrl, new XmlReaderSettings() { Async = true })) + using (var reader = XmlReader.Create(_configurationHandler.Configuration().FeedUrl, new XmlReaderSettings() { Async = true })) { var feedReader = new RssFeedReader(reader); @@ -43,7 +48,7 @@ namespace AutomessageFeed } } - if (_currentFeedItem < items.Count && (_configuration.MaxFeedItems == 0 || _currentFeedItem < _configuration.MaxFeedItems)) + if (_currentFeedItem < items.Count && (_configurationHandler.Configuration().MaxFeedItems == 0 || _currentFeedItem < _configurationHandler.Configuration().MaxFeedItems)) { _currentFeedItem++; return items[_currentFeedItem - 1]; @@ -60,15 +65,12 @@ namespace AutomessageFeed public async Task OnLoadAsync(IManager manager) { - var cfg = new BaseConfigurationHandler("AutomessageFeedPluginSettings"); - if (cfg.Configuration() == null) + if (_configurationHandler.Configuration() == null) { - cfg.Set((Configuration)new Configuration().Generate()); - await cfg.Save(); + _configurationHandler.Set((Configuration)new Configuration().Generate()); + await _configurationHandler.Save(); } - _configuration = cfg.Configuration(); - manager.GetMessageTokens().Add(new MessageToken("FEED", GetNextFeedItem)); } diff --git a/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj b/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj index bde75dc1b..90c95642e 100644 --- a/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj +++ b/Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj @@ -10,7 +10,7 @@ - + diff --git a/Plugins/LiveRadar/Controllers/RadarController.cs b/Plugins/LiveRadar/Controllers/RadarController.cs index ed3b789d2..17f6e3930 100644 --- a/Plugins/LiveRadar/Controllers/RadarController.cs +++ b/Plugins/LiveRadar/Controllers/RadarController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using LiveRadar.Configuration; +using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using SharedLibraryCore; using SharedLibraryCore.Dtos; @@ -16,10 +17,12 @@ namespace LiveRadar.Web.Controllers }; private readonly IManager _manager; + private readonly LiveRadarConfiguration _config; - public RadarController(IManager manager) : base(manager) + public RadarController(IManager manager, IConfigurationHandlerFactory configurationHandlerFactory) : base(manager) { _manager = manager; + _config = configurationHandlerFactory.GetConfigurationHandler("LiveRadarConfiguration").Configuration() ?? new LiveRadarConfiguration(); } [HttpGet] @@ -45,7 +48,7 @@ namespace LiveRadar.Web.Controllers public IActionResult Map(long? serverId = null) { var server = serverId == null ? _manager.GetServers().FirstOrDefault() : _manager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId); - var map = Plugin.Config.Configuration().Maps.FirstOrDefault(_map => _map.Name == server.CurrentMap.Name); + var map = _config.Maps.FirstOrDefault(_map => _map.Name == server.CurrentMap.Name); if (map != null) { diff --git a/Plugins/LiveRadar/LiveRadar.csproj b/Plugins/LiveRadar/LiveRadar.csproj index bec099675..f1cf46f6d 100644 --- a/Plugins/LiveRadar/LiveRadar.csproj +++ b/Plugins/LiveRadar/LiveRadar.csproj @@ -16,7 +16,7 @@ - + diff --git a/Plugins/LiveRadar/Plugin.cs b/Plugins/LiveRadar/Plugin.cs index 3389d7ddc..ed16cc8f0 100644 --- a/Plugins/LiveRadar/Plugin.cs +++ b/Plugins/LiveRadar/Plugin.cs @@ -16,7 +16,12 @@ namespace LiveRadar public string Author => "RaidMax"; - internal static BaseConfigurationHandler Config; + private readonly IConfigurationHandler _configurationHandler; + + public Plugin(IConfigurationHandlerFactory configurationHandlerFactory) + { + _configurationHandler = configurationHandlerFactory.GetConfigurationHandler("LiveRadarConfiguration"); + } public Task OnEventAsync(GameEvent E, Server S) { @@ -36,7 +41,7 @@ namespace LiveRadar } } - catch(Exception e) + catch (Exception e) { S.Logger.WriteWarning($"Could not parse live radar output: {e.Data}"); S.Logger.WriteDebug(e.GetExceptionInfo()); @@ -49,12 +54,10 @@ namespace LiveRadar public async Task OnLoadAsync(IManager manager) { - // load custom configuration - Config = new BaseConfigurationHandler("LiveRadarConfiguration"); - if (Config.Configuration() == null) + if (_configurationHandler.Configuration() == null) { - Config.Set((LiveRadarConfiguration)new LiveRadarConfiguration().Generate()); - await Config.Save(); + _configurationHandler.Set((LiveRadarConfiguration)new LiveRadarConfiguration().Generate()); + await _configurationHandler.Save(); } manager.GetPageList().Pages.Add(Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"], "/Radar/All"); diff --git a/Plugins/Login/Login.csproj b/Plugins/Login/Login.csproj index ed3444425..9cef02c85 100644 --- a/Plugins/Login/Login.csproj +++ b/Plugins/Login/Login.csproj @@ -23,7 +23,7 @@ - + diff --git a/Plugins/Login/Plugin.cs b/Plugins/Login/Plugin.cs index b18c00aee..7d7936c2b 100644 --- a/Plugins/Login/Plugin.cs +++ b/Plugins/Login/Plugin.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using IW4MAdmin.Plugins.Login.Commands; using SharedLibraryCore; using SharedLibraryCore.Commands; -using SharedLibraryCore.Configuration; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Exceptions; using SharedLibraryCore.Interfaces; @@ -20,11 +19,16 @@ namespace IW4MAdmin.Plugins.Login public string Author => "RaidMax"; public static ConcurrentDictionary AuthorizedClients { get; private set; } - private Configuration Config; + private readonly IConfigurationHandler _configHandler; + + public Plugin(IConfigurationHandlerFactory configurationHandlerFactory) + { + _configHandler = configurationHandlerFactory.GetConfigurationHandler("LoginPluginSettings"); + } public Task OnEventAsync(GameEvent E, Server S) { - if (E.IsRemote || Config.RequirePrivilegedClientLogin == false) + if (E.IsRemote || _configHandler.Configuration().RequirePrivilegedClientLogin == false) return Task.CompletedTask; if (E.Type == GameEvent.EventType.Connect) @@ -72,14 +76,11 @@ namespace IW4MAdmin.Plugins.Login { AuthorizedClients = new ConcurrentDictionary(); - var cfg = new BaseConfigurationHandler("LoginPluginSettings"); - if (cfg.Configuration() == null) + if (_configHandler.Configuration() == null) { - cfg.Set((Configuration)new Configuration().Generate()); - await cfg.Save(); + _configHandler.Set((Configuration)new Configuration().Generate()); + await _configHandler.Save(); } - - Config = cfg.Configuration(); } public Task OnTickAsync(Server S) => Task.CompletedTask; diff --git a/Plugins/ProfanityDeterment/Plugin.cs b/Plugins/ProfanityDeterment/Plugin.cs index 60ee40613..de2fda40d 100644 --- a/Plugins/ProfanityDeterment/Plugin.cs +++ b/Plugins/ProfanityDeterment/Plugin.cs @@ -17,18 +17,23 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment public string Author => "RaidMax"; - BaseConfigurationHandler Settings; + private readonly IConfigurationHandler _configHandler; + + public Plugin(IConfigurationHandlerFactory configurationHandlerFactory) + { + _configHandler = configurationHandlerFactory.GetConfigurationHandler("ProfanityDetermentSettings"); + } public Task OnEventAsync(GameEvent E, Server S) { - if (!Settings.Configuration().EnableProfanityDeterment) + if (!_configHandler.Configuration().EnableProfanityDeterment) return Task.CompletedTask; if (E.Type == GameEvent.EventType.Connect) { E.Origin.SetAdditionalProperty("_profanityInfringements", 0); - var objectionalWords = Settings.Configuration().OffensiveWords; + var objectionalWords = _configHandler.Configuration().OffensiveWords; var matchedFilters = new List(); bool containsObjectionalWord = false; @@ -51,7 +56,7 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment AutomatedOffense = $"{E.Origin.Name} - {string.Join(",", matchedFilters)}" } }; - E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, sender); + E.Origin.Kick(_configHandler.Configuration().ProfanityKickMessage, sender); }; } @@ -62,7 +67,7 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment if (E.Type == GameEvent.EventType.Say) { - var objectionalWords = Settings.Configuration().OffensiveWords; + var objectionalWords = _configHandler.Configuration().OffensiveWords; bool containsObjectionalWord = false; var matchedFilters = new List(); @@ -88,15 +93,15 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment } }; - if (profanityInfringments >= Settings.Configuration().KickAfterInfringementCount) + if (profanityInfringments >= _configHandler.Configuration().KickAfterInfringementCount) { - E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, sender); + E.Origin.Kick(_configHandler.Configuration().ProfanityKickMessage, sender); } - else if (profanityInfringments < Settings.Configuration().KickAfterInfringementCount) + else if (profanityInfringments < _configHandler.Configuration().KickAfterInfringementCount) { E.Origin.SetAdditionalProperty("_profanityInfringements", profanityInfringments + 1); - E.Origin.Warn(Settings.Configuration().ProfanityWarningMessage, sender); + E.Origin.Warn(_configHandler.Configuration().ProfanityWarningMessage, sender); } } } @@ -105,12 +110,10 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment public async Task OnLoadAsync(IManager manager) { - // load custom configuration - Settings = new BaseConfigurationHandler("ProfanityDetermentSettings"); - if (Settings.Configuration() == null) + if (_configHandler.Configuration() == null) { - Settings.Set((Configuration)new Configuration().Generate()); - await Settings.Save(); + _configHandler.Set((Configuration)new Configuration().Generate()); + await _configHandler.Save(); } } diff --git a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj index b6d2e8ec1..33b45bc79 100644 --- a/Plugins/ProfanityDeterment/ProfanityDeterment.csproj +++ b/Plugins/ProfanityDeterment/ProfanityDeterment.csproj @@ -16,7 +16,7 @@ - + diff --git a/Plugins/Stats/Config/StatsConfiguration.cs b/Plugins/Stats/Config/StatsConfiguration.cs index 5ad5818f9..11c05b6e4 100644 --- a/Plugins/Stats/Config/StatsConfiguration.cs +++ b/Plugins/Stats/Config/StatsConfiguration.cs @@ -17,7 +17,7 @@ namespace IW4MAdmin.Plugins.Stats.Config public IDictionary DetectionDistributions { get; set; } public IDictionary ServerDetectionTypes { get; set; } - public string Name() => "Stats"; + public string Name() => "StatsPluginSettings"; public IBaseConfiguration Generate() { EnableAntiCheat = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]); diff --git a/Plugins/Stats/Helpers/StreakMessage.cs b/Plugins/Stats/Helpers/StreakMessage.cs index e665f66b7..504c09d4b 100644 --- a/Plugins/Stats/Helpers/StreakMessage.cs +++ b/Plugins/Stats/Helpers/StreakMessage.cs @@ -21,8 +21,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers var killstreakMessage = Plugin.Config.Configuration().KillstreakMessages; var deathstreakMessage = Plugin.Config.Configuration().DeathstreakMessages; - string message = killstreakMessage.FirstOrDefault(m => m.Count == killStreak)?.Message; - message = message ?? deathstreakMessage.FirstOrDefault(m => m.Count == deathStreak)?.Message; + string message = killstreakMessage?.FirstOrDefault(m => m.Count == killStreak)?.Message; + message = message ?? deathstreakMessage?.FirstOrDefault(m => m.Count == deathStreak)?.Message; return message ?? ""; } } diff --git a/Plugins/Stats/Plugin.cs b/Plugins/Stats/Plugin.cs index 9bd06281f..b1c86a055 100644 --- a/Plugins/Stats/Plugin.cs +++ b/Plugins/Stats/Plugin.cs @@ -3,7 +3,6 @@ using IW4MAdmin.Plugins.Stats.Helpers; using IW4MAdmin.Plugins.Stats.Models; using Microsoft.EntityFrameworkCore; using SharedLibraryCore; -using SharedLibraryCore.Configuration; using SharedLibraryCore.Database; using SharedLibraryCore.Database.Models; using SharedLibraryCore.Dtos; @@ -13,7 +12,6 @@ using SharedLibraryCore.Services; using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading.Tasks; namespace IW4MAdmin.Plugins.Stats @@ -28,12 +26,17 @@ namespace IW4MAdmin.Plugins.Stats public static StatManager Manager { get; private set; } public static IManager ServerManager; - public static BaseConfigurationHandler Config { get; private set; } + public static IConfigurationHandler Config { get; private set; } #if DEBUG int scriptDamageCount; int scriptKillCount; #endif + public Plugin(IConfigurationHandlerFactory configurationHandlerFactory) + { + Config = configurationHandlerFactory.GetConfigurationHandler("StatsPluginSettings"); + } + public async Task OnEventAsync(GameEvent E, Server S) { switch (E.Type) @@ -53,16 +56,16 @@ namespace IW4MAdmin.Plugins.Stats if (!string.IsNullOrEmpty(E.Data) && E.Origin.ClientId > 1) { - await Manager.AddMessageAsync(E.Origin.ClientId, StatManager.GetIdForServer(E.Owner), E.Data); + await Manager.AddMessageAsync(E.Origin.ClientId, StatManager.GetIdForServer(S), E.Data); } break; case GameEvent.EventType.MapChange: - Manager.SetTeamBased(StatManager.GetIdForServer(E.Owner), E.Owner.Gametype != "dm"); - Manager.ResetKillstreaks(E.Owner); - await Manager.Sync(E.Owner); + Manager.SetTeamBased(StatManager.GetIdForServer(S), S.Gametype != "dm"); + Manager.ResetKillstreaks(S); + await Manager.Sync(S); break; case GameEvent.EventType.MapEnd: - await Manager.Sync(E.Owner); + await Manager.Sync(S); break; case GameEvent.EventType.JoinTeam: break; @@ -82,7 +85,7 @@ namespace IW4MAdmin.Plugins.Stats break; case GameEvent.EventType.ScriptKill: string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0]; - if ((E.Owner.CustomCallback || ShouldOverrideAnticheatSetting(E.Owner)) && killInfo.Length >= 18 && !ShouldIgnoreEvent(E.Origin, E.Target)) + if ((S.CustomCallback || ShouldOverrideAnticheatSetting(S)) && killInfo.Length >= 18 && !ShouldIgnoreEvent(E.Origin, E.Target)) { // this treats "world" damage as self damage if (IsWorldDamage(E.Origin)) @@ -95,7 +98,7 @@ namespace IW4MAdmin.Plugins.Stats S.Logger.WriteInfo($"Start ScriptKill {scriptKillCount}"); #endif - await Manager.AddScriptHit(false, E.Time, E.Origin, E.Target, StatManager.GetIdForServer(E.Owner), S.CurrentMap.Name, killInfo[7], killInfo[8], + await Manager.AddScriptHit(false, E.Time, E.Origin, E.Target, StatManager.GetIdForServer(S), S.CurrentMap.Name, killInfo[7], killInfo[8], killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]); #if DEBUG @@ -105,7 +108,7 @@ namespace IW4MAdmin.Plugins.Stats else { - E.Owner.Logger.WriteDebug("Skipping script kill as it is ignored or data in customcallbacks is outdated/missing"); + S.Logger.WriteDebug("Skipping script kill as it is ignored or data in customcallbacks is outdated/missing"); } break; case GameEvent.EventType.Kill: @@ -129,12 +132,12 @@ namespace IW4MAdmin.Plugins.Stats E.Origin = E.Target; } - Manager.AddDamageEvent(E.Data, E.Origin.ClientId, E.Target.ClientId, StatManager.GetIdForServer(E.Owner)); + Manager.AddDamageEvent(E.Data, E.Origin.ClientId, E.Target.ClientId, StatManager.GetIdForServer(S)); } break; case GameEvent.EventType.ScriptDamage: killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0]; - if ((E.Owner.CustomCallback || ShouldOverrideAnticheatSetting(E.Owner)) && killInfo.Length >= 18 && !ShouldIgnoreEvent(E.Origin, E.Target)) + if ((S.CustomCallback || ShouldOverrideAnticheatSetting(S)) && killInfo.Length >= 18 && !ShouldIgnoreEvent(E.Origin, E.Target)) { // this treats "world" damage as self damage if (IsWorldDamage(E.Origin)) @@ -147,7 +150,7 @@ namespace IW4MAdmin.Plugins.Stats S.Logger.WriteInfo($"Start ScriptDamage {scriptDamageCount}"); #endif - await Manager.AddScriptHit(true, E.Time, E.Origin, E.Target, StatManager.GetIdForServer(E.Owner), S.CurrentMap.Name, killInfo[7], killInfo[8], + await Manager.AddScriptHit(true, E.Time, E.Origin, E.Target, StatManager.GetIdForServer(S), S.CurrentMap.Name, killInfo[7], killInfo[8], killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]); #if DEBUG @@ -157,7 +160,7 @@ namespace IW4MAdmin.Plugins.Stats else { - E.Owner.Logger.WriteDebug("Skipping script damage as it is ignored or data in customcallbacks is outdated/missing"); + S.Logger.WriteDebug("Skipping script damage as it is ignored or data in customcallbacks is outdated/missing"); } break; } @@ -166,7 +169,6 @@ namespace IW4MAdmin.Plugins.Stats public async Task OnLoadAsync(IManager manager) { // load custom configuration - Config = new BaseConfigurationHandler("StatsPluginSettings"); if (Config.Configuration() == null) { Config.Set((StatsConfiguration)new StatsConfiguration().Generate()); @@ -518,7 +520,7 @@ namespace IW4MAdmin.Plugins.Stats /// private bool ShouldIgnoreEvent(EFClient origin, EFClient target) { - return ((origin?.NetworkId == 1 && target?.NetworkId == 1) || (origin?.ClientId <= 1 && target?.ClientId <= 1)); + return ((origin?.NetworkId == 1 && target?.NetworkId == 1)); } /// diff --git a/Plugins/Stats/Stats.csproj b/Plugins/Stats/Stats.csproj index 945e92591..4b4db7393 100644 --- a/Plugins/Stats/Stats.csproj +++ b/Plugins/Stats/Stats.csproj @@ -16,7 +16,7 @@ - + diff --git a/Plugins/Tests/ManagerFixture.cs b/Plugins/Tests/ManagerFixture.cs index fbe1cb82c..90e37ea5c 100644 --- a/Plugins/Tests/ManagerFixture.cs +++ b/Plugins/Tests/ManagerFixture.cs @@ -1,4 +1,5 @@ using IW4MAdmin.Application; +using IW4MAdmin.Application.Misc; using SharedLibraryCore.Configuration; using SharedLibraryCore.Interfaces; using System; diff --git a/Plugins/Tests/TestRconParser.cs b/Plugins/Tests/TestRconParser.cs index e8becb30b..d90eb324c 100644 --- a/Plugins/Tests/TestRconParser.cs +++ b/Plugins/Tests/TestRconParser.cs @@ -1,4 +1,5 @@ using SharedLibraryCore.Database.Models; +using SharedLibraryCore.Interfaces; using SharedLibraryCore.RCon; using System; using System.Collections.Generic; @@ -14,7 +15,7 @@ namespace Tests public override string Version => "test"; - public override async Task<(List, string)> GetStatusAsync(Connection connection) + public override async Task<(List, string)> GetStatusAsync(IRConConnection connection) { var clientList = new List(); diff --git a/Plugins/Web/StatsWeb/StatsWeb.csproj b/Plugins/Web/StatsWeb/StatsWeb.csproj index 3f1b2cf33..1a2e1a4a9 100644 --- a/Plugins/Web/StatsWeb/StatsWeb.csproj +++ b/Plugins/Web/StatsWeb/StatsWeb.csproj @@ -14,7 +14,7 @@ Always - + diff --git a/Plugins/Welcome/Plugin.cs b/Plugins/Welcome/Plugin.cs index 94c157636..b0f57764f 100644 --- a/Plugins/Welcome/Plugin.cs +++ b/Plugins/Welcome/Plugin.cs @@ -64,16 +64,19 @@ namespace IW4MAdmin.Plugins.Welcome public string Name => "Welcome Plugin"; - private BaseConfigurationHandler Config; + private readonly IConfigurationHandler _configHandler; + + public Plugin(IConfigurationHandlerFactory configurationHandlerFactory) + { + _configHandler = configurationHandlerFactory.GetConfigurationHandler("WelcomePluginSettings"); + } public async Task OnLoadAsync(IManager manager) { - // load custom configuration - Config = new BaseConfigurationHandler("WelcomePluginSettings"); - if (Config.Configuration() == null) + if (_configHandler.Configuration() == null) { - Config.Set((WelcomeConfiguration)new WelcomeConfiguration().Generate()); - await Config.Save(); + _configHandler.Set((WelcomeConfiguration)new WelcomeConfiguration().Generate()); + await _configHandler.Save(); } } @@ -87,9 +90,9 @@ namespace IW4MAdmin.Plugins.Welcome { EFClient newPlayer = E.Origin; if (newPlayer.Level >= Permission.Trusted && !E.Origin.Masked) - E.Owner.Broadcast(await ProcessAnnouncement(Config.Configuration().PrivilegedAnnouncementMessage, newPlayer)); + E.Owner.Broadcast(await ProcessAnnouncement(_configHandler.Configuration().PrivilegedAnnouncementMessage, newPlayer)); - newPlayer.Tell(await ProcessAnnouncement(Config.Configuration().UserWelcomeMessage, newPlayer)); + newPlayer.Tell(await ProcessAnnouncement(_configHandler.Configuration().UserWelcomeMessage, newPlayer)); if (newPlayer.Level == Permission.Flagged) { @@ -107,7 +110,7 @@ namespace IW4MAdmin.Plugins.Welcome E.Owner.ToAdmins($"^1NOTICE: ^7Flagged player ^5{newPlayer.Name} ^7({penaltyReason}) has joined!"); } else - E.Owner.Broadcast(await ProcessAnnouncement(Config.Configuration().UserAnnouncementMessage, newPlayer)); + E.Owner.Broadcast(await ProcessAnnouncement(_configHandler.Configuration().UserAnnouncementMessage, newPlayer)); } } diff --git a/Plugins/Welcome/Welcome.csproj b/Plugins/Welcome/Welcome.csproj index 3d8c9545f..4da0714c9 100644 --- a/Plugins/Welcome/Welcome.csproj +++ b/Plugins/Welcome/Welcome.csproj @@ -16,7 +16,7 @@ - + diff --git a/SharedLibraryCore/Commands/NativeCommands.cs b/SharedLibraryCore/Commands/NativeCommands.cs index 09da50e68..e1c068795 100644 --- a/SharedLibraryCore/Commands/NativeCommands.cs +++ b/SharedLibraryCore/Commands/NativeCommands.cs @@ -500,18 +500,18 @@ namespace SharedLibraryCore.Commands public override Task ExecuteAsync(GameEvent E) { - String cmd = E.Data.Trim(); + string cmd = E.Data.Trim(); if (cmd.Length > 2) { bool found = false; - foreach (Command C in E.Owner.Manager.GetCommands()) + foreach (var command in E.Owner.Manager.GetCommands()) { - if (C.Name == cmd.ToLower() || - C.Alias == cmd.ToLower()) + if (command.Name == cmd.ToLower() || + command.Alias == cmd.ToLower()) { - E.Origin.Tell($"[^3{C.Name}^7] {C.Description}"); - E.Origin.Tell(C.Syntax); + E.Origin.Tell($"[^3{command.Name}^7] {command.Description}"); + E.Origin.Tell(command.Syntax); found = true; } } diff --git a/SharedLibraryCore/Configuration/ServerConfiguration.cs b/SharedLibraryCore/Configuration/ServerConfiguration.cs index 1e09bb3f3..18dc73d4b 100644 --- a/SharedLibraryCore/Configuration/ServerConfiguration.cs +++ b/SharedLibraryCore/Configuration/ServerConfiguration.cs @@ -56,11 +56,11 @@ namespace SharedLibraryCore.Configuration { var loc = Utilities.CurrentLocalization.LocalizationIndex; var parserVersions = rconParsers.Select(_parser => _parser.Name).ToArray(); - var selection = Utilities.PromptSelection($"{loc["SETUP_SERVER_RCON_PARSER_VERSION"]} ({IPAddress}:{Port})", $"{loc["SETUP_PROMPT_DEFAULT"]} (Call of Duty)", null, parserVersions); + var selection = Utilities.PromptSelection($"{loc["SETUP_SERVER_RCON_PARSER_VERSION"]} ({IPAddress}:{Port})", parserVersions[0], null, parserVersions); if (selection.Item1 >= 0) { - RConParserVersion = rconParsers.First(_parser => _parser.Name == selection.Item2).Version; + RConParserVersion = rconParsers.FirstOrDefault(_parser => _parser.Name == selection.Item2)?.Version; if (selection.Item1 > 0 && !rconParsers[selection.Item1 - 1].CanGenerateLogPath) { @@ -70,11 +70,11 @@ namespace SharedLibraryCore.Configuration } parserVersions = eventParsers.Select(_parser => _parser.Name).ToArray(); - selection = Utilities.PromptSelection($"{loc["SETUP_SERVER_EVENT_PARSER_VERSION"]} ({IPAddress}:{Port})", $"{loc["SETUP_PROMPT_DEFAULT"]} (Call of Duty)", null, parserVersions); + selection = Utilities.PromptSelection($"{loc["SETUP_SERVER_EVENT_PARSER_VERSION"]} ({IPAddress}:{Port})", parserVersions[0], null, parserVersions); if (selection.Item1 >= 0) { - EventParserVersion = eventParsers.First(_parser => _parser.Name == selection.Item2).Version; + EventParserVersion = eventParsers.FirstOrDefault(_parser => _parser.Name == selection.Item2)?.Version; } } diff --git a/SharedLibraryCore/Configuration/Validation/ServerConfigurationValidator.cs b/SharedLibraryCore/Configuration/Validation/ServerConfigurationValidator.cs index a5877013f..eb7397e3b 100644 --- a/SharedLibraryCore/Configuration/Validation/ServerConfigurationValidator.cs +++ b/SharedLibraryCore/Configuration/Validation/ServerConfigurationValidator.cs @@ -26,12 +26,6 @@ namespace SharedLibraryCore.Configuration.Validation RuleForEach(_server => _server.AutoMessages) .NotEmpty(); - RuleFor(_server => _server.RConParserVersion) - .NotEmpty(); - - RuleFor(_server => _server.EventParserVersion) - .NotEmpty(); - RuleFor(_server => _server.ReservedSlotNumber) .InclusiveBetween(0, 32); } diff --git a/SharedLibraryCore/Interfaces/IBasePathProvider.cs b/SharedLibraryCore/Interfaces/IBasePathProvider.cs new file mode 100644 index 000000000..63ab43fe4 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IBasePathProvider.cs @@ -0,0 +1,14 @@ +namespace SharedLibraryCore.Interfaces +{ + /// + /// defines the capabilities for providing a base path + /// unused as of now, will be used later during refactorying + /// + public interface IBasePathProvider + { + /// + /// working directory of IW4MAdmin + /// + string BasePath { get; } + } +} diff --git a/SharedLibraryCore/Interfaces/IConfigurationHandlerFactory.cs b/SharedLibraryCore/Interfaces/IConfigurationHandlerFactory.cs new file mode 100644 index 000000000..3ab07de87 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IConfigurationHandlerFactory.cs @@ -0,0 +1,17 @@ +namespace SharedLibraryCore.Interfaces +{ + /// + /// defines the capabilities of the configuration handler factory + /// used to generate new instance of configuration handlers + /// + public interface IConfigurationHandlerFactory + { + /// + /// generates a new configuration handler + /// + /// base configuration type + /// file name of configuration + /// new configuration handler instance + IConfigurationHandler GetConfigurationHandler(string name) where T : IBaseConfiguration; + } +} diff --git a/SharedLibraryCore/Interfaces/IGameServerInstanceFactory.cs b/SharedLibraryCore/Interfaces/IGameServerInstanceFactory.cs new file mode 100644 index 000000000..856fd9f12 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IGameServerInstanceFactory.cs @@ -0,0 +1,18 @@ +using SharedLibraryCore.Configuration; + +namespace SharedLibraryCore.Interfaces +{ + /// + /// defines the capabilities of game server instance factory + /// + public interface IGameServerInstanceFactory + { + /// + /// creates the instance of a game server + /// + /// server configuration + /// application manager + /// + Server CreateServer(ServerConfiguration config, IManager manager); + } +} diff --git a/SharedLibraryCore/Interfaces/IManager.cs b/SharedLibraryCore/Interfaces/IManager.cs index 135dfe7ce..f058a3985 100644 --- a/SharedLibraryCore/Interfaces/IManager.cs +++ b/SharedLibraryCore/Interfaces/IManager.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using SharedLibraryCore.Services; using SharedLibraryCore.Configuration; -using System.Reflection; using SharedLibraryCore.Database.Models; using System.Threading; using System.Collections; @@ -29,7 +28,10 @@ namespace SharedLibraryCore.Interfaces /// /// EventHandler for the manager IEventHandler GetEventHandler(); - IList GetPluginAssemblies(); + /// + /// enumerates the registered plugin instances + /// + IEnumerable Plugins { get; } /// /// provides a page list to add and remove from /// diff --git a/SharedLibraryCore/Interfaces/IPluginImporter.cs b/SharedLibraryCore/Interfaces/IPluginImporter.cs index 7403ce142..32173efae 100644 --- a/SharedLibraryCore/Interfaces/IPluginImporter.cs +++ b/SharedLibraryCore/Interfaces/IPluginImporter.cs @@ -1,32 +1,23 @@ using System; using System.Collections.Generic; -using System.Reflection; namespace SharedLibraryCore.Interfaces { /// - /// Defines the capabilities of the plugin importer + /// defines the capabilities of the plugin importer /// public interface IPluginImporter - { + { /// - /// Command types that are defined in plugin assemblies + /// discovers C# assembly plugin and command types /// - IList CommandTypes { get; } + /// tuple of IPlugin implementation type definitions, and IManagerCommand type definitions + (IEnumerable, IEnumerable) DiscoverAssemblyPluginImplementations(); /// - /// The loaded plugins from plugin assemblies + /// discovers the script plugins /// - IList ActivePlugins { get; } - - /// - /// Assemblies that contain plugins - /// - IList PluginAssemblies { get; } - - /// - /// All assemblies in the plugin folder - /// - IList Assemblies { get; } + /// initialized script plugin collection + IEnumerable DiscoverScriptPlugins(); } } diff --git a/SharedLibraryCore/Interfaces/IRConConnection.cs b/SharedLibraryCore/Interfaces/IRConConnection.cs new file mode 100644 index 000000000..ec716c3d4 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IRConConnection.cs @@ -0,0 +1,25 @@ +using SharedLibraryCore.RCon; +using System.Threading.Tasks; + +namespace SharedLibraryCore.Interfaces +{ + /// + /// defines the capabilities of an RCon connection + /// + public interface IRConConnection + { + /// + /// sends a query with the instance of the rcon connection + /// + /// type of RCon query to perform + /// optional parameter list + /// + Task SendQueryAsync(StaticHelpers.QueryType type, string parameters = ""); + + /// + /// sets the rcon parser configuration + /// + /// parser config + void SetConfiguration(IRConParserConfiguration config); + } +} diff --git a/SharedLibraryCore/Interfaces/IRConConnectionFactory.cs b/SharedLibraryCore/Interfaces/IRConConnectionFactory.cs new file mode 100644 index 000000000..ae06457bf --- /dev/null +++ b/SharedLibraryCore/Interfaces/IRConConnectionFactory.cs @@ -0,0 +1,17 @@ +namespace SharedLibraryCore.Interfaces +{ + /// + /// defines the capabilities of an RCon connection factory + /// + public interface IRConConnectionFactory + { + /// + /// creates an rcon connection instance + /// + /// ip address of the server + /// port of the server + /// password of the server + /// instance of rcon connection + IRConConnection CreateConnection(string ipAddress, int port, string password); + } +} diff --git a/SharedLibraryCore/Interfaces/IRConParser.cs b/SharedLibraryCore/Interfaces/IRConParser.cs index 916dca84a..62227a67b 100644 --- a/SharedLibraryCore/Interfaces/IRConParser.cs +++ b/SharedLibraryCore/Interfaces/IRConParser.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using SharedLibraryCore.Database.Models; -using SharedLibraryCore.RCon; using static SharedLibraryCore.Server; namespace SharedLibraryCore.Interfaces @@ -15,7 +14,7 @@ namespace SharedLibraryCore.Interfaces /// RCon connection to retrieve with /// name of DVAR /// - Task> GetDvarAsync(Connection connection, string dvarName); + Task> GetDvarAsync(IRConConnection connection, string dvarName); /// /// set value of DVAR by name @@ -24,7 +23,7 @@ namespace SharedLibraryCore.Interfaces /// name of DVAR to set /// value to set DVAR to /// - Task SetDvarAsync(Connection connection, string dvarName, object dvarValue); + Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue); /// /// executes a console command on the server @@ -32,14 +31,14 @@ namespace SharedLibraryCore.Interfaces /// RCon connection to use /// console command to execute /// - Task ExecuteCommandAsync(Connection connection, string command); + Task ExecuteCommandAsync(IRConConnection connection, string command); /// /// get the list of connected clients from status response /// /// RCon connection to use /// list of clients, and current map - Task<(List, string)> GetStatusAsync(Connection connection); + Task<(List, string)> GetStatusAsync(IRConConnection connection); /// /// stores the RCon configuration diff --git a/SharedLibraryCore/RCon/CommandPrefix.cs b/SharedLibraryCore/RCon/CommandPrefix.cs index 0940b4fd4..2d2a1734a 100644 --- a/SharedLibraryCore/RCon/CommandPrefix.cs +++ b/SharedLibraryCore/RCon/CommandPrefix.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace SharedLibraryCore.RCon +namespace SharedLibraryCore.RCon { public class CommandPrefix { diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs index 5cea93f79..0807eb191 100644 --- a/SharedLibraryCore/Server.cs +++ b/SharedLibraryCore/Server.cs @@ -28,7 +28,7 @@ namespace SharedLibraryCore T7 = 8 } - public Server(IManager mgr, ServerConfiguration config) + public Server(IManager mgr, IRConConnectionFactory rconConnectionFactory, ServerConfiguration config) { Password = config.Password; IP = config.IPAddress; @@ -37,8 +37,7 @@ namespace SharedLibraryCore Logger = Manager.GetLogger(this.EndPoint); Logger.WriteInfo(this.ToString()); ServerConfig = config; - RemoteConnection = new RCon.Connection(IP, Port, Password, Logger, null); - + RemoteConnection = rconConnectionFactory.CreateConnection(IP, Port, Password); EventProcessing = new SemaphoreSlim(1, 1); Clients = new List(new EFClient[18]); Reports = new List(); @@ -283,7 +282,7 @@ namespace SharedLibraryCore public IManager Manager { get; protected set; } public ILogger Logger { get; private set; } public ServerConfiguration ServerConfig { get; private set; } - public List Maps { get; protected set; } + public List Maps { get; protected set; } = new List(); public List Reports { get; set; } public List ChatHistory { get; protected set; } public Queue ClientHistory { get; private set; } @@ -307,7 +306,7 @@ namespace SharedLibraryCore public bool Throttled { get; protected set; } public bool CustomCallback { get; protected set; } public string WorkingDirectory { get; protected set; } - public RCon.Connection RemoteConnection { get; protected set; } + public IRConConnection RemoteConnection { get; protected set; } public IRConParser RconParser { get; protected set; } public IEventParser EventParser { get; set; } public string LogPath { get; protected set; } diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index bd1c3e42c..cc9c5e082 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -6,7 +6,7 @@ RaidMax.IW4MAdmin.SharedLibraryCore - 2.2.6 + 2.2.7 RaidMax Forever None Debug;Release;Prerelease @@ -20,8 +20,8 @@ true MIT Shared Library for IW4MAdmin - 2.2.6.0 - 2.2.6.0 + 2.2.7.0 + 2.2.7.0 @@ -68,6 +68,10 @@ + + + + diff --git a/Tests/ApplicationTests/ApplicationTests.csproj b/Tests/ApplicationTests/ApplicationTests.csproj new file mode 100644 index 000000000..121244179 --- /dev/null +++ b/Tests/ApplicationTests/ApplicationTests.csproj @@ -0,0 +1,34 @@ + + + + netcoreapp3.1 + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/Tests/ApplicationTests/ServerTests.cs b/Tests/ApplicationTests/ServerTests.cs new file mode 100644 index 000000000..452d347a9 --- /dev/null +++ b/Tests/ApplicationTests/ServerTests.cs @@ -0,0 +1,85 @@ +using FakeItEasy; +using IW4MAdmin; +using IW4MAdmin.Application; +using IW4MAdmin.Application.EventParsers; +using NUnit.Framework; +using SharedLibraryCore.Interfaces; +using System; +using System.Diagnostics; + +namespace ApplicationTests +{ + [TestFixture] + public class ServerTests + { + ILogger logger; + + [SetUp] + public void Setup() + { + logger = A.Fake(); + + void testLog(string msg) => Console.WriteLine(msg); + + A.CallTo(() => logger.WriteError(A.Ignored)).Invokes((string msg) => testLog(msg)); + A.CallTo(() => logger.WriteWarning(A.Ignored)).Invokes((string msg) => testLog(msg)); + A.CallTo(() => logger.WriteInfo(A.Ignored)).Invokes((string msg) => testLog(msg)); + A.CallTo(() => logger.WriteDebug(A.Ignored)).Invokes((string msg) => testLog(msg)); + } + + [Test] + public void GameTimeFalseQuitTest() + { + var mgr = A.Fake(); + var server = new IW4MServer(mgr, + new SharedLibraryCore.Configuration.ServerConfiguration() { IPAddress = "127.0.0.1", Port = 28960 }, + A.Fake(), A.Fake()); + + var parser = new BaseEventParser(); + parser.Configuration.GuidNumberStyle = System.Globalization.NumberStyles.Integer; + + var log = System.IO.File.ReadAllLines("Files\\T6MapRotation.log"); + foreach (string line in log) + { + var e = parser.GenerateGameEvent(line); + if (e.Origin != null) + { + e.Origin.CurrentServer = server; + } + + server.ExecuteEvent(e).Wait(); + } + } + + [Test] + public void LogFileReplay() + { + var mgr = A.Fake(); + A.CallTo(() => mgr.GetLogger(A.Ignored)).Returns(logger); + + var server = new IW4MServer(mgr, + new SharedLibraryCore.Configuration.ServerConfiguration() { IPAddress = "127.0.0.1", Port = 28960 }, + A.Fake(), A.Fake()); + + var parser = new BaseEventParser(); + parser.Configuration.GuidNumberStyle = System.Globalization.NumberStyles.Integer; + + var log = System.IO.File.ReadAllLines("Files\\T6Game.log"); + long lastEventId = 0; + foreach (string line in log) + { + var e = parser.GenerateGameEvent(line); + server.Logger.WriteInfo($"{e.GameTime}"); + if (e.Origin != null) + { + e.Origin.CurrentServer = server; + } + + server.ExecuteEvent(e).Wait(); + lastEventId = e.Id; + } + + Assert.GreaterOrEqual(lastEventId, log.Length); + } + } +} \ No newline at end of file diff --git a/Tests/ApplicationTests/StatsTests.cs b/Tests/ApplicationTests/StatsTests.cs new file mode 100644 index 000000000..f0e0bf46c --- /dev/null +++ b/Tests/ApplicationTests/StatsTests.cs @@ -0,0 +1,117 @@ +using NUnit.Framework; +using System; +using SharedLibraryCore.Interfaces; +using IW4MAdmin; +using FakeItEasy; +using IW4MAdmin.Application.EventParsers; +using System.Linq; +using IW4MAdmin.Plugins.Stats.Models; +using IW4MAdmin.Application.Helpers; +using IW4MAdmin.Plugins.Stats.Config; +using System.Collections.Generic; +using SharedLibraryCore.Database.Models; + +namespace ApplicationTests +{ + [TestFixture] + public class StatsTests + { + ILogger logger; + + [SetUp] + public void Setup() + { + logger = A.Fake(); + + void testLog(string msg) => Console.WriteLine(msg); + + A.CallTo(() => logger.WriteError(A.Ignored)).Invokes((string msg) => testLog(msg)); + A.CallTo(() => logger.WriteWarning(A.Ignored)).Invokes((string msg) => testLog(msg)); + A.CallTo(() => logger.WriteInfo(A.Ignored)).Invokes((string msg) => testLog(msg)); + A.CallTo(() => logger.WriteDebug(A.Ignored)).Invokes((string msg) => testLog(msg)); + } + + [Test] + public void TestKDR() + { + var mgr = A.Fake(); + var handlerFactory = A.Fake(); + var config = A.Fake>(); + var plugin = new IW4MAdmin.Plugins.Stats.Plugin(handlerFactory); + + A.CallTo(() => config.Configuration()) + .Returns(new StatsConfiguration() + { + EnableAntiCheat = true + }); + + A.CallTo(() => handlerFactory.GetConfigurationHandler(A.Ignored)) + .Returns(config); + + A.CallTo(() => mgr.GetLogger(A.Ignored)) + .Returns(logger); + + var server = new IW4MServer(mgr, + new SharedLibraryCore.Configuration.ServerConfiguration() { IPAddress = "127.0.0.1", Port = 28960 }, + A.Fake(), + A.Fake()); + + var parser = new BaseEventParser(); + parser.Configuration.GuidNumberStyle = System.Globalization.NumberStyles.Integer; + + var log = System.IO.File.ReadAllLines("Files\\T6GameStats.log"); + plugin.OnLoadAsync(mgr).Wait(); + plugin.OnEventAsync(new SharedLibraryCore.GameEvent() { Type = SharedLibraryCore.GameEvent.EventType.Start, Owner = server }, server).Wait(); + + var clientList = new Dictionary(); + + foreach (string line in log) + { + var e = parser.GenerateGameEvent(line); + if (e.Origin != null) + { + //if (!clientList.ContainsKey(e.Origin.NetworkId)) + //{ + // clientList.Add(e.Origin.NetworkId, e.Origin); + //} + + //else + //{ + // e.Origin = clientList[e.Origin.NetworkId]; + //} + + e.Origin = server.GetClientsAsList().FirstOrDefault(_client => _client.NetworkId == e.Origin.NetworkId) ?? e.Origin; + e.Origin.CurrentServer = server; + } + + if (e.Target != null) + { + //if (!clientList.ContainsKey(e.Target.NetworkId)) + //{ + // clientList.Add(e.Target.NetworkId, e.Target); + //} + + //else + //{ + // e.Target = clientList[e.Target.NetworkId]; + //} + + e.Target = server.GetClientsAsList().FirstOrDefault(_client => _client.NetworkId == e.Target.NetworkId) ?? e.Target; + e.Target.CurrentServer = server; + } + + server.ExecuteEvent(e).Wait(); + plugin.OnEventAsync(e, server).Wait(); + } + + var client = server.GetClientsAsList().First(_client => _client?.NetworkId == 2028755667); + var stats = client.GetAdditionalProperty("ClientStats"); + } + + class BasePathProvider : IBasePathProvider + { + public string BasePath => @"X:\IW4MAdmin\BUILD\Plugins"; + } + + } +} diff --git a/WebfrontCore/Controllers/HomeController.cs b/WebfrontCore/Controllers/HomeController.cs index 70fd16694..af7f3e90c 100644 --- a/WebfrontCore/Controllers/HomeController.cs +++ b/WebfrontCore/Controllers/HomeController.cs @@ -11,11 +11,8 @@ namespace WebfrontCore.Controllers { public class HomeController : BaseController { - private readonly IPluginImporter _pluginImporter; - - public HomeController(IManager manager, IPluginImporter importer) : base(manager) + public HomeController(IManager manager) : base(manager) { - _pluginImporter = importer; } public async Task Index() @@ -68,7 +65,7 @@ namespace WebfrontCore.Controllers var pluginType = _cmd.GetType().Assembly.GetTypes().FirstOrDefault(_type => _type.Assembly != excludedAssembly && typeof(IPlugin).IsAssignableFrom(_type)); return pluginType == null ? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_HELP_COMMAND_NATIVE"] : - _pluginImporter.ActivePlugins.First(_plugin => _plugin.GetType() == pluginType).Name; // for now we're just returning the name of the plugin, maybe later we'll include more info + Manager.Plugins.First(_plugin => _plugin.GetType() == pluginType).Name; // for now we're just returning the name of the plugin, maybe later we'll include more info }) .Select(_grp => (_grp.Key, _grp.AsEnumerable())); diff --git a/WebfrontCore/Startup.cs b/WebfrontCore/Startup.cs index eb1d3401e..66edbc0f8 100644 --- a/WebfrontCore/Startup.cs +++ b/WebfrontCore/Startup.cs @@ -7,9 +7,14 @@ using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using SharedLibraryCore; using SharedLibraryCore.Database; using SharedLibraryCore.Interfaces; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Net; +using System.Reflection; using System.Threading.Tasks; using WebfrontCore.Middleware; @@ -32,20 +37,33 @@ namespace WebfrontCore }); }); + IEnumerable pluginAssemblies() + { + string pluginDir = $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}"; + + if (Directory.Exists(pluginDir)) + { + var dllFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.dll"); + return dllFileNames.Select(_file => Assembly.LoadFrom(_file)); + } + + return Enumerable.Empty(); + } + // Add framework services. var mvcBuilder = services.AddMvc(_options => _options.SuppressAsyncSuffixInActionNames = false) - .ConfigureApplicationPartManager(_ => + .ConfigureApplicationPartManager(_partManager => { - foreach (var assembly in Program.Manager.GetPluginAssemblies()) + foreach (var assembly in pluginAssemblies()) { if (assembly.FullName.Contains("Views")) { - _.ApplicationParts.Add(new CompiledRazorAssemblyPart(assembly)); + _partManager.ApplicationParts.Add(new CompiledRazorAssemblyPart(assembly)); } else if (assembly.FullName.Contains("Web")) { - _.ApplicationParts.Add(new AssemblyPart(assembly)); + _partManager.ApplicationParts.Add(new AssemblyPart(assembly)); } } }); @@ -58,7 +76,7 @@ namespace WebfrontCore }); #endif - foreach (var asm in Program.Manager.GetPluginAssemblies()) + foreach (var asm in pluginAssemblies()) { mvcBuilder.AddApplicationPart(asm); } @@ -83,7 +101,7 @@ namespace WebfrontCore #endif services.AddSingleton(Program.Manager); - services.AddSingleton(Program.ApplicationServiceProvider.GetService(typeof(IPluginImporter)) as IPluginImporter); + services.AddSingleton(Program.ApplicationServiceProvider.GetService()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.