diff --git a/.gitignore b/.gitignore
index f5c4f09f..f385e3da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -244,3 +244,7 @@ launchSettings.json
/Tests/ApplicationTests/Files/GameEvents.json
/Tests/ApplicationTests/Files/replay.json
/GameLogServer/game_log_server_env
+.idea/*
+*.db
+/Data/IW4MAdmin_Migration.db-shm
+/Data/IW4MAdmin_Migration.db-wal
diff --git a/Application/API/Master/IMasterApi.cs b/Application/API/Master/IMasterApi.cs
index f6a46bf2..2b99422a 100644
--- a/Application/API/Master/IMasterApi.cs
+++ b/Application/API/Master/IMasterApi.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
-using IW4MAdmin.Application.Helpers;
+using IW4MAdmin.Application.Misc;
using Newtonsoft.Json;
using RestEase;
using SharedLibraryCore.Helpers;
diff --git a/Application/Application.csproj b/Application/Application.csproj
index b0ef4959..234f718b 100644
--- a/Application/Application.csproj
+++ b/Application/Application.csproj
@@ -5,7 +5,7 @@
netcoreapp3.1
false
RaidMax.IW4MAdmin.Application
- 2.3.2.0
+ 2020.0.0.0
RaidMax
Forever None
IW4MAdmin
@@ -25,13 +25,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
@@ -39,7 +39,6 @@
true
true
Latest
-
@@ -49,6 +48,8 @@
+
+
true
@@ -59,6 +60,9 @@
Always
+
+ Always
+
diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs
index 135e0256..596f3ef0 100644
--- a/Application/ApplicationManager.cs
+++ b/Application/ApplicationManager.cs
@@ -1,19 +1,15 @@
-using IW4MAdmin.Application.API.Master;
-using IW4MAdmin.Application.EventParsers;
+using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Misc;
-using IW4MAdmin.Application.RconParsers;
+using IW4MAdmin.Application.RConParsers;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Configuration.Validation;
-using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models;
-using SharedLibraryCore.Dtos;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
-using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Services;
using System;
using System.Collections;
@@ -24,7 +20,15 @@ using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Data.Abstractions;
+using Data.Context;
+using IW4MAdmin.Application.Migration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Serilog.Context;
using static SharedLibraryCore.GameEvent;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
namespace IW4MAdmin.Application
{
@@ -32,7 +36,7 @@ namespace IW4MAdmin.Application
{
private readonly ConcurrentBag _servers;
public List Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
- public ILogger Logger => GetLogger(0);
+ [Obsolete] public ObsoleteLogger Logger => _serviceProvider.GetRequiredService();
public bool IsRunning { get; private set; }
public bool IsInitialized { get; private set; }
public DateTime StartTime { get; private set; }
@@ -50,11 +54,9 @@ namespace IW4MAdmin.Application
private readonly ILogger _logger;
private readonly List MessageTokens;
private readonly ClientService ClientSvc;
- readonly AliasService AliasSvc;
readonly PenaltyService PenaltySvc;
public IConfigurationHandler ConfigHandler;
readonly IPageList PageList;
- private readonly Dictionary _loggers = new Dictionary();
private readonly IMetaService _metaService;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource;
@@ -68,30 +70,33 @@ namespace IW4MAdmin.Application
private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IMetaRegistration _metaRegistration;
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ChangeHistoryService _changeHistoryService;
+ private readonly ApplicationConfiguration _appConfig;
+ public ConcurrentDictionary ProcessingEvents { get; } = new ConcurrentDictionary();
- public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable commands,
+ public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable commands,
ITranslationLookup translationLookup, IConfigurationHandler commandConfiguration,
IConfigurationHandler appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable plugins, IParserRegexFactory parserRegexFactory, IEnumerable customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
- IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver)
+ IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
+ ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag();
MessageTokens = new List();
- ClientSvc = new ClientService(contextFactory);
- AliasSvc = new AliasService();
- PenaltySvc = new PenaltyService();
+ ClientSvc = clientService;
+ PenaltySvc = penaltyService;
ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow;
PageList = new PageList();
- AdditionalEventParsers = new List() { new BaseEventParser(parserRegexFactory, logger, appConfigHandler.Configuration()) };
- AdditionalRConParsers = new List() { new BaseRConParser(parserRegexFactory) };
+ AdditionalEventParsers = new List() { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
+ AdditionalRConParsers = new List() { new BaseRConParser(serviceProvider.GetRequiredService>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication();
_logger = logger;
_metaService = metaService;
_tokenSource = new CancellationTokenSource();
- _loggers.Add(0, logger);
_commands = commands.ToList();
_translationLookup = translationLookup;
_commandConfiguration = commandConfiguration;
@@ -102,6 +107,9 @@ namespace IW4MAdmin.Application
_scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver;
+ _serviceProvider = serviceProvider;
+ _changeHistoryService = changeHistoryService;
+ _appConfig = appConfig;
Plugins = plugins;
}
@@ -109,10 +117,8 @@ namespace IW4MAdmin.Application
public async Task ExecuteEvent(GameEvent newEvent)
{
-#if DEBUG == true
- Logger.WriteDebug($"Entering event process for {newEvent.Id}");
-#endif
-
+ ProcessingEvents.TryAdd(newEvent.Id, newEvent);
+
// the event has failed already
if (newEvent.Failed)
{
@@ -124,22 +130,17 @@ namespace IW4MAdmin.Application
await newEvent.Owner.ExecuteEvent(newEvent);
// save the event info to the database
- var changeHistorySvc = new ChangeHistoryService();
- await changeHistorySvc.Add(newEvent);
-
-#if DEBUG
- Logger.WriteDebug($"Processed event with id {newEvent.Id}");
-#endif
+ await _changeHistoryService.Add(newEvent);
}
catch (TaskCanceledException)
{
- Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early");
+ _logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
}
catch (OperationCanceledException)
{
- Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early");
+ _logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
}
// this happens if a plugin requires login
@@ -152,31 +153,58 @@ namespace IW4MAdmin.Application
catch (NetworkException ex)
{
newEvent.FailReason = EventFailReason.Exception;
- Logger.WriteError(ex.Message);
- Logger.WriteDebug(ex.GetExceptionInfo());
+ using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
+ {
+ _logger.LogError(ex, ex.Message);
+ }
}
catch (ServerException ex)
{
newEvent.FailReason = EventFailReason.Exception;
- Logger.WriteWarning(ex.Message);
+ using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
+ {
+ _logger.LogError(ex, ex.Message);
+ }
}
catch (Exception ex)
{
newEvent.FailReason = EventFailReason.Exception;
- Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner));
- Logger.WriteDebug(ex.GetExceptionInfo());
+ Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner));
+ using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
+ {
+ _logger.LogError(ex, "Unexpected exception");
+ }
+ }
+
+ skip:
+ if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null)
+ {
+ var correlatedEvents =
+ ProcessingEvents.Values.Where(ev =>
+ ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id)
+ .ToList();
+
+ await Task.WhenAll(correlatedEvents.Select(ev =>
+ ev.WaitAsync(Utilities.DefaultCommandTimeout, CancellationToken)));
+ newEvent.Output.AddRange(correlatedEvents.SelectMany(ev => ev.Output));
+
+ foreach (var correlatedEvent in correlatedEvents)
+ {
+ ProcessingEvents.Remove(correlatedEvent.Id, out _);
+ }
+ }
+
+ // we don't want to remove events that are correlated to command
+ if (ProcessingEvents.Values.ToList()?.Count(gameEvent => gameEvent.CorrelationId == newEvent.CorrelationId) == 1)
+ {
+ ProcessingEvents.Remove(newEvent.Id, out _);
}
- skip:
// tell anyone waiting for the output that we're done
newEvent.Complete();
OnGameEventExecuted?.Invoke(this, newEvent);
-
-#if DEBUG == true
- Logger.WriteDebug($"Exiting event process for {newEvent.Id}");
-#endif
}
public IList GetServers()
@@ -192,15 +220,15 @@ namespace IW4MAdmin.Application
public async Task UpdateServerStates()
{
// store the server hash code and task for it
- var runningUpdateTasks = new Dictionary();
+ var runningUpdateTasks = new Dictionary();
while (!_tokenSource.IsCancellationRequested)
{
// select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks
- .Where(ut => ut.Value.Status == TaskStatus.RanToCompletion ||
- ut.Value.Status == TaskStatus.Canceled ||
- ut.Value.Status == TaskStatus.Faulted)
+ .Where(ut => ut.Value.task.Status == TaskStatus.RanToCompletion ||
+ ut.Value.task.Status == TaskStatus.Canceled || // we want to cancel if a task takes longer than 5 minutes
+ ut.Value.task.Status == TaskStatus.Faulted || DateTime.Now - ut.Value.startTime > TimeSpan.FromMinutes(5))
.Select(ut => ut.Key)
.ToList();
@@ -211,9 +239,14 @@ namespace IW4MAdmin.Application
IsInitialized = true;
}
- // remove the update tasks as they have completd
- foreach (long serverId in serverTasksToRemove)
+ // remove the update tasks as they have completed
+ foreach (var serverId in serverTasksToRemove.Where(serverId => runningUpdateTasks.ContainsKey(serverId)))
{
+ if (!runningUpdateTasks[serverId].tokenSource.Token.IsCancellationRequested)
+ {
+ runningUpdateTasks[serverId].tokenSource.Cancel();
+ }
+
runningUpdateTasks.Remove(serverId);
}
@@ -221,36 +254,33 @@ namespace IW4MAdmin.Application
var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
{
- runningUpdateTasks.Add(server.EndPoint, Task.Run(async () =>
+ var tokenSource = new CancellationTokenSource();
+ runningUpdateTasks.Add(server.EndPoint, (Task.Run(async () =>
{
try
{
- await server.ProcessUpdatesAsync(_tokenSource.Token);
-
- if (server.Throttled)
+ if (runningUpdateTasks.ContainsKey(server.EndPoint))
{
- await Task.Delay((int)_throttleTimeout.TotalMilliseconds, _tokenSource.Token);
+ await server.ProcessUpdatesAsync(_tokenSource.Token)
+ .WithWaitCancellation(runningUpdateTasks[server.EndPoint].tokenSource.Token);
}
}
catch (Exception e)
{
- Logger.WriteWarning($"Failed to update status for {server}");
- Logger.WriteDebug(e.GetExceptionInfo());
+ using (LogContext.PushProperty("Server", server.ToString()))
+ {
+ _logger.LogError(e, "Failed to update status");
+ }
}
finally
{
server.IsInitialized = true;
}
- }));
+ }, tokenSource.Token), tokenSource, DateTime.Now));
}
-#if DEBUG
- Logger.WriteDebug($"{runningUpdateTasks.Count} servers queued for stats updates");
- ThreadPool.GetMaxThreads(out int workerThreads, out int n);
- ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
- Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
-#endif
+
try
{
await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
@@ -272,6 +302,15 @@ namespace IW4MAdmin.Application
IsRunning = true;
ExternalIPAddress = await Utilities.GetExternalIP();
+ #region DATABASE
+ _logger.LogInformation("Beginning database migration sync");
+ Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]);
+ await ContextSeed.Seed(_serviceProvider.GetRequiredService(), _tokenSource.Token);
+ await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService(), _tokenSource.Token);
+ _logger.LogInformation("Finished database migration sync");
+ Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
+ #endregion
+
#region PLUGINS
foreach (var plugin in Plugins)
{
@@ -289,8 +328,8 @@ namespace IW4MAdmin.Application
catch (Exception ex)
{
- Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"].FormatExt(scriptPlugin.Name));
- Logger.WriteDebug(ex.Message);
+ Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"].FormatExt(scriptPlugin.Name));
+ _logger.LogError(ex, "Could not properly load plugin {plugin}", scriptPlugin.Name);
}
};
}
@@ -303,32 +342,27 @@ namespace IW4MAdmin.Application
catch (Exception ex)
{
- Logger.WriteError($"{_translationLookup["SERVER_ERROR_PLUGIN"]} {plugin.Name}");
- Logger.WriteDebug(ex.GetExceptionInfo());
+ _logger.LogError(ex, $"{_translationLookup["SERVER_ERROR_PLUGIN"]} {plugin.Name}");
}
}
#endregion
#region CONFIG
- var config = ConfigHandler.Configuration();
-
// copy over default config if it doesn't exist
- if (config == null)
+ if (!_appConfig.Servers?.Any() ?? true)
{
- var defaultConfig = new BaseConfigurationHandler("DefaultSettings").Configuration();
- ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
- var newConfig = ConfigHandler.Configuration();
+ var defaultConfig = new BaseConfigurationHandler("DefaultSettings").Configuration();
+ //ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
+ //var newConfig = ConfigHandler.Configuration();
- newConfig.AutoMessages = defaultConfig.AutoMessages;
- newConfig.GlobalRules = defaultConfig.GlobalRules;
- newConfig.Maps = defaultConfig.Maps;
- newConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
- newConfig.QuickMessages = defaultConfig.QuickMessages;
+ _appConfig.AutoMessages = defaultConfig.AutoMessages;
+ _appConfig.GlobalRules = defaultConfig.GlobalRules;
+ _appConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
- if (newConfig.Servers == null)
+ //if (newConfig.Servers == null)
{
- ConfigHandler.Set(newConfig);
- newConfig.Servers = new ServerConfiguration[1];
+ ConfigHandler.Set(_appConfig);
+ _appConfig.Servers = new ServerConfiguration[1];
do
{
@@ -343,30 +377,41 @@ namespace IW4MAdmin.Application
serverConfig.AddEventParser(parser);
}
- newConfig.Servers = newConfig.Servers.Where(_servers => _servers != null).Append((ServerConfiguration)serverConfig.Generate()).ToArray();
+ _appConfig.Servers = _appConfig.Servers.Where(_servers => _servers != null).Append((ServerConfiguration)serverConfig.Generate()).ToArray();
} while (Utilities.PromptBool(_translationLookup["SETUP_SERVER_SAVE"]));
- config = newConfig;
await ConfigHandler.Save();
}
}
else
{
- if (string.IsNullOrEmpty(config.Id))
+ if (string.IsNullOrEmpty(_appConfig.Id))
{
- config.Id = Guid.NewGuid().ToString();
+ _appConfig.Id = Guid.NewGuid().ToString();
await ConfigHandler.Save();
}
- if (string.IsNullOrEmpty(config.WebfrontBindUrl))
+ if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl))
{
- config.WebfrontBindUrl = "http://0.0.0.0:1624";
+ _appConfig.WebfrontBindUrl = "http://0.0.0.0:1624";
await ConfigHandler.Save();
}
+#pragma warning disable 618
+ if (_appConfig.Maps != null)
+ {
+ _appConfig.Maps = null;
+ }
+
+ if (_appConfig.QuickMessages != null)
+ {
+ _appConfig.QuickMessages = null;
+ }
+#pragma warning restore 618
+
var validator = new ApplicationConfigurationValidator();
- var validationResult = validator.Validate(config);
+ var validationResult = validator.Validate(_appConfig);
if (!validationResult.IsValid)
{
@@ -377,9 +422,9 @@ namespace IW4MAdmin.Application
};
}
- foreach (var serverConfig in config.Servers)
+ foreach (var serverConfig in _appConfig.Servers)
{
- Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
+ ConfigurationMigration.ModifyLogPath020919(serverConfig);
if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null)
{
@@ -399,26 +444,18 @@ namespace IW4MAdmin.Application
}
}
- if (config.Servers.Length == 0)
+ if (_appConfig.Servers.Length == 0)
{
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
}
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
- Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252");
+ Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(_appConfig.CustomParserEncoding) ? _appConfig.CustomParserEncoding : "windows-1252");
#endregion
- #region DATABASE
- using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString,
- GetApplicationSettings().Configuration()?.DatabaseProvider))
- {
- await new ContextSeed(db).Seed();
- }
- #endregion
-
#region COMMANDS
- if (ClientSvc.GetOwners().Result.Count > 0)
+ if (await ClientSvc.HasOwnerAsync(_tokenSource.Token))
{
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
}
@@ -440,8 +477,8 @@ namespace IW4MAdmin.Application
// this is because I want to store the command prefix in IW4MAdminSettings, but can't easily
// inject it to all the places that need it
- cmdConfig.CommandPrefix = config.CommandPrefix;
- cmdConfig.BroadcastCommandPrefix = config.BroadcastCommandPrefix;
+ cmdConfig.CommandPrefix = _appConfig.CommandPrefix;
+ cmdConfig.BroadcastCommandPrefix = _appConfig.BroadcastCommandPrefix;
foreach (var cmd in commandsToAddToConfig)
{
@@ -472,6 +509,7 @@ namespace IW4MAdmin.Application
}
#endregion
+ Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
await InitializeServers();
}
@@ -487,16 +525,20 @@ namespace IW4MAdmin.Application
{
// todo: this might not always be an IW4MServer
var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
- await ServerInstance.Initialize();
+ using (LogContext.PushProperty("Server", ServerInstance.ToString()))
+ {
+ _logger.LogInformation("Beginning server communication initialization");
+ await ServerInstance.Initialize();
- _servers.Add(ServerInstance);
+ _servers.Add(ServerInstance);
+ Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname.StripColors()));
+ _logger.LogInformation("Finishing initialization and now monitoring [{server}]", ServerInstance.Hostname, ServerInstance.ToString());
+ }
- Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname));
// add the start event for this server
-
var e = new GameEvent()
{
- Type = GameEvent.EventType.Start,
+ Type = EventType.Start,
Data = $"{ServerInstance.GameName} started",
Owner = ServerInstance
};
@@ -507,13 +549,11 @@ namespace IW4MAdmin.Application
catch (ServerException e)
{
- Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"].FormatExt($"[{Conf.IPAddress}:{Conf.Port}]"));
-
- if (e.GetType() == typeof(DvarException))
+ Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"].FormatExt($"[{Conf.IPAddress}:{Conf.Port}]"));
+ using (LogContext.PushProperty("Server", $"{Conf.IPAddress}:{Conf.Port}"))
{
- Logger.WriteDebug($"{e.Message} {(e.GetType() == typeof(DvarException) ? $"({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})" : "")}");
+ _logger.LogError(e, "Unexpected exception occurred during initialization");
}
-
lastException = e;
}
}
@@ -548,20 +588,10 @@ namespace IW4MAdmin.Application
Stop();
}
- public ILogger GetLogger(long serverId)
+ [Obsolete]
+ public ObsoleteLogger GetLogger(long serverId)
{
- if (_loggers.ContainsKey(serverId))
- {
- return _loggers[serverId];
- }
-
- else
- {
- var newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
-
- _loggers.Add(serverId, newLogger);
- return newLogger;
- }
+ return _serviceProvider.GetRequiredService();
}
public IList GetMessageTokens()
@@ -580,11 +610,6 @@ namespace IW4MAdmin.Application
return ClientSvc;
}
- public AliasService GetAliasService()
- {
- return AliasSvc;
- }
-
public PenaltyService GetPenaltyService()
{
return PenaltySvc;
@@ -607,7 +632,7 @@ namespace IW4MAdmin.Application
public IRConParser GenerateDynamicRConParser(string name)
{
- return new DynamicRConParser(_parserRegexFactory)
+ return new DynamicRConParser(_serviceProvider.GetRequiredService>(), _parserRegexFactory)
{
Name = name
};
diff --git a/Application/BuildScripts/PostBuild.bat b/Application/BuildScripts/PostBuild.bat
index 6433f9de..c3cbc996 100644
--- a/Application/BuildScripts/PostBuild.bat
+++ b/Application/BuildScripts/PostBuild.bat
@@ -4,24 +4,13 @@ set TargetDir=%3
set OutDir=%4
set Version=%5
-echo %Version% > "%SolutionDir%DEPLOY\version.txt"
-
echo Copying dependency configs
copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%"
-copy "%SolutionDir%SharedLibaryCore\%OutDir%*.deps.json" "%TargetDir%"
+copy "%SolutionDir%SharedLibraryCore\%OutDir%*.deps.json" "%TargetDir%"
if not exist "%TargetDir%Plugins" (
echo "Making plugin dir"
md "%TargetDir%Plugins"
)
-xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
-
-echo Copying plugins for publish
-del %SolutionDir%BUILD\Plugins\Tests.dll
-xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\Windows\Plugins\"
-xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
-
-echo Copying script plugins for publish
-xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\Windows\Plugins\"
-xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
\ No newline at end of file
+xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
\ No newline at end of file
diff --git a/Application/Commands/OfflineMessageCommand.cs b/Application/Commands/OfflineMessageCommand.cs
new file mode 100644
index 00000000..f2ab7284
--- /dev/null
+++ b/Application/Commands/OfflineMessageCommand.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Threading.Tasks;
+using Data.Abstractions;
+using Data.Models.Client;
+using Data.Models.Misc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using SharedLibraryCore;
+using SharedLibraryCore.Configuration;
+using SharedLibraryCore.Interfaces;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace IW4MAdmin.Application.Commands
+{
+ public class OfflineMessageCommand : Command
+ {
+ private readonly IDatabaseContextFactory _contextFactory;
+ private readonly ILogger _logger;
+ private const short MaxLength = 1024;
+
+ public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
+ IDatabaseContextFactory contextFactory, ILogger logger) : base(config, layout)
+ {
+ Name = "offlinemessage";
+ Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
+ Alias = "om";
+ Permission = EFClient.Permission.Moderator;
+ RequiresTarget = true;
+
+ _contextFactory = contextFactory;
+ _logger = logger;
+ }
+
+ public override async Task ExecuteAsync(GameEvent gameEvent)
+ {
+ if (gameEvent.Data.Length > MaxLength)
+ {
+ gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
+ return;
+ }
+
+ if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
+ {
+ gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
+ return;
+ }
+
+ if (gameEvent.Target.IsIngame)
+ {
+ gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"].FormatExt(gameEvent.Target.Name));
+ return;
+ }
+
+ await using var context = _contextFactory.CreateContext(enableTracking: false);
+ var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
+
+ var newMessage = new EFInboxMessage()
+ {
+ SourceClientId = gameEvent.Origin.ClientId,
+ DestinationClientId = gameEvent.Target.ClientId,
+ ServerId = server.Id,
+ Message = gameEvent.Data,
+ };
+
+ try
+ {
+ context.Set().Add(newMessage);
+ await context.SaveChangesAsync();
+ gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SUCCESS"]);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not save offline message {@Message}", newMessage);
+ throw;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/Commands/ReadMessageCommand.cs b/Application/Commands/ReadMessageCommand.cs
new file mode 100644
index 00000000..a09bebd8
--- /dev/null
+++ b/Application/Commands/ReadMessageCommand.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Data.Abstractions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using SharedLibraryCore;
+using SharedLibraryCore.Configuration;
+using SharedLibraryCore.Interfaces;
+using EFClient = Data.Models.Client.EFClient;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+
+namespace IW4MAdmin.Application.Commands
+{
+ public class ReadMessageCommand : Command
+ {
+ private readonly IDatabaseContextFactory _contextFactory;
+ private readonly ILogger _logger;
+
+ public ReadMessageCommand(CommandConfiguration config, ITranslationLookup layout,
+ IDatabaseContextFactory contextFactory, ILogger logger) : base(config, layout)
+ {
+ Name = "readmessage";
+ Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
+ Alias = "rm";
+ Permission = EFClient.Permission.Flagged;
+
+ _contextFactory = contextFactory;
+ _logger = logger;
+ }
+
+ public override async Task ExecuteAsync(GameEvent gameEvent)
+ {
+ try
+ {
+ await using var context = _contextFactory.CreateContext();
+
+ var inboxItems = await context.InboxMessages
+ .Include(message => message.SourceClient)
+ .ThenInclude(client => client.CurrentAlias)
+ .Where(message => message.DestinationClientId == gameEvent.Origin.ClientId)
+ .Where(message => !message.IsDelivered)
+ .ToListAsync();
+
+ if (!inboxItems.Any())
+ {
+ gameEvent.Origin.Tell(_translationLookup["COMMANDS_READ_MESSAGE_NONE"]);
+ return;
+ }
+
+ var index = 1;
+ foreach (var inboxItem in inboxItems)
+ {
+ await gameEvent.Origin.Tell(_translationLookup["COMMANDS_READ_MESSAGE_SUCCESS"]
+ .FormatExt($"{index}/{inboxItems.Count}", inboxItem.SourceClient.CurrentAlias.Name))
+ .WaitAsync();
+
+ foreach (var messageFragment in inboxItem.Message.FragmentMessageForDisplay())
+ {
+ await gameEvent.Origin.Tell(messageFragment).WaitAsync();
+ }
+
+ index++;
+ }
+
+ inboxItems.ForEach(item => { item.IsDelivered = true; });
+
+ context.UpdateRange(inboxItems);
+ await context.SaveChangesAsync();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Could not retrieve offline messages for {Client}", gameEvent.Origin.ToString());
+ throw;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/Configuration/LoggingConfiguration.json b/Application/Configuration/LoggingConfiguration.json
new file mode 100644
index 00000000..f6290ffc
--- /dev/null
+++ b/Application/Configuration/LoggingConfiguration.json
@@ -0,0 +1,49 @@
+{
+ "Serilog": {
+ "Using": [
+ "Serilog.Sinks.File"
+ ],
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "System": "Warning",
+ "Microsoft": "Warning"
+ }
+ },
+ "WriteTo": [
+ {
+ "Name": "File",
+ "Args": {
+ "path": "Log/IW4MAdmin-Application.log",
+ "rollingInterval": "Day",
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}"
+ }
+ }
+ ],
+ "Enrich": [
+ "FromLogContext",
+ "WithMachineName",
+ "WithThreadId"
+ ],
+ "Destructure": [
+ {
+ "Name": "ToMaximumDepth",
+ "Args": {
+ "maximumDestructuringDepth": 4
+ }
+ },
+ {
+ "Name": "ToMaximumStringLength",
+ "Args": {
+ "maximumStringLength": 1000
+ }
+ },
+ {
+ "Name": "ToMaximumCollectionCount",
+ "Args": {
+ "maximumCollectionCount": 24
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/Application/Configuration/ScriptPluginConfiguration.cs b/Application/Configuration/ScriptPluginConfiguration.cs
new file mode 100644
index 00000000..5728055e
--- /dev/null
+++ b/Application/Configuration/ScriptPluginConfiguration.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using SharedLibraryCore.Interfaces;
+
+namespace IW4MAdmin.Application.Configuration
+{
+ public class ScriptPluginConfiguration : Dictionary>, IBaseConfiguration
+ {
+ public string Name() => nameof(ScriptPluginConfiguration);
+
+ public IBaseConfiguration Generate()
+ {
+ return new ScriptPluginConfiguration();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/DefaultSettings.json b/Application/DefaultSettings.json
index 811565d1..5fae6054 100644
--- a/Application/DefaultSettings.json
+++ b/Application/DefaultSettings.json
@@ -1,4 +1,4 @@
-{
+{
"AutoMessagePeriod": 60,
"AutoMessages": [
"This server uses ^5IW4M Admin v{{VERSION}} ^7get it at ^5raidmax.org/IW4MAdmin",
@@ -16,7 +16,13 @@
"Keep grenade launcher use to a minimum",
"Balance teams at ALL times"
],
- "DisallowedClientNames": [ "Unknown Soldier", "VickNet", "UnknownSoldier", "CHEATER", "Play77" ],
+ "DisallowedClientNames": [
+ "Unknown Soldier",
+ "VickNet",
+ "UnknownSoldier",
+ "CHEATER",
+ "Play77"
+ ],
"QuickMessages": [
{
"Game": "IW4",
@@ -163,6 +169,18 @@
"Alias": "Asylum",
"Name": "mp_asylum"
},
+ {
+ "Alias": "Banzai",
+ "Name": "mp_kwai"
+ },
+ {
+ "Alias": "Battery",
+ "Name": "mp_drum"
+ },
+ {
+ "Alias": "Breach",
+ "Name": "mp_bgate"
+ },
{
"Alias": "Castle",
"Name": "mp_castle"
@@ -171,6 +189,10 @@
"Alias": "Cliffside",
"Name": "mp_shrine"
},
+ {
+ "Alias": "Corrosion",
+ "Name": "mp_stalingrad"
+ },
{
"Alias": "Courtyard",
"Name": "mp_courtyard"
@@ -184,60 +206,52 @@
"Name": "mp_downfall"
},
{
- "Alias": "Hanger",
+ "Alias": "Hangar",
"Name": "mp_hangar"
},
- {
- "Alias": "Makin",
- "Name": "mp_makin"
- },
- {
- "Alias": "Outskirts",
- "Name": "mp_outskirts"
- },
- {
- "Alias": "Roundhouse",
- "Name": "mp_roundhouse"
- },
- {
- "Alias": "Upheaval",
- "Name": "mp_suburban"
- },
{
"Alias": "Knee Deep",
"Name": "mp_kneedeep"
},
+ {
+ "Alias": "Makin",
+ "Name": "mp_makin"
+ },
+ {
+ "Alias": "Makin Day",
+ "Name": "mp_makin_day"
+ },
{
"Alias": "Nightfire",
"Name": "mp_nachtfeuer"
},
+ {
+ "Alias": "Outskirts",
+ "Name": "mp_outskirts"
+ },
+ {
+ "Alias": "Revolution",
+ "Name": "mp_vodka"
+ },
+ {
+ "Alias": "Roundhouse",
+ "Name": "mp_roundhouse"
+ },
+ {
+ "Alias": "Seelow",
+ "Name": "mp_seelow"
+ },
{
"Alias": "Station",
"Name": "mp_subway"
},
- {
- "Alias": "Banzai",
- "Name": "mp_kwai"
- },
- {
- "Alias": "Corrosion",
- "Name": "mp_stalingrad"
- },
{
"Alias": "Sub Pens",
"Name": "mp_docks"
},
{
- "Alias": "Battery",
- "Name": "mp_drum"
- },
- {
- "Alias": "Breach",
- "Name": "mp_bgate"
- },
- {
- "Alias": "Revolution",
- "Name": "mp_vodka"
+ "Alias": "Upheaval",
+ "Name": "mp_suburban"
}
]
},
@@ -248,225 +262,181 @@
"Alias": "Rust",
"Name": "mp_rust"
},
-
{
"Alias": "Terminal",
"Name": "mp_terminal"
},
-
{
"Alias": "Crash",
"Name": "mp_crash"
},
-
{
"Alias": "Afghan",
"Name": "mp_afghan"
},
-
{
"Alias": "Derail",
"Name": "mp_derail"
},
-
{
"Alias": "Estate",
"Name": "mp_estate"
},
-
{
"Alias": "Favela",
"Name": "mp_favela"
},
-
{
"Alias": "Highrise",
"Name": "mp_highrise"
},
-
{
"Alias": "Invasion",
"Name": "mp_invasion"
},
-
{
"Alias": "Karachi",
"Name": "mp_checkpoint"
},
-
{
"Alias": "Quarry",
"Name": "mp_quarry"
},
-
{
"Alias": "Rundown",
"Name": "mp_rundown"
},
-
{
"Alias": "Scrapyard",
"Name": "mp_boneyard"
},
-
{
"Alias": "Skidrow",
"Name": "mp_nightshift"
},
-
{
"Alias": "Sub Base",
"Name": "mp_subbase"
},
-
{
"Alias": "Underpass",
"Name": "mp_underpass"
},
-
{
"Alias": "Wasteland",
"Name": "mp_brecourt"
},
-
{
"Alias": "Overgrown",
"Name": "mp_overgrown"
},
-
{
"Alias": "Strike",
"Name": "mp_strike"
},
-
{
"Alias": "Vacant",
"Name": "mp_vacant"
},
-
{
"Alias": "Carnival",
"Name": "mp_abandon"
},
-
{
"Alias": "Trailer Park",
"Name": "mp_trailerpark"
},
-
{
"Alias": "Fuel",
"Name": "mp_fuel2"
},
-
{
"Alias": "Storm",
"Name": "mp_storm"
},
-
{
"Alias": "Bailout",
"Name": "mp_complex"
},
-
{
"Alias": "Salvage",
"Name": "mp_compact"
},
-
{
"Alias": "Nuketown",
"Name": "mp_nuked"
},
-
{
"Alias": "Test map",
"Name": "iw4_credits"
},
-
{
"Alias": "Killhouse",
"Name": "mp_killhouse"
},
-
{
"Alias": "Bog",
"Name": "mp_bog_sh"
},
-
{
"Alias": "Freighter",
"Name": "mp_cargoship_sh"
},
-
{
"Alias": "Cargoship",
"Name": "mp_cargoship"
},
-
{
"Alias": "Shipment",
"Name": "mp_shipment"
},
-
{
"Alias": "Shipment - Long",
"Name": "mp_shipment_long"
},
-
{
"Alias": "Rust - Long",
"Name": "mp_rust_long"
},
-
{
"Alias": "Firing Range",
"Name": "mp_firingrange"
},
-
{
"Alias": "Chemical Plant",
"Name": "mp_storm_spring"
},
-
{
"Alias": "Tropical Favela",
"Name": "mp_fav_tropical"
},
-
{
"Alias": "Tropical Estate",
"Name": "mp_estate_tropical"
},
-
{
"Alias": "Tropical Crash",
"Name": "mp_crash_tropical"
},
-
{
"Alias": "Forgotten City",
"Name": "mp_bloc_sh"
},
-
{
"Alias": "Crossfire",
"Name": "mp_cross_fire"
},
-
{
"Alias": "Bloc",
"Name": "mp_bloc"
},
-
{
"Alias": "Oilrig",
"Name": "oilrig"
},
-
{
- "Name": "Village",
- "Alias": "co_hunted"
+ "Alias": "Village",
+ "Name": "co_hunted"
}
]
},
@@ -519,7 +489,7 @@
},
{
"Alias": "Havana",
- "Name": "mp_cairo"
+ "Name": "mp_cairo"
},
{
"Alias": "Hazard",
@@ -725,6 +695,14 @@
{
"Alias": "Terminal",
"Name": "mp_terminal_cls"
+ },
+ {
+ "Alias": "Rust",
+ "Name": "mp_rust"
+ },
+ {
+ "Alias": "Highrise",
+ "Name": "mp_highrise"
}
]
},
@@ -884,6 +862,717 @@
"Name": "zm_transit"
}
]
+ },
+ {
+ "Game": "T7",
+ "Maps": [
+ {
+ "Alias": "Evac",
+ "Name": "mp_apartments"
+ },
+ {
+ "Alias": "Aquarium",
+ "Name": "mp_biodome"
+ },
+ {
+ "Alias": "Exodus",
+ "Name": "mp_chinatown"
+ },
+ {
+ "Alias": "Hunted",
+ "Name": "mp_ethiopia"
+ },
+ {
+ "Alias": "Havoc",
+ "Name": "mp_havoc"
+ },
+ {
+ "Alias": "Infection",
+ "Name": "mp_infection"
+ },
+ {
+ "Alias": "Metro",
+ "Name": "mp_metro"
+ },
+ {
+ "Alias": "Redwood",
+ "Name": "mp_redwood"
+ },
+ {
+ "Alias": "Combine",
+ "Name": "mp_sector"
+ },
+ {
+ "Alias": "Breach",
+ "Name": "mp_spire"
+ },
+ {
+ "Alias": "Stronghold",
+ "Name": "mp_stronghold"
+ },
+ {
+ "Alias": "Fringe",
+ "Name": "mp_veiled"
+ },
+ {
+ "Alias": "Nuk3town",
+ "Name": "mp_nuketown_x"
+ },
+ {
+ "Alias": "Gauntlet",
+ "Name": "mp_crucible"
+ },
+ {
+ "Alias": "Rise",
+ "Name": "mp_rise"
+ },
+ {
+ "Alias": "Skyjacked",
+ "Name": "mp_skyjacked"
+ },
+ {
+ "Alias": "Splash",
+ "Name": "mp_waterpark"
+ },
+ {
+ "Alias": "Spire",
+ "Name": "mp_aerospace"
+ },
+ {
+ "Alias": "Verge",
+ "Name": "mp_banzai"
+ },
+ {
+ "Alias": "Rift",
+ "Name": "mp_conduit"
+ },
+ {
+ "Alias": "Knockout",
+ "Name": "mp_kung_fu"
+ },
+ {
+ "Alias": "Rumble",
+ "Name": "mp_arena"
+ },
+ {
+ "Alias": "Cyrogen",
+ "Name": "mp_cryogen"
+ },
+ {
+ "Alias": "Empire",
+ "Name": "mp_rome"
+ },
+ {
+ "Alias": "Berserk",
+ "Name": "mp_shrine"
+ },
+ {
+ "Alias": "Rupture",
+ "Name": "mp_city"
+ },
+ {
+ "Alias": "Micro",
+ "Name": "mp_miniature"
+ },
+ {
+ "Alias": "Citadel",
+ "Name": "mp_ruins"
+ },
+ {
+ "Alias": "Outlaw",
+ "Name": "mp_western"
+ }
+ ]
+ },
+ {
+ "Game": "IW6",
+ "Maps": [
+ {
+ "Alias": "Prison Break",
+ "Name": "mp_prisonbreak"
+ },
+ {
+ "Alias": "Octane",
+ "Name": "mp_dart"
+ },
+ {
+ "Alias": "Tremor",
+ "Name": "mp_lonestar"
+ },
+ {
+ "Alias": "Freight",
+ "Name": "mp_frag"
+ },
+ {
+ "Alias": "Whiteout",
+ "Name": "mp_snow"
+ },
+ {
+ "Alias": "Stormfront",
+ "Name": "mp_fahrenheit"
+ },
+ {
+ "Alias": "Siege",
+ "Name": "mp_hashima"
+ },
+ {
+ "Alias": "Warhawk",
+ "Name": "mp_warhawk"
+ },
+ {
+ "Alias": "Sovereign",
+ "Name": "mp_sovereign"
+ },
+ {
+ "Alias": "Overload",
+ "Name": "mp_zebra"
+ },
+ {
+ "Alias": "Stonehaven",
+ "Name": "mp_skeleton"
+ },
+ {
+ "Alias": "Chasm",
+ "Name": "mp_chasm"
+ },
+ {
+ "Alias": "Flooded",
+ "Name": "mp_flooded"
+ },
+ {
+ "Alias": "Strikezone",
+ "Name": "mp_strikezone"
+ },
+ {
+ "Alias": "Free Fall",
+ "Name": "mp_descent_new"
+ },
+ {
+ "Alias": "Unearthed",
+ "Name": "mp_dome_ns"
+ },
+ {
+ "Alias": "Collision",
+ "Name": "mp_ca_impact"
+ },
+ {
+ "Alias": "Behemoth",
+ "Name": "mp_ca_behemoth"
+ },
+ {
+ "Alias": "Ruins",
+ "Name": "mp_battery3"
+ },
+ {
+ "Alias": "Pharaoh",
+ "Name": "mp_dig"
+ },
+ {
+ "Alias": "Favela",
+ "Name": "mp_favela_iw6"
+ },
+ {
+ "Alias": "Mutiny",
+ "Name": "mp_pirate"
+ },
+ {
+ "Alias": "Departed",
+ "Name": "mp_zulu"
+ },
+ {
+ "Alias": "Dynasty",
+ "Name": "mp_conflict"
+ },
+ {
+ "Alias": "Goldrush",
+ "Name": "mp_mine"
+ },
+ {
+ "Alias": "Showtime",
+ "Name": "mp_shipment_ns"
+ },
+ {
+ "Alias": "Subzero",
+ "Name": "mp_zerosub"
+ },
+ {
+ "Alias": "Ignition",
+ "Name": "mp_boneyard_ns"
+ },
+ {
+ "Alias": "Containment",
+ "Name": "mp_ca_red_river"
+ },
+ {
+ "Alias": "Bayview",
+ "Name": "mp_ca_rumble"
+ },
+ {
+ "Alias": "Fog",
+ "Name": "mp_swamp"
+ },
+ {
+ "Alias": "Point of Contact",
+ "Name": "mp_alien_town"
+ },
+ {
+ "Alias": "Nightfall",
+ "Name": "mp_alien_armory"
+ },
+ {
+ "Alias": "Mayday",
+ "Name": "mp_alien_beacon"
+ },
+ {
+ "Alias": "Awakening",
+ "Name": "mp_alien_dlc3"
+ },
+ {
+ "Alias": "Exodus",
+ "Name": "mp_alien_last"
+ }
+ ]
+ },
+ {
+ "Game": "SHG1",
+ "Maps": [
+ {
+ "Alias": "Ascend",
+ "Name": "mp_refraction"
+ },
+ {
+ "Alias": "Bio Lab",
+ "Name": "mp_lab2"
+ },
+ {
+ "Alias": "Comeback",
+ "Name": "mp_comeback"
+ },
+ {
+ "Alias": "Defender",
+ "Name": "mp_laser2"
+ },
+ {
+ "Alias": "Detroit",
+ "Name": "mp_detroit"
+ },
+ {
+ "Alias": "Greenband",
+ "Name": "mp_greenband"
+ },
+ {
+ "Alias": "Horizon",
+ "Name": "mp_levity"
+ },
+ {
+ "Alias": "Instinct",
+ "Name": "mp_instinct"
+ },
+ {
+ "Alias": "Recovery",
+ "Name": "mp_recovery"
+ },
+ {
+ "Alias": "Retreat",
+ "Name": "mp_venus"
+ },
+ {
+ "Alias": "Riot",
+ "Name": "mp_prison"
+ },
+ {
+ "Alias": "Solar",
+ "Name": "mp_solar"
+ },
+ {
+ "Alias": "Terrace",
+ "Name": "mp_terrace"
+ },
+ {
+ "Alias": "Atlas Gorge",
+ "Name": "mp_dam"
+ },
+ {
+ "Alias": "Chop Shop",
+ "Name": "mp_spark"
+ },
+ {
+ "Alias": "Climate",
+ "Name": "mp_climate_3"
+ },
+ {
+ "Alias": "Compound",
+ "Name": "mp_sector17"
+ },
+ {
+ "Alias": "Core",
+ "Name": "mp_lost"
+ },
+ {
+ "Alias": "Drift",
+ "Name": "mp_torqued"
+ },
+ {
+ "Alias": "Fracture",
+ "Name": "mp_fracture"
+ },
+ {
+ "Alias": "Kremlin",
+ "Name": "mp_kremlin"
+ },
+ {
+ "Alias": "Overload",
+ "Name": "mp_lair"
+ },
+ {
+ "Alias": "Parliament",
+ "Name": "mp_bigben2"
+ },
+ {
+ "Alias": "Perplex",
+ "Name": "mp_perplex_1"
+ },
+ {
+ "Alias": "Quarantine",
+ "Name": "mp_liberty"
+ },
+ {
+ "Alias": "Sideshow",
+ "Name": "mp_clowntown3"
+ },
+ {
+ "Alias": "Site 244",
+ "Name": "mp_blackbox"
+ },
+ {
+ "Alias": "Skyrise",
+ "Name": "mp_highrise2"
+ },
+ {
+ "Alias": "Swarn",
+ "Name": "mp_seoul2"
+ },
+ {
+ "Alias": "Urban",
+ "Name": "mp_urban"
+ }
+ ]
+ },
+ {
+ "Game": "CSGO",
+ "Maps": [
+ {
+ "Name": "ar_baggage",
+ "Alias": "Baggage"
+ },
+ {
+ "Name": "ar_dizzy",
+ "Alias": "Dizzy"
+ },
+ {
+ "Name": "ar_lunacy",
+ "Alias": "Lunacy"
+ },
+ {
+ "Name": "ar_monastery",
+ "Alias": "Monastery"
+ },
+ {
+ "Name": "ar_shoots",
+ "Alias": "Shoots"
+ },
+ {
+ "Name": "cs_agency",
+ "Alias": "Agency"
+ },
+ {
+ "Name": "cs_assault",
+ "Alias": "Assault"
+ },
+ {
+ "Name": "cs_italy",
+ "Alias": "Italy"
+ },
+ {
+ "Name": "cs_militia",
+ "Alias": "Militia"
+ },
+ {
+ "Name": "cs_office",
+ "Alias": "Office"
+ },
+ {
+ "Name": "de_ancient",
+ "Alias": "Ancient"
+ },
+ {
+ "Name": "de_bank",
+ "Alias": "Bank"
+ },
+ {
+ "Name": "de_cache",
+ "Alias": "Cache"
+ },
+ {
+ "Name": "de_calavera",
+ "Alias": "Calavera"
+ },
+ {
+ "Name": "de_canals",
+ "Alias": "Canals"
+ },
+ {
+ "Name": "de_cbble",
+ "Alias": "Cobblestone"
+ },
+ {
+ "Name": "de_dust2",
+ "Alias": "Dust II"
+ },
+ {
+ "Name": "de_grind",
+ "Alias": "Grind"
+ },
+ {
+ "Name": "de_inferno",
+ "Alias": "Inferno"
+ },
+ {
+ "Name": "de_lake",
+ "Alias": "Lake"
+ },
+ {
+ "Name": "de_mirage",
+ "Alias": "Mirage"
+ },
+ {
+ "Name": "de_mocha",
+ "Alias": "Mocha"
+ },
+ {
+ "Name": "de_nuke",
+ "Alias": "Nuke"
+ },
+ {
+ "Name": "de_overpass",
+ "Alias": "Overpass"
+ },
+ {
+ "Name": "de_pitstop",
+ "Alias": "Pitstop"
+ },
+ {
+ "Name": "de_safehouse",
+ "Alias": "Safehouse"
+ },
+ {
+ "Name": "de_shortdust",
+ "Alias": "Shortdust"
+ },
+ {
+ "Name": "de_shortnuke",
+ "Alias": "Shortnuke"
+ },
+ {
+ "Name": "de_stmarc",
+ "Alias": "St. Marc"
+ },
+ {
+ "Name": "de_sugarcane",
+ "Alias": "Sugarcane"
+ },
+ {
+ "Name": "de_train",
+ "Alias": "Train"
+ },
+ {
+ "Name": "de_vertigo",
+ "Alias": "Vertigo"
+ },
+ {
+ "Name": "dz_blacksite",
+ "Alias": "Blacksite"
+ },
+ {
+ "Name": "dz_frostbite",
+ "Alias": "Frostbite"
+ },
+ {
+ "Name": "dz_sirocco",
+ "Alias": "Sirocco"
+ }
+ ]
}
- ]
+ ],
+ "GameStrings": {
+ "IW4": {
+ "torso_upper": "Upper Torso",
+ "torso_lower": "Lower Torso",
+ "right_leg_upper": "Upper Right Leg",
+ "right_leg_lower": "Lower Right Leg",
+ "right_hand": "Right Hand",
+ "right_foot": "Right Foot",
+ "right_arm_upper": "Upper Right Arm",
+ "right_arm_lower": "Lower Right Arm",
+ "left_leg_upper": "Upper Left Leg",
+ "left_leg_lower": "Lower Left Leg",
+ "left_hand": "Left Hand",
+ "left_foot": "Left Foot",
+ "left_arm_upper": "Upper Left Arm",
+ "left_arm_lower": "Lower Left Arm",
+ "acog": "ACOG Sight",
+ "eotech": "Holographic Sight",
+ "fmj": "FMJ",
+ "gl": "Grenade Launcher",
+ "heartbeat": "Heartbeat Sensor",
+ "reflex": "Red Dot Sight",
+ "rof": "Rapid Fire",
+ "thermal": "Thermal",
+ "xmags": "Extended Mags",
+ "m4": "M4A1",
+ "m40a3": "M40A3",
+ "ak47": "AK-47",
+ "ak47classic": "AK-47 Classic",
+ "fn2000": "F2000",
+ "masada": "ACR",
+ "famas": "FAMAS",
+ "fal": "FAL",
+ "scar": "SCAR-H",
+ "tavor": "TAR-21",
+ "m16": "M16A4",
+ "mp5k": "MP5K",
+ "ump45": "UMP45",
+ "kriss": "Vector",
+ "uzi": "Mini-Uzi",
+ "rpd": "RPD",
+ "sa80": "L86 LSW",
+ "mg4": "MG4",
+ "aug": "AUG HBAR",
+ "cheytac": "Intervention",
+ "barrett": "Barrett .50cal",
+ "wa2000": "WA2000",
+ "m21": "M21 EBR",
+ "pp2000": "PP2000",
+ "glock": "G18",
+ "beretta": "M93 Raffica",
+ "tmp": "TMP",
+ "spas12": "SPAS-12",
+ "aa12": "AA-12",
+ "model1887": "Model 1887",
+ "usp": "USP .45",
+ "coltanaconda": ".44 Magnum",
+ "deserteagle": "Desert Eagle",
+ "deserteaglegold": "Desert Eagle Gold",
+ "at4": "AT4-HS",
+ "m79": "Thumper",
+ "rpg": "RPG-7",
+ "concussion": "Stun",
+ "throwingknife": "Throwing Knife",
+ "ffar": "Airstrike",
+ "pavelow": "Pave Low",
+ "cobra": "Attack Helicopter",
+ "ac130": "AC-130",
+ "remotemissile": "Predator Missile",
+ "artillery": "Precision Airstrike",
+ "player": "",
+ "attach": ""
+ },
+ "IW3": {
+ "torso_upper": "Upper Torso",
+ "torso_lower": "Lower Torso",
+ "right_leg_upper": "Upper Right Leg",
+ "right_leg_lower": "Lower Right Leg",
+ "right_hand": "Right Hand",
+ "right_foot": "Right Foot",
+ "right_arm_upper": "Upper Right Arm",
+ "right_arm_lower": "Lower Right Arm",
+ "left_leg_upper": "Upper Left Leg",
+ "left_leg_lower": "Lower Left Leg",
+ "left_hand": "Left Hand",
+ "left_foot": "Left Foot",
+ "left_arm_upper": "Upper Left Arm",
+ "left_arm_lower": "Lower Left Arm",
+ "acog": "ACOG Sight",
+ "gl": "Grenade Launcher",
+ "reflex": "Red Dot Sight",
+ "grip": "Grip",
+ "m4": "M4 Carbine",
+ "m40a3": "M40A3",
+ "ak47": "AK-47",
+ "ak74u": "AK-74u",
+ "rpg": "RPG-7",
+ "deserteagle": "Desert Eagle",
+ "deserteaglegold": "Desert Eagle Gold",
+ "m16": "M16A4",
+ "g36c": "G36C",
+ "uzi": "Mini-Uzi",
+ "m60e4": "M60E4",
+ "mp5": "MP5",
+ "barrett": "Barrett .50cal",
+ "mp44": "MP44",
+ "remington700": "R700",
+ "rpd": "RDP",
+ "saw": " M249 SAW",
+ "usp": "USP .45",
+ "winchester1200": "W1200",
+ "concussion": "Stun",
+ "melee": "Knife",
+ "Frag" : "Grenade",
+ "airstrike": "Airstrike",
+ "helicopter": "Attack Helicopter",
+ "player": "",
+ "attach": ""
+ },
+ "T4": {
+ "torso_upper": "Upper Torso",
+ "torso_lower": "Lower Torso",
+ "right_leg_upper": "Upper Right Leg",
+ "right_leg_lower": "Lower Right Leg",
+ "right_hand": "Right Hand",
+ "right_foot": "Right Foot",
+ "right_arm_upper": "Upper Right Arm",
+ "right_arm_lower": "Lower Right Arm",
+ "left_leg_upper": "Upper Left Leg",
+ "left_leg_lower": "Lower Left Leg",
+ "left_hand": "Left Hand",
+ "left_foot": "Left Foot",
+ "left_arm_upper": "Upper Left Arm",
+ "left_arm_lower": "Lower Left Arm",
+ "gl": "Rifle Grenade",
+ "bigammo": "Round Drum",
+ "scoped": "Sniper Scope",
+ "telescopic": "Telescopic Sight",
+ "aperture": "Aperture Sight",
+ "flash": "Flash Hider",
+ "silenced": "Silencer",
+ "molotov": "Molotov Cocktail",
+ "sticky": "N° 74 ST",
+ "m2": "M2 Flamethrower",
+ "artillery": "Artillery Strike",
+ "dog": "Attack Dogs",
+ "colt": "Colt M1911",
+ "357magnum": ".357 Magnum",
+ "walther": "Walther P38",
+ "tokarev": "Tokarev TT-33",
+ "shotgun": "M1897 Trench Gun",
+ "doublebarreledshotgun": "Double-Barreled Shotgun",
+ "mp40": "MP40",
+ "type100smg": "Type 100",
+ "ppsh": "PPSh-41",
+ "svt40": "SVT-40",
+ "gewehr43": "Gewehr 43",
+ "m1garand": "M1 Garand",
+ "stg44": "STG-44",
+ "m1carbine": "M1A1 Carbine",
+ "type99lmg": "Type 99",
+ "bar": "BAR",
+ "dp28": "DP-28",
+ "mg42": "MG42",
+ "fg42": "FG42",
+ "30cal": "Browning M1919",
+ "type99rifle": "Arisaka",
+ "mosinrifle": "Mosin-Nagant",
+ "ptrs41":"PTRS-41"
+ }
+ }
}
diff --git a/Application/EventParsers/BaseEventParser.cs b/Application/EventParsers/BaseEventParser.cs
index a786628e..47755de9 100644
--- a/Application/EventParsers/BaseEventParser.cs
+++ b/Application/EventParsers/BaseEventParser.cs
@@ -5,7 +5,10 @@ using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
+using Data.Models;
+using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.EventParsers
{
@@ -14,6 +17,8 @@ namespace IW4MAdmin.Application.EventParsers
private readonly Dictionary)> _customEventRegistrations;
private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig;
+ private readonly Dictionary _regexMap;
+ private readonly Dictionary _eventTypeMap;
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig)
{
@@ -75,7 +80,28 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
+ Configuration.MapChange.Pattern = @".*InitGame.*";
+ Configuration.MapEnd.Pattern = @".*(?:ExitLevel|ShutdownGame).*";
+
Configuration.Time.Pattern = @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )";
+
+ _regexMap = new Dictionary
+ {
+ {Configuration.Say, GameEvent.EventType.Say},
+ {Configuration.Kill, GameEvent.EventType.Kill},
+ {Configuration.MapChange, GameEvent.EventType.MapChange},
+ {Configuration.MapEnd, GameEvent.EventType.MapEnd}
+ };
+
+ _eventTypeMap = new Dictionary
+ {
+ {"say", GameEvent.EventType.Say},
+ {"sayteam", GameEvent.EventType.Say},
+ {"K", GameEvent.EventType.Kill},
+ {"D", GameEvent.EventType.Damage},
+ {"J", GameEvent.EventType.PreConnect},
+ {"Q", GameEvent.EventType.PreDisconnect},
+ };
}
public IEventParserConfiguration Configuration { get; set; }
@@ -88,47 +114,79 @@ namespace IW4MAdmin.Application.EventParsers
public string Name { get; set; } = "Call of Duty";
+ private (GameEvent.EventType type, string eventKey) GetEventTypeFromLine(string logLine)
+ {
+ var lineSplit = logLine.Split(';');
+ if (lineSplit.Length > 1)
+ {
+ var type = lineSplit[0];
+ return _eventTypeMap.ContainsKey(type) ? (_eventTypeMap[type], type): (GameEvent.EventType.Unknown, lineSplit[0]);
+ }
+
+ foreach (var (key, value) in _regexMap)
+ {
+ var result = key.PatternMatcher.Match(logLine);
+ if (result.Success)
+ {
+ return (value, null);
+ }
+ }
+
+ return (GameEvent.EventType.Unknown, null);
+ }
+
+
public virtual GameEvent GenerateGameEvent(string logLine)
{
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
- int gameTime = 0;
+ var gameTime = 0L;
if (timeMatch.Success)
{
- gameTime = timeMatch
- .Values
- .Skip(2)
- // this converts the timestamp into seconds passed
- .Select((_value, index) => int.Parse(_value.ToString()) * (index == 0 ? 60 : 1))
- .Sum();
+ if (timeMatch.Values[0].Contains(":"))
+ {
+ gameTime = timeMatch
+ .Values
+ .Skip(2)
+ // this converts the timestamp into seconds passed
+ .Select((value, index) => long.Parse(value.ToString()) * (index == 0 ? 60 : 1))
+ .Sum();
+
+ }
+ else
+ {
+ gameTime = long.Parse(timeMatch.Values[0]);
+ }
+
// we want to strip the time from the log line
- logLine = logLine.Substring(timeMatch.Values.First().Length);
+ logLine = logLine.Substring(timeMatch.Values.First().Length).Trim();
}
- string[] lineSplit = logLine.Split(';');
- string eventType = lineSplit[0];
+ var eventParseResult = GetEventTypeFromLine(logLine);
+ var eventType = eventParseResult.type;
+
+ _logger.LogDebug(logLine);
- if (eventType == "say" || eventType == "sayteam")
+ if (eventType == GameEvent.EventType.Say)
{
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
if (matchResult.Success)
{
- string message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
- .ToString()
+ var message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.Replace("\x15", "")
.Trim();
if (message.Length > 0)
{
- string originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
- string originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
+ var originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
+ var originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]];
- long originId = originIdString.IsBotGuid() ?
+ var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
- int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
+ var clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
{
@@ -160,26 +218,26 @@ namespace IW4MAdmin.Application.EventParsers
}
}
- if (eventType == "K")
+ if (eventType == GameEvent.EventType.Kill)
{
var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (match.Success)
{
- string originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
- string targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
- string originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
- string targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
+ var originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
+ var targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
+ var originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]];
+ var targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]];
- long originId = originIdString.IsBotGuid() ?
+ var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
- long targetId = targetIdString.IsBotGuid() ?
+ var targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
- int originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
- int targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
+ var originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
+ var targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
@@ -194,26 +252,26 @@ namespace IW4MAdmin.Application.EventParsers
}
}
- if (eventType == "D")
+ if (eventType == GameEvent.EventType.Damage)
{
var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (match.Success)
{
- string originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
- string targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
- string originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
- string targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
+ var originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
+ var targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
+ var originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]];
+ var targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]];
- long originId = originIdString.IsBotGuid() ?
+ var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
- long targetId = targetIdString.IsBotGuid() ?
+ var targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
- int originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
- int targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
+ var originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
+ var targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
@@ -228,16 +286,16 @@ namespace IW4MAdmin.Application.EventParsers
}
}
- if (eventType == "J")
+ if (eventType == GameEvent.EventType.PreConnect)
{
var match = Configuration.Join.PatternMatcher.Match(logLine);
if (match.Success)
{
- string originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
- string originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
+ var originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
+ var originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]];
- long networkId = originIdString.IsBotGuid() ?
+ var networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
@@ -249,10 +307,10 @@ namespace IW4MAdmin.Application.EventParsers
{
CurrentAlias = new EFAlias()
{
- Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine(),
+ Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine(),
},
NetworkId = networkId,
- ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
+ ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]),
State = EFClient.ClientState.Connecting,
},
Extra = originIdString,
@@ -264,16 +322,16 @@ namespace IW4MAdmin.Application.EventParsers
}
}
- if (eventType == "Q")
+ if (eventType == GameEvent.EventType.PreDisconnect)
{
var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (match.Success)
{
- string originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
- string originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
+ var originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
+ var originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]];
- long networkId = originIdString.IsBotGuid() ?
+ var networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
@@ -285,10 +343,10 @@ namespace IW4MAdmin.Application.EventParsers
{
CurrentAlias = new EFAlias()
{
- Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine()
+ Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine()
},
NetworkId = networkId,
- ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
+ ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]),
State = EFClient.ClientState.Disconnecting
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
@@ -299,7 +357,7 @@ namespace IW4MAdmin.Application.EventParsers
}
}
- if (eventType.Contains("ExitLevel"))
+ if (eventType == GameEvent.EventType.MapEnd)
{
return new GameEvent()
{
@@ -313,9 +371,9 @@ namespace IW4MAdmin.Application.EventParsers
};
}
- if (eventType.Contains("InitGame"))
+ if (eventType == GameEvent.EventType.MapChange)
{
- string dump = eventType.Replace("InitGame: ", "");
+ var dump = logLine.Replace("InitGame: ", "");
return new GameEvent()
{
@@ -330,26 +388,37 @@ namespace IW4MAdmin.Application.EventParsers
};
}
- if (_customEventRegistrations.ContainsKey(eventType))
+ if (eventParseResult.eventKey == null || !_customEventRegistrations.ContainsKey(eventParseResult.eventKey))
{
- var eventModifier = _customEventRegistrations[eventType];
-
- try
+ return new GameEvent()
{
- return eventModifier.Item2(logLine, Configuration, new GameEvent()
- {
- Type = GameEvent.EventType.Other,
- Data = logLine,
- Subtype = eventModifier.Item1,
- GameTime = gameTime,
- Source = GameEvent.EventSource.Log
- });
- }
+ Type = GameEvent.EventType.Unknown,
+ Data = logLine,
+ Origin = Utilities.IW4MAdminClient(),
+ Target = Utilities.IW4MAdminClient(),
+ RequiredEntity = GameEvent.EventRequiredEntity.None,
+ GameTime = gameTime,
+ Source = GameEvent.EventSource.Log
+ };
+ }
- catch (Exception e)
+ var eventModifier = _customEventRegistrations[eventParseResult.eventKey];
+
+ try
+ {
+ return eventModifier.Item2(logLine, Configuration, new GameEvent()
{
- _logger.WriteWarning($"Could not handle custom event generation - {e.GetExceptionInfo()}");
- }
+ Type = GameEvent.EventType.Other,
+ Data = logLine,
+ Subtype = eventModifier.Item1,
+ GameTime = gameTime,
+ Source = GameEvent.EventSource.Log
+ });
+ }
+
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Could not handle custom event generation");
}
return new GameEvent()
diff --git a/Application/EventParsers/DynamicEventParser.cs b/Application/EventParsers/DynamicEventParser.cs
index 17022320..8c8c0fb3 100644
--- a/Application/EventParsers/DynamicEventParser.cs
+++ b/Application/EventParsers/DynamicEventParser.cs
@@ -1,5 +1,6 @@
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.EventParsers
{
diff --git a/Application/EventParsers/DynamicEventParserConfiguration.cs b/Application/EventParsers/DynamicEventParserConfiguration.cs
index 026c275b..59873bf1 100644
--- a/Application/EventParsers/DynamicEventParserConfiguration.cs
+++ b/Application/EventParsers/DynamicEventParserConfiguration.cs
@@ -1,5 +1,6 @@
using SharedLibraryCore.Interfaces;
using System.Globalization;
+using SharedLibraryCore;
namespace IW4MAdmin.Application.EventParsers
{
@@ -17,6 +18,8 @@ namespace IW4MAdmin.Application.EventParsers
public ParserRegex Damage { get; set; }
public ParserRegex Action { get; set; }
public ParserRegex Time { get; set; }
+ public ParserRegex MapChange { get; set; }
+ public ParserRegex MapEnd { get; set; }
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public DynamicEventParserConfiguration(IParserRegexFactory parserRegexFactory)
@@ -28,6 +31,8 @@ namespace IW4MAdmin.Application.EventParsers
Damage = parserRegexFactory.CreateParserRegex();
Action = parserRegexFactory.CreateParserRegex();
Time = parserRegexFactory.CreateParserRegex();
+ MapChange = parserRegexFactory.CreateParserRegex();
+ MapEnd = parserRegexFactory.CreateParserRegex();
}
}
}
diff --git a/Application/Extensions/StartupExtensions.cs b/Application/Extensions/StartupExtensions.cs
new file mode 100644
index 00000000..4e0e2242
--- /dev/null
+++ b/Application/Extensions/StartupExtensions.cs
@@ -0,0 +1,104 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using Data.MigrationContext;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Serilog;
+using Serilog.Events;
+using SharedLibraryCore;
+using SharedLibraryCore.Configuration;
+using ILogger = Serilog.ILogger;
+
+namespace IW4MAdmin.Application.Extensions
+{
+ public static class StartupExtensions
+ {
+ private static ILogger _defaultLogger = null;
+
+ public static IServiceCollection AddBaseLogger(this IServiceCollection services,
+ ApplicationConfiguration appConfig)
+ {
+ if (_defaultLogger == null)
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddJsonFile(Path.Join(Utilities.OperatingDirectory, "Configuration", "LoggingConfiguration.json"))
+ .Build();
+
+ var loggerConfig = new LoggerConfiguration()
+ .ReadFrom.Configuration(configuration)
+ .MinimumLevel.Override("Microsoft", LogEventLevel.Warning);
+
+ if (Utilities.IsDevelopment)
+ {
+ loggerConfig = loggerConfig.WriteTo.Console(
+ outputTemplate:
+ "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
+ .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
+ .MinimumLevel.Debug();
+ }
+
+ _defaultLogger = loggerConfig.CreateLogger();
+ }
+
+ services.AddLogging(builder => builder.AddSerilog(_defaultLogger, dispose: true));
+ services.AddSingleton(new LoggerFactory()
+ .AddSerilog(_defaultLogger, true));
+ return services;
+ }
+
+ public static IServiceCollection AddDatabaseContextOptions(this IServiceCollection services,
+ ApplicationConfiguration appConfig)
+ {
+ var activeProvider = appConfig.DatabaseProvider?.ToLower();
+
+ if (string.IsNullOrEmpty(appConfig.ConnectionString) || activeProvider == "sqlite")
+ {
+ var currentPath = Utilities.OperatingDirectory;
+ currentPath = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? $"{Path.DirectorySeparatorChar}{currentPath}"
+ : currentPath;
+
+ var connectionStringBuilder = new SqliteConnectionStringBuilder
+ {DataSource = Path.Join(currentPath, "Database", "Database.db")};
+ var connectionString = connectionStringBuilder.ToString();
+
+ services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder()
+ .UseSqlite(connectionString)
+ .UseLoggerFactory(sp.GetRequiredService())
+ .EnableSensitiveDataLogging().Options);
+ return services;
+ }
+
+ switch (activeProvider)
+ {
+ case "mysql":
+ var appendTimeout = !appConfig.ConnectionString.Contains("default command timeout",
+ StringComparison.InvariantCultureIgnoreCase);
+ services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder()
+ .UseMySql(appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : ""),
+ mysqlOptions => mysqlOptions.EnableRetryOnFailure())
+ .UseLoggerFactory(sp.GetRequiredService()).Options);
+ return services;
+ case "postgresql":
+ appendTimeout = !appConfig.ConnectionString.Contains("Command Timeout",
+ StringComparison.InvariantCultureIgnoreCase);
+ services.AddSingleton(sp =>
+ (DbContextOptions) new DbContextOptionsBuilder()
+ .UseNpgsql(appConfig.ConnectionString + (appendTimeout ? ";Command Timeout=0" : ""),
+ postgresqlOptions =>
+ {
+ postgresqlOptions.EnableRetryOnFailure();
+ postgresqlOptions.SetPostgresVersion(new Version("9.4"));
+ })
+ .UseLoggerFactory(sp.GetRequiredService()).Options);
+ return services;
+ default:
+ throw new ArgumentException($"No context available for {appConfig.DatabaseProvider}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/Factories/DatabaseContextFactory.cs b/Application/Factories/DatabaseContextFactory.cs
index 631b7b5a..604d8e6c 100644
--- a/Application/Factories/DatabaseContextFactory.cs
+++ b/Application/Factories/DatabaseContextFactory.cs
@@ -1,5 +1,9 @@
-using SharedLibraryCore.Database;
-using SharedLibraryCore.Interfaces;
+using System;
+using Data.Abstractions;
+using Data.Context;
+using Data.MigrationContext;
+using Microsoft.EntityFrameworkCore;
+using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Factories
{
@@ -8,6 +12,15 @@ namespace IW4MAdmin.Application.Factories
///
public class DatabaseContextFactory : IDatabaseContextFactory
{
+ private readonly DbContextOptions _contextOptions;
+ private readonly string _activeProvider;
+
+ public DatabaseContextFactory(ApplicationConfiguration appConfig, DbContextOptions contextOptions)
+ {
+ _contextOptions = contextOptions;
+ _activeProvider = appConfig.DatabaseProvider?.ToLower();
+ }
+
///
/// creates a new database context
///
@@ -15,7 +28,35 @@ namespace IW4MAdmin.Application.Factories
///
public DatabaseContext CreateContext(bool? enableTracking = true)
{
- return enableTracking.HasValue ? new DatabaseContext(disableTracking: !enableTracking.Value) : new DatabaseContext();
+ var context = BuildContext();
+
+ enableTracking ??= true;
+
+ if (enableTracking.Value)
+ {
+ context.ChangeTracker.AutoDetectChangesEnabled = true;
+ context.ChangeTracker.LazyLoadingEnabled = true;
+ context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
+ }
+ else
+ {
+ context.ChangeTracker.AutoDetectChangesEnabled = false;
+ context.ChangeTracker.LazyLoadingEnabled = false;
+ context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ }
+
+ return context;
+ }
+
+ private DatabaseContext BuildContext()
+ {
+ return _activeProvider switch
+ {
+ "sqlite" => new SqliteDatabaseContext(_contextOptions),
+ "mysql" => new MySqlDatabaseContext(_contextOptions),
+ "postgresql" => new PostgresqlDatabaseContext(_contextOptions),
+ _ => throw new ArgumentException($"No context found for {_activeProvider}")
+ };
}
}
-}
+}
\ No newline at end of file
diff --git a/Application/Factories/GameLogReaderFactory.cs b/Application/Factories/GameLogReaderFactory.cs
index 53854e8b..e035e012 100644
--- a/Application/Factories/GameLogReaderFactory.cs
+++ b/Application/Factories/GameLogReaderFactory.cs
@@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore.Interfaces;
using System;
+using Microsoft.Extensions.Logging;
namespace IW4MAdmin.Application.Factories
{
@@ -19,12 +20,12 @@ namespace IW4MAdmin.Application.Factories
var baseUri = logUris[0];
if (baseUri.Scheme == Uri.UriSchemeHttp)
{
- return new GameLogReaderHttp(logUris, eventParser, _serviceProvider.GetRequiredService());
+ return new GameLogReaderHttp(logUris, eventParser, _serviceProvider.GetRequiredService>());
}
else if (baseUri.Scheme == Uri.UriSchemeFile)
{
- return new GameLogReader(baseUri.LocalPath, eventParser, _serviceProvider.GetRequiredService());
+ 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 97a53257..0e222fbc 100644
--- a/Application/Factories/GameServerInstanceFactory.cs
+++ b/Application/Factories/GameServerInstanceFactory.cs
@@ -1,7 +1,10 @@
-using SharedLibraryCore;
+using System;
+using Data.Abstractions;
+using Data.Models.Server;
+using Microsoft.Extensions.DependencyInjection;
+using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
-using System.Collections;
namespace IW4MAdmin.Application.Factories
{
@@ -11,21 +14,21 @@ namespace IW4MAdmin.Application.Factories
internal class GameServerInstanceFactory : IGameServerInstanceFactory
{
private readonly ITranslationLookup _translationLookup;
- private readonly IRConConnectionFactory _rconConnectionFactory;
- private readonly IGameLogReaderFactory _gameLogReaderFactory;
private readonly IMetaService _metaService;
+ private readonly IServiceProvider _serviceProvider;
///
/// base constructor
///
///
///
- public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService)
+ public GameServerInstanceFactory(ITranslationLookup translationLookup,
+ IMetaService metaService,
+ IServiceProvider serviceProvider)
{
_translationLookup = translationLookup;
- _rconConnectionFactory = rconConnectionFactory;
- _gameLogReaderFactory = gameLogReaderFactory;
_metaService = metaService;
+ _serviceProvider = serviceProvider;
}
///
@@ -36,7 +39,10 @@ namespace IW4MAdmin.Application.Factories
///
public Server CreateServer(ServerConfiguration config, IManager manager)
{
- return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory, _gameLogReaderFactory, _metaService);
+ return new IW4MServer(config,
+ _serviceProvider.GetRequiredService(), _translationLookup, _metaService,
+ _serviceProvider, _serviceProvider.GetRequiredService(),
+ _serviceProvider.GetRequiredService>());
}
}
-}
+}
\ No newline at end of file
diff --git a/Application/Factories/RConConnectionFactory.cs b/Application/Factories/RConConnectionFactory.cs
index 1dd0d2f8..55d222b0 100644
--- a/Application/Factories/RConConnectionFactory.cs
+++ b/Application/Factories/RConConnectionFactory.cs
@@ -1,6 +1,13 @@
-using IW4MAdmin.Application.RCon;
+using System;
+using System.Net;
using SharedLibraryCore.Interfaces;
using System.Text;
+using Integrations.Cod;
+using Integrations.Source;
+using Integrations.Source.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Factories
{
@@ -9,28 +16,31 @@ namespace IW4MAdmin.Application.Factories
///
internal class RConConnectionFactory : IRConConnectionFactory
{
- private static readonly Encoding gameEncoding = Encoding.GetEncoding("windows-1252");
- private readonly ILogger _logger;
-
+ private static readonly Encoding GameEncoding = Encoding.GetEncoding("windows-1252");
+ private readonly IServiceProvider _serviceProvider;
+
///
/// Base constructor
///
///
- public RConConnectionFactory(ILogger logger)
+ public RConConnectionFactory(IServiceProvider serviceProvider)
{
- _logger = logger;
+ _serviceProvider = serviceProvider;
}
- ///
- /// 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)
+ ///
+ public IRConConnection CreateConnection(IPEndPoint ipEndpoint, string password, string rconEngine)
{
- return new RConConnection(ipAddress, port, password, _logger, gameEncoding);
+ return rconEngine switch
+ {
+ "COD" => new CodRConConnection(ipEndpoint, password,
+ _serviceProvider.GetRequiredService>(), GameEncoding,
+ _serviceProvider.GetRequiredService()?.ServerConnectionAttempts ?? 6),
+ "Source" => new SourceRConConnection(
+ _serviceProvider.GetRequiredService>(),
+ _serviceProvider.GetRequiredService(), ipEndpoint, password),
+ _ => throw new ArgumentException($"No supported RCon engine available for '{rconEngine}'")
+ };
}
}
-}
+}
\ No newline at end of file
diff --git a/Application/Factories/ScriptCommandFactory.cs b/Application/Factories/ScriptCommandFactory.cs
index 8b609144..ce6ea26e 100644
--- a/Application/Factories/ScriptCommandFactory.cs
+++ b/Application/Factories/ScriptCommandFactory.cs
@@ -6,7 +6,9 @@ using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
-using static SharedLibraryCore.Database.Models.EFClient;
+using Data.Models.Client;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace IW4MAdmin.Application.Factories
{
@@ -15,26 +17,30 @@ namespace IW4MAdmin.Application.Factories
///
public class ScriptCommandFactory : IScriptCommandFactory
{
- private CommandConfiguration _config;
+ private readonly CommandConfiguration _config;
private readonly ITranslationLookup _transLookup;
+ private readonly IServiceProvider _serviceProvider;
- public ScriptCommandFactory(CommandConfiguration config, ITranslationLookup transLookup)
+ public ScriptCommandFactory(CommandConfiguration config, ITranslationLookup transLookup, IServiceProvider serviceProvider)
{
_config = config;
_transLookup = transLookup;
+ _serviceProvider = serviceProvider;
}
///
- public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, bool isTargetRequired, IEnumerable<(string, bool)> args, Action executeAction)
+ public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission,
+ bool isTargetRequired, IEnumerable<(string, bool)> args, Action executeAction)
{
- var permissionEnum = Enum.Parse(permission);
+ var permissionEnum = Enum.Parse(permission);
var argsArray = args.Select(_arg => new CommandArgument
{
Name = _arg.Item1,
Required = _arg.Item2
}).ToArray();
- return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction, _config, _transLookup);
+ return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction,
+ _config, _transLookup, _serviceProvider.GetRequiredService>());
}
}
}
diff --git a/Application/GameEventHandler.cs b/Application/GameEventHandler.cs
index 1dfc5282..5bd1a085 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;
+using Microsoft.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application
{
public class GameEventHandler : IEventHandler
{
private readonly EventLog _eventLog;
+ private readonly ILogger _logger;
+ private readonly IEventPublisher _eventPublisher;
private static readonly GameEvent.EventType[] overrideEvents = new[]
{
GameEvent.EventType.Connect,
@@ -22,34 +22,25 @@ namespace IW4MAdmin.Application
GameEvent.EventType.Stop
};
- public GameEventHandler()
+ public GameEventHandler(ILogger logger, IEventPublisher eventPublisher)
{
_eventLog = new EventLog();
+ _logger = logger;
+ _eventPublisher = eventPublisher;
}
public void HandleEvent(IManager manager, GameEvent gameEvent)
{
-#if DEBUG
- ThreadPool.GetMaxThreads(out int workerThreads, out int n);
- ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
- gameEvent.Owner.Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
-
-#endif
if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
{
-#if DEBUG
- gameEvent.Owner.Logger.WriteDebug($"Adding event with id {gameEvent.Id}");
-#endif
-
EventApi.OnGameEvent(gameEvent);
+ _eventPublisher.Publish(gameEvent);
Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent));
}
-#if DEBUG
else
{
- gameEvent.Owner.Logger.WriteDebug($"Skipping event as we're shutting down {gameEvent.Id}");
+ _logger.LogDebug("Skipping event as we're shutting down {eventId}", gameEvent.Id);
}
-#endif
}
}
}
diff --git a/Application/IO/GameLogEventDetection.cs b/Application/IO/GameLogEventDetection.cs
index be9468a8..955a95c5 100644
--- a/Application/IO/GameLogEventDetection.cs
+++ b/Application/IO/GameLogEventDetection.cs
@@ -3,6 +3,9 @@ using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Serilog.Context;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO
{
@@ -12,12 +15,14 @@ namespace IW4MAdmin.Application.IO
private readonly Server _server;
private readonly IGameLogReader _reader;
private readonly bool _ignoreBots;
+ private readonly ILogger _logger;
- public GameLogEventDetection(Server server, Uri[] gameLogUris, IGameLogReaderFactory gameLogReaderFactory)
+ public GameLogEventDetection(ILogger logger, IW4MServer server, Uri[] gameLogUris, IGameLogReaderFactory gameLogReaderFactory)
{
_reader = gameLogReaderFactory.CreateGameLogReader(gameLogUris, server.EventParser);
_server = server;
_ignoreBots = server?.Manager.GetApplicationSettings().Configuration().IgnoreBots ?? false;
+ _logger = logger;
}
public async Task PollForChanges()
@@ -33,15 +38,17 @@ namespace IW4MAdmin.Application.IO
catch (Exception e)
{
- _server.Logger.WriteWarning($"Failed to update log event for {_server.EndPoint}");
- _server.Logger.WriteDebug(e.GetExceptionInfo());
+ using(LogContext.PushProperty("Server", _server.ToString()))
+ {
+ _logger.LogError(e, "Failed to update log event for {endpoint}", _server.EndPoint);
+ }
}
}
await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken);
}
- _server.Logger.WriteDebug("Stopped polling for changes");
+ _logger.LogDebug("Stopped polling for changes");
}
public async Task UpdateLogEvents()
@@ -68,9 +75,6 @@ namespace IW4MAdmin.Application.IO
{
try
{
-#if DEBUG
- _server.Logger.WriteVerbose(gameEvent.Data);
-#endif
gameEvent.Owner = _server;
// we don't want to add the event if ignoreBots is on and the event comes from a bot
@@ -102,10 +106,14 @@ namespace IW4MAdmin.Application.IO
catch (InvalidOperationException)
{
- if (!_ignoreBots)
+ if (_ignoreBots)
{
- _server.Logger.WriteWarning("Could not find client in client list when parsing event line");
- _server.Logger.WriteDebug(gameEvent.Data);
+ continue;
+ }
+
+ using(LogContext.PushProperty("Server", _server.ToString()))
+ {
+ _logger.LogError("Could not find client in client list when parsing event line {data}", gameEvent.Data);
}
}
}
diff --git a/Application/IO/GameLogReader.cs b/Application/IO/GameLogReader.cs
index 764de90a..d1339cd9 100644
--- a/Application/IO/GameLogReader.cs
+++ b/Application/IO/GameLogReader.cs
@@ -6,6 +6,8 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO
{
@@ -19,7 +21,7 @@ namespace IW4MAdmin.Application.IO
public int UpdateInterval => 300;
- public GameLogReader(string logFile, IEventParser parser, ILogger logger)
+ public GameLogReader(string logFile, IEventParser parser, ILogger logger)
{
_logFile = logFile;
_parser = parser;
@@ -73,9 +75,7 @@ namespace IW4MAdmin.Application.IO
catch (Exception e)
{
- _logger.WriteWarning("Could not properly parse event line");
- _logger.WriteDebug(e.Message);
- _logger.WriteDebug(eventLine);
+ _logger.LogError(e, "Could not properly parse event line {@eventLine}", eventLine);
}
}
diff --git a/Application/IO/GameLogReaderHttp.cs b/Application/IO/GameLogReaderHttp.cs
index 822861d0..5662593e 100644
--- a/Application/IO/GameLogReaderHttp.cs
+++ b/Application/IO/GameLogReaderHttp.cs
@@ -6,6 +6,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO
{
@@ -20,7 +22,7 @@ namespace IW4MAdmin.Application.IO
private readonly string _safeLogPath;
private string lastKey = "next";
- public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger logger)
+ public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger logger)
{
_eventParser = parser;
_logServerApi = RestClient.For(gameLogServerUris[0].ToString());
@@ -40,7 +42,7 @@ namespace IW4MAdmin.Application.IO
if (!response.Success && string.IsNullOrEmpty(lastKey))
{
- _logger.WriteError($"Could not get log server info of {_safeLogPath}");
+ _logger.LogError("Could not get log server info of {logPath}", _safeLogPath);
return events;
}
@@ -62,9 +64,7 @@ namespace IW4MAdmin.Application.IO
catch (Exception e)
{
- _logger.WriteError("Could not properly parse event line from http");
- _logger.WriteDebug(e.Message);
- _logger.WriteDebug(eventLine);
+ _logger.LogError(e, "Could not properly parse event line from http {eventLine}", eventLine);
}
}
}
diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs
index 1b5f02a6..525e4369 100644
--- a/Application/IW4MServer.cs
+++ b/Application/IW4MServer.cs
@@ -11,12 +11,20 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Net;
using System.Runtime.InteropServices;
-using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using Data.Abstractions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient;
+using Data.Models;
+using Data.Models.Server;
+using Microsoft.EntityFrameworkCore;
+using static Data.Models.Client.EFClient;
namespace IW4MAdmin
{
@@ -27,40 +35,59 @@ namespace IW4MAdmin
private readonly ITranslationLookup _translationLookup;
private readonly IMetaService _metaService;
private const int REPORT_FLAG_COUNT = 4;
- private int lastGameTime = 0;
+ private long lastGameTime = 0;
public int Id { get; private set; }
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IClientNoticeMessageFormatter _messageFormatter;
+ private readonly ILookupCache _serverCache;
+ private readonly CommandConfiguration _commandConfiguration;
- public IW4MServer(IManager mgr, ServerConfiguration cfg, ITranslationLookup lookup,
- IRConConnectionFactory connectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService) : base(cfg, mgr, connectionFactory, gameLogReaderFactory)
+ public IW4MServer(
+ ServerConfiguration serverConfiguration,
+ CommandConfiguration commandConfiguration,
+ ITranslationLookup lookup,
+ IMetaService metaService,
+ IServiceProvider serviceProvider,
+ IClientNoticeMessageFormatter messageFormatter,
+ ILookupCache serverCache) : base(serviceProvider.GetRequiredService>(),
+ serviceProvider.GetRequiredService(),
+ serverConfiguration,
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService())
{
_translationLookup = lookup;
_metaService = metaService;
+ _serviceProvider = serviceProvider;
+ _messageFormatter = messageFormatter;
+ _serverCache = serverCache;
+ _commandConfiguration = commandConfiguration;
}
- override public async Task OnClientConnected(EFClient clientFromLog)
+ public override async Task OnClientConnected(EFClient clientFromLog)
{
- Logger.WriteDebug($"Client slot #{clientFromLog.ClientNumber} now reserved");
+ ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber);
EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId);
// first time client is connecting to server
if (client == null)
{
- Logger.WriteDebug($"Client {clientFromLog} first time connecting");
+ ServerLogger.LogDebug("Client {client} first time connecting", clientFromLog.ToString());
clientFromLog.CurrentServer = this;
client = await Manager.GetClientService().Create(clientFromLog);
}
- /// this is only a temporary version until the IPAddress is transmitted
+ client.CopyAdditionalProperties(clientFromLog);
+
+ // this is only a temporary version until the IPAddress is transmitted
client.CurrentAlias = new EFAlias()
{
Name = clientFromLog.Name,
IPAddress = clientFromLog.IPAddress
};
- Logger.WriteInfo($"Client {client} connected...");
-
// Do the player specific stuff
client.ClientNumber = clientFromLog.ClientNumber;
client.Score = clientFromLog.Score;
@@ -69,9 +96,7 @@ namespace IW4MAdmin
client.State = ClientState.Connecting;
Clients[client.ClientNumber] = client;
-#if DEBUG == true
- Logger.WriteDebug($"End PreConnect for {client}");
-#endif
+ ServerLogger.LogDebug("End PreConnect for {client}", client.ToString());
var e = new GameEvent()
{
Origin = client,
@@ -83,11 +108,14 @@ namespace IW4MAdmin
return client;
}
- override public async Task OnClientDisconnected(EFClient client)
+ public override async Task OnClientDisconnected(EFClient client)
{
if (!GetClientsAsList().Any(_client => _client.NetworkId == client.NetworkId))
{
- Logger.WriteInfo($"{client} disconnecting, but they are not connected");
+ using (LogContext.PushProperty("Server", ToString()))
+ {
+ ServerLogger.LogWarning("{client} disconnecting, but they are not connected", client.ToString());
+ }
return;
}
@@ -95,7 +123,7 @@ namespace IW4MAdmin
if (client.ClientNumber >= 0)
{
#endif
- Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting...");
+ ServerLogger.LogDebug("Client {@client} disconnecting...", new { client=client.ToString(), client.State });
Clients[client.ClientNumber] = null;
await client.OnDisconnect();
@@ -114,124 +142,113 @@ namespace IW4MAdmin
public override async Task ExecuteEvent(GameEvent E)
{
- if (E == null)
- {
- Logger.WriteError("Received NULL event");
- return;
- }
-
- if (E.IsBlocking)
- {
- await E.Origin?.Lock();
- }
-
- bool canExecuteCommand = true;
- Exception lastException = null;
-
- try
- {
- if (!await ProcessEvent(E))
- {
- return;
- }
-
- Command C = null;
- if (E.Type == GameEvent.EventType.Command)
- {
- try
- {
- C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration());
- }
-
- catch (CommandException e)
- {
- Logger.WriteInfo(e.Message);
- E.FailReason = GameEvent.EventFailReason.Invalid;
- }
-
- if (C != null)
- {
- E.Extra = C;
- }
- }
-
- try
- {
- var loginPlugin = Manager.Plugins.FirstOrDefault(_plugin => _plugin.Name == "Login");
-
- if (loginPlugin != null)
- {
- await loginPlugin.OnEventAsync(E, this);
- }
- }
-
- catch (AuthorizationException e)
- {
- E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
- canExecuteCommand = false;
- }
-
- // hack: this prevents commands from getting executing that 'shouldn't' be
- if (E.Type == GameEvent.EventType.Command && E.Extra is Command command &&
- (canExecuteCommand || E.Origin?.Level == Permission.Console))
- {
- await command.ExecuteAsync(E);
- }
-
- var pluginTasks = Manager.Plugins.Where(_plugin => _plugin.Name != "Login").Select(async _plugin =>
- {
- try
- {
- // we don't want to run the events on parser plugins
- if (_plugin is ScriptPlugin scriptPlugin && scriptPlugin.IsParser)
- {
- return;
- }
-
- using (var tokenSource = new CancellationTokenSource())
- {
- tokenSource.CancelAfter(Utilities.DefaultCommandTimeout);
- await (_plugin.OnEventAsync(E, this)).WithWaitCancellation(tokenSource.Token);
- }
- }
- catch (Exception Except)
- {
- Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{_plugin.Name}]");
- Logger.WriteDebug(Except.GetExceptionInfo());
- }
- }).ToArray();
-
- if (pluginTasks.Any())
- {
- await Task.WhenAny(pluginTasks);
- }
- }
-
- catch (Exception e)
- {
- lastException = e;
-
- if (E.Origin != null && E.Type == GameEvent.EventType.Command)
- {
- E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
- }
- }
-
- finally
+ using (LogContext.PushProperty("Server", ToString()))
{
if (E.IsBlocking)
{
- E.Origin?.Unlock();
+ await E.Origin?.Lock();
}
- if (lastException != null)
+ bool canExecuteCommand = true;
+
+ try
{
- bool notifyDisconnects = !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost;
- if (notifyDisconnects || (!notifyDisconnects && lastException as NetworkException == null))
+ if (!await ProcessEvent(E))
{
- throw lastException;
+ return;
+ }
+
+ Command C = null;
+ if (E.Type == GameEvent.EventType.Command)
+ {
+ try
+ {
+ C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
+ }
+
+ catch (CommandException e)
+ {
+ ServerLogger.LogWarning(e, "Error validating command from event {@event}",
+ new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId });
+ E.FailReason = GameEvent.EventFailReason.Invalid;
+ }
+
+ if (C != null)
+ {
+ E.Extra = C;
+ }
+ }
+
+ try
+ {
+ var loginPlugin = Manager.Plugins.FirstOrDefault(_plugin => _plugin.Name == "Login");
+
+ if (loginPlugin != null)
+ {
+ await loginPlugin.OnEventAsync(E, this);
+ }
+ }
+
+ catch (AuthorizationException e)
+ {
+ E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
+ canExecuteCommand = false;
+ }
+
+ // hack: this prevents commands from getting executing that 'shouldn't' be
+ if (E.Type == GameEvent.EventType.Command && E.Extra is Command command &&
+ (canExecuteCommand || E.Origin?.Level == Permission.Console))
+ {
+ ServerLogger.LogInformation("Executing command {comamnd} for {client}", command.Name, E.Origin.ToString());
+ await command.ExecuteAsync(E);
+ }
+
+ var pluginTasks = Manager.Plugins
+ .Where(_plugin => _plugin.Name != "Login")
+ .Select(async plugin => await CreatePluginTask(plugin, E));
+
+ await Task.WhenAll(pluginTasks);
+ }
+
+ catch (Exception e)
+ {
+ ServerLogger.LogError(e, "Unexpected exception occurred processing event");
+ if (E.Origin != null && E.Type == GameEvent.EventType.Command)
+ {
+ E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
}
}
+
+ finally
+ {
+ if (E.IsBlocking)
+ {
+ E.Origin?.Unlock();
+ }
+ }
+ }
+ }
+
+ private async Task CreatePluginTask(IPlugin plugin, GameEvent gameEvent)
+ {
+ // we don't want to run the events on parser plugins
+ if (plugin is ScriptPlugin scriptPlugin && scriptPlugin.IsParser)
+ {
+ return;
+ }
+
+ using var tokenSource = new CancellationTokenSource();
+ tokenSource.CancelAfter(Utilities.DefaultCommandTimeout);
+
+ try
+ {
+ await (plugin.OnEventAsync(gameEvent, this)).WithWaitCancellation(tokenSource.Token);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(loc["SERVER_PLUGIN_ERROR"]);
+ ServerLogger.LogError(ex, "Could not execute {methodName} for plugin {plugin}",
+ nameof(plugin.OnEventAsync), plugin.Name);
}
}
@@ -240,403 +257,470 @@ namespace IW4MAdmin
///
///
///
- override protected async Task ProcessEvent(GameEvent E)
+ protected override async Task ProcessEvent(GameEvent E)
{
-#if DEBUG
- Logger.WriteDebug($"processing event of type {E.Type}");
-#endif
-
- if (E.Type == GameEvent.EventType.ConnectionLost)
+ using (LogContext.PushProperty("Server", ToString()))
+ using (LogContext.PushProperty("EventType", E.Type))
{
- var exception = E.Extra as Exception;
- if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
+ ServerLogger.LogDebug("processing event of type {type}", E.Type);
+
+ if (E.Type == GameEvent.EventType.Start)
{
- Logger.WriteError(exception.Message);
- if (exception.Data["internal_exception"] != null)
+ var existingServer = (await _serverCache
+ .FirstAsync(server => server.Id == EndPoint));
+
+ var serverId = await GetIdForServer(E.Owner);
+
+ if (existingServer == null)
{
- Logger.WriteDebug($"Internal Exception: {exception.Data["internal_exception"]}");
+ var server = new EFServer()
+ {
+ Port = Port,
+ EndPoint = ToString(),
+ ServerId = serverId,
+ GameName = (Reference.Game?)GameName,
+ HostName = Hostname
+ };
+
+ await _serverCache.AddAsync(server);
}
}
- Logger.WriteInfo("Connection lost to server, so we are throttling the poll rate");
- Throttled = true;
- }
-
- if (E.Type == GameEvent.EventType.ConnectionRestored)
- {
- if (Throttled && !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
+
+ if (E.Type == GameEvent.EventType.ConnectionLost)
{
- Logger.WriteVerbose(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
- }
- Logger.WriteInfo("Connection restored to server, so we are no longer throttling the poll rate");
- Throttled = false;
- }
+ var exception = E.Extra as Exception;
+ ServerLogger.LogError(exception,
+ "Connection lost with {server}", ToString());
- if (E.Type == GameEvent.EventType.ChangePermission)
- {
- var newPermission = (Permission)E.Extra;
- Logger.WriteInfo($"{E.Origin} is setting {E.Target} to permission level {newPermission}");
- await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin);
- }
-
- else if (E.Type == GameEvent.EventType.Connect)
- {
- if (E.Origin.State != ClientState.Connected)
- {
- E.Origin.State = ClientState.Connected;
- E.Origin.LastConnection = DateTime.UtcNow;
- E.Origin.Connections += 1;
-
- ChatHistory.Add(new ChatInfo()
+ if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
- Name = E.Origin.Name,
- Message = "CONNECTED",
- Time = DateTime.UtcNow
- });
-
- await E.Origin.OnJoin(E.Origin.IPAddress);
- }
- }
-
- else if (E.Type == GameEvent.EventType.PreConnect)
- {
- // we don't want to track bots in the database at all if ignore bots is requested
- if (E.Origin.IsBot && Manager.GetApplicationSettings().Configuration().IgnoreBots)
- {
- return false;
- }
-
- if (E.Origin.CurrentServer == null)
- {
- Logger.WriteWarning($"preconnecting client {E.Origin} did not have a current server specified");
- E.Origin.CurrentServer = this;
- }
-
- var existingClient = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
-
- // they're already connected
- if (existingClient != null && existingClient.ClientNumber == E.Origin.ClientNumber && !E.Origin.IsBot)
- {
- Logger.WriteWarning($"detected preconnect for {E.Origin}, but they are already connected");
- return false;
- }
-
- // 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)
- // 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
- await OnClientDisconnected(Clients[existingClient.ClientNumber]);
- }
-
- if (Clients[E.Origin.ClientNumber] == null)
- {
-#if DEBUG == true
- Logger.WriteDebug($"Begin PreConnect for {E.Origin}");
-#endif
- // we can go ahead and put them in so that they don't get re added
- Clients[E.Origin.ClientNumber] = E.Origin;
- try
- {
- E.Origin = await OnClientConnected(E.Origin);
- E.Target = E.Origin;
+ Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
}
- catch (Exception ex)
- {
- Logger.WriteError($"{loc["SERVER_ERROR_ADDPLAYER"]} {E.Origin}");
- Logger.WriteDebug(ex.GetExceptionInfo());
+ Throttled = true;
+ }
- Clients[E.Origin.ClientNumber] = null;
+ if (E.Type == GameEvent.EventType.ConnectionRestored)
+ {
+ ServerLogger.LogInformation(
+ "Connection restored with {server}", ToString());
+
+ if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
+ {
+ Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
+ }
+
+ if (!string.IsNullOrEmpty(CustomSayName))
+ {
+ await this.SetDvarAsync("sv_sayname", CustomSayName);
+ }
+
+ Throttled = false;
+ }
+
+ if (E.Type == GameEvent.EventType.ChangePermission)
+ {
+ var newPermission = (Permission) E.Extra;
+ ServerLogger.LogInformation("{origin} is setting {target} to permission level {newPermission}",
+ E.Origin.ToString(), E.Target.ToString(), newPermission);
+ await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin);
+ }
+
+ else if (E.Type == GameEvent.EventType.Connect)
+ {
+ if (E.Origin.State != ClientState.Connected)
+ {
+ E.Origin.State = ClientState.Connected;
+ E.Origin.LastConnection = DateTime.UtcNow;
+ E.Origin.Connections += 1;
+
+ ChatHistory.Add(new ChatInfo()
+ {
+ Name = E.Origin.Name,
+ Message = "CONNECTED",
+ Time = DateTime.UtcNow
+ });
+
+ var clientTag = await _metaService.GetPersistentMeta(EFMeta.ClientTag, E.Origin);
+
+ if (clientTag?.LinkedMeta != null)
+ {
+ E.Origin.Tag = clientTag.LinkedMeta.Value;
+ }
+
+ try
+ {
+ var factory = _serviceProvider.GetRequiredService();
+ await using var context = factory.CreateContext();
+
+ var messageCount = await context.InboxMessages
+ .CountAsync(msg => msg.DestinationClientId == E.Origin.ClientId && !msg.IsDelivered);
+
+ if (messageCount > 0)
+ {
+ E.Origin.Tell(_translationLookup["SERVER_JOIN_OFFLINE_MESSAGES"]);
+ }
+ }
+ catch (Exception ex)
+ {
+ ServerLogger.LogError(ex, "Could not get offline message count for {Client}", E.Origin.ToString());
+ throw;
+ }
+
+ await E.Origin.OnJoin(E.Origin.IPAddress, Manager.GetApplicationSettings().Configuration().EnableImplicitAccountLinking);
+ }
+ }
+
+ else if (E.Type == GameEvent.EventType.PreConnect)
+ {
+ ServerLogger.LogInformation("Detected PreConnect for {client} from {source}", E.Origin.ToString(), E.Source);
+ // we don't want to track bots in the database at all if ignore bots is requested
+ if (E.Origin.IsBot && Manager.GetApplicationSettings().Configuration().IgnoreBots)
+ {
return false;
}
- if (E.Origin.Level > Permission.Moderator)
+ if (E.Origin.CurrentServer == null)
{
- E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
+ ServerLogger.LogWarning("Preconnecting client {client} did not have a current server specified",
+ E.Origin.ToString());
+ E.Origin.CurrentServer = this;
}
- }
- // for some reason there's still a client in the spot
- else
- {
- Logger.WriteWarning($"{E.Origin} is connecting but {Clients[E.Origin.ClientNumber]} is currently in that client slot");
- }
- }
+ var existingClient = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
- else if (E.Type == GameEvent.EventType.Flag)
- {
- DateTime? expires = null;
-
- if (E.Extra is TimeSpan ts)
- {
- expires = DateTime.UtcNow + ts;
- }
-
- // todo: maybe move this to a seperate function
- var newPenalty = new EFPenalty()
- {
- Type = EFPenalty.PenaltyType.Flag,
- Expires = expires,
- Offender = E.Target,
- Offense = E.Data,
- Punisher = E.ImpersonationOrigin ?? E.Origin,
- When = DateTime.UtcNow,
- Link = E.Target.AliasLink
- };
-
- var addedPenalty = await Manager.GetPenaltyService().Create(newPenalty);
- E.Target.SetLevel(Permission.Flagged, E.Origin);
- }
-
- else if (E.Type == GameEvent.EventType.Unflag)
- {
- var unflagPenalty = new EFPenalty()
- {
- Type = EFPenalty.PenaltyType.Unflag,
- Expires = DateTime.UtcNow,
- Offender = E.Target,
- Offense = E.Data,
- Punisher = E.ImpersonationOrigin ?? E.Origin,
- When = DateTime.UtcNow,
- Link = E.Target.AliasLink
- };
-
- E.Target.SetLevel(Permission.User, E.Origin);
- await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId);
- await Manager.GetPenaltyService().Create(unflagPenalty);
- }
-
- else if (E.Type == GameEvent.EventType.Report)
- {
- Reports.Add(new Report()
- {
- Origin = E.Origin,
- Target = E.Target,
- Reason = E.Data
- });
-
- var newReport = new EFPenalty()
- {
- Type = EFPenalty.PenaltyType.Report,
- Expires = DateTime.UtcNow,
- Offender = E.Target,
- Offense = E.Message,
- Punisher = E.ImpersonationOrigin ?? E.Origin,
- Active = true,
- When = DateTime.UtcNow,
- Link = E.Target.AliasLink
- };
-
- await Manager.GetPenaltyService().Create(newReport);
-
- int reportNum = await Manager.GetClientService().GetClientReportCount(E.Target.ClientId);
- bool isAutoFlagged = await Manager.GetClientService().IsAutoFlagged(E.Target.ClientId);
-
- if (!E.Target.IsPrivileged() && reportNum >= REPORT_FLAG_COUNT && !isAutoFlagged)
- {
- E.Target.Flag(Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"].FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner));
- }
- }
-
- else if (E.Type == GameEvent.EventType.TempBan)
- {
- await TempBan(E.Data, (TimeSpan)E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin); ;
- }
-
- else if (E.Type == GameEvent.EventType.Ban)
- {
- bool isEvade = E.Extra != null ? (bool)E.Extra : false;
- await Ban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin, isEvade);
- }
-
- else if (E.Type == GameEvent.EventType.Unban)
- {
- await Unban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin);
- }
-
- else if (E.Type == GameEvent.EventType.Kick)
- {
- await Kick(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin);
- }
-
- else if (E.Type == GameEvent.EventType.Warn)
- {
- await Warn(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin);
- }
-
- else if (E.Type == GameEvent.EventType.Disconnect)
- {
- ChatHistory.Add(new ChatInfo()
- {
- Name = E.Origin.Name,
- Message = "DISCONNECTED",
- Time = DateTime.UtcNow
- });
-
- await _metaService.AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin);
- await _metaService.AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin);
- }
-
- else if (E.Type == GameEvent.EventType.PreDisconnect)
- {
- bool isPotentialFalseQuit = E.GameTime.HasValue && E.GameTime.Value == lastGameTime;
-
- if (isPotentialFalseQuit)
- {
- Logger.WriteInfo($"Receive predisconnect event for {E.Origin}, but it occured at game time {E.GameTime.Value}, which is the same last map change, so we're ignoring");
- return false;
- }
-
- // predisconnect comes from minimal rcon polled players and minimal log players
- // so we need to disconnect the "full" version of the client
- var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
-
- if (client == null)
- {
- Logger.WriteWarning($"Client {E.Origin} detected as disconnecting, but could not find them in the player list");
- return false;
- }
-
- else if (client.State != ClientState.Unknown)
- {
-#if DEBUG == true
- Logger.WriteDebug($"Begin PreDisconnect for {client}");
-#endif
- await OnClientDisconnected(client);
-#if DEBUG == true
- Logger.WriteDebug($"End PreDisconnect for {client}");
-#endif
- return true;
- }
-
- else
- {
- Logger.WriteWarning($"Expected disconnecting client {client} to be in state {ClientState.Connected.ToString()}, but is in state {client.State}");
- return false;
- }
- }
-
- else if (E.Type == GameEvent.EventType.Update)
- {
-#if DEBUG == true
- Logger.WriteDebug($"Begin Update for {E.Origin}");
-#endif
- await OnClientUpdate(E.Origin);
- }
-
- if (E.Type == GameEvent.EventType.Say)
- {
- if (E.Data?.Length > 0)
- {
- string message = E.Data;
- if (E.Data.IsQuickMessage())
+ // they're already connected
+ if (existingClient != null && existingClient.ClientNumber == E.Origin.ClientNumber &&
+ !E.Origin.IsBot)
{
+ ServerLogger.LogInformation("{client} is already connected, so we are ignoring their PreConnect",
+ E.Origin.ToString());
+ return false;
+ }
+
+ // 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)
+ // bots can have duplicate names which causes conflicting GUIDs
+ if (existingClient != null && existingClient.ClientNumber != E.Origin.ClientNumber &&
+ !E.Origin.IsBot)
+ {
+ ServerLogger.LogWarning(
+ "client {client} is trying to connect in client slot {newClientSlot}, but they are already registered in client slot {oldClientSlot}, swapping...",
+ E.Origin.ToString(), E.Origin.ClientNumber, existingClient.ClientNumber);
+ // we need to remove them so the client spots can swap
+ await OnClientDisconnected(Clients[existingClient.ClientNumber]);
+ }
+
+ if (Clients[E.Origin.ClientNumber] == null)
+ {
+ ServerLogger.LogDebug("Begin PreConnect for {origin}", E.Origin.ToString());
+ // we can go ahead and put them in so that they don't get re added
+ Clients[E.Origin.ClientNumber] = E.Origin;
try
{
- message = Manager.GetApplicationSettings().Configuration()
- .QuickMessages
- .First(_qm => _qm.Game == GameName)
- .Messages[E.Data.Substring(1)];
+ E.Origin = await OnClientConnected(E.Origin);
+ E.Target = E.Origin;
}
- catch
+
+ catch (Exception ex)
{
- message = E.Data.Substring(1);
+ Console.WriteLine($"{loc["SERVER_ERROR_ADDPLAYER"]} {E.Origin}");
+ ServerLogger.LogError(ex, "Could not add player {player}", E.Origin.ToString());
+ Clients[E.Origin.ClientNumber] = null;
+ return false;
+ }
+
+ if (E.Origin.Level > Permission.Moderator)
+ {
+ E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
}
}
+ // for some reason there's still a client in the spot
+ else
+ {
+ ServerLogger.LogWarning(
+ "{origin} is connecting but {existingClient} is currently in that client slot",
+ E.Origin.ToString(), Clients[E.Origin.ClientNumber].ToString());
+ }
+ }
+
+ else if (E.Type == GameEvent.EventType.Flag)
+ {
+ DateTime? expires = null;
+
+ if (E.Extra is TimeSpan ts)
+ {
+ expires = DateTime.UtcNow + ts;
+ }
+
+ // todo: maybe move this to a seperate function
+ var newPenalty = new EFPenalty()
+ {
+ Type = EFPenalty.PenaltyType.Flag,
+ Expires = expires,
+ Offender = E.Target,
+ Offense = E.Data,
+ Punisher = E.ImpersonationOrigin ?? E.Origin,
+ When = DateTime.UtcNow,
+ Link = E.Target.AliasLink
+ };
+
+ await Manager.GetPenaltyService().Create(newPenalty);
+ E.Target.SetLevel(Permission.Flagged, E.Origin);
+ }
+
+ else if (E.Type == GameEvent.EventType.Unflag)
+ {
+ var unflagPenalty = new EFPenalty()
+ {
+ Type = EFPenalty.PenaltyType.Unflag,
+ Expires = DateTime.UtcNow,
+ Offender = E.Target,
+ Offense = E.Data,
+ Punisher = E.ImpersonationOrigin ?? E.Origin,
+ When = DateTime.UtcNow,
+ Link = E.Target.AliasLink
+ };
+
+ E.Target.SetLevel(Permission.User, E.Origin);
+ await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId);
+ await Manager.GetPenaltyService().Create(unflagPenalty);
+ }
+
+ else if (E.Type == GameEvent.EventType.Report)
+ {
+ Reports.Add(new Report()
+ {
+ Origin = E.Origin,
+ Target = E.Target,
+ Reason = E.Data
+ });
+
+ var newReport = new EFPenalty()
+ {
+ Type = EFPenalty.PenaltyType.Report,
+ Expires = DateTime.UtcNow,
+ Offender = E.Target,
+ Offense = E.Message,
+ Punisher = E.ImpersonationOrigin ?? E.Origin,
+ Active = true,
+ When = DateTime.UtcNow,
+ Link = E.Target.AliasLink
+ };
+
+ await Manager.GetPenaltyService().Create(newReport);
+
+ var reportNum = await Manager.GetClientService().GetClientReportCount(E.Target.ClientId);
+ var canBeAutoFlagged = await Manager.GetClientService().CanBeAutoFlagged(E.Target.ClientId);
+
+ if (!E.Target.IsPrivileged() && reportNum >= REPORT_FLAG_COUNT && canBeAutoFlagged)
+ {
+ E.Target.Flag(
+ Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"]
+ .FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner));
+ }
+ }
+
+ else if (E.Type == GameEvent.EventType.TempBan)
+ {
+ await TempBan(E.Data, (TimeSpan) E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin);
+ }
+
+ else if (E.Type == GameEvent.EventType.Ban)
+ {
+ bool isEvade = E.Extra != null ? (bool) E.Extra : false;
+ await Ban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin, isEvade);
+ }
+
+ else if (E.Type == GameEvent.EventType.Unban)
+ {
+ await Unban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin);
+ }
+
+ else if (E.Type == GameEvent.EventType.Kick)
+ {
+ await Kick(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin, E.Extra as EFPenalty);
+ }
+
+ else if (E.Type == GameEvent.EventType.Warn)
+ {
+ await Warn(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin);
+ }
+
+ else if (E.Type == GameEvent.EventType.Disconnect)
+ {
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
- Message = message,
- Time = DateTime.UtcNow,
- IsHidden = !string.IsNullOrEmpty(GamePassword)
+ Message = "DISCONNECTED",
+ Time = DateTime.UtcNow
});
+
+ await _metaService.AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin);
+ await _metaService.AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin);
}
- }
- if (E.Type == GameEvent.EventType.MapChange)
- {
- Logger.WriteInfo($"New map loaded - {ClientNum} active players");
-
- // iw4 doesn't log the game info
- if (E.Extra == null)
+ else if (E.Type == GameEvent.EventType.PreDisconnect)
{
- var dict = await this.GetInfoAsync(new TimeSpan(0, 0, 20));
+ ServerLogger.LogInformation("Detected PreDisconnect for {client} from {source}",
+ E.Origin.ToString(), E.Source);
+ bool isPotentialFalseQuit = E.GameTime.HasValue && E.GameTime.Value == lastGameTime;
- if (dict == null)
+ if (isPotentialFalseQuit)
{
- Logger.WriteWarning("Map change event response doesn't have any data");
+ ServerLogger.LogDebug(
+ "Received PreDisconnect event for {origin}, but it occured at game time {gameTime}, which is the same last map change, so we're ignoring",
+ E.Origin.ToString(), E.GameTime);
+ return false;
+ }
+
+ // predisconnect comes from minimal rcon polled players and minimal log players
+ // so we need to disconnect the "full" version of the client
+ var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
+
+ if (client == null)
+ {
+ // this can happen when the status picks up the connect before the log does
+ ServerLogger.LogInformation(
+ "Ignoring PreDisconnect for {origin} because they are no longer on the client list",
+ E.Origin.ToString());
+ return false;
+ }
+
+ else if (client.State != ClientState.Unknown)
+ {
+ await OnClientDisconnected(client);
+ return true;
}
else
{
- Gametype = dict["gametype"];
- Hostname = dict["hostname"];
-
- string mapname = dict["mapname"] ?? CurrentMap.Name;
- UpdateMap(mapname);
+ ServerLogger.LogWarning(
+ "Expected disconnecting client {client} to be in state {state}, but is in state {clientState}",
+ client.ToString(), ClientState.Connected.ToString(), client.State);
+ return false;
}
}
- else
+ else if (E.Type == GameEvent.EventType.Update)
{
- var dict = (Dictionary)E.Extra;
- Gametype = dict["g_gametype"];
- Hostname = dict["sv_hostname"];
- MaxClients = int.Parse(dict["sv_maxclients"]);
-
- string mapname = dict["mapname"];
- UpdateMap(mapname);
+ ServerLogger.LogDebug("Begin Update for {origin}", E.Origin.ToString());
+ await OnClientUpdate(E.Origin);
}
- if (E.GameTime.HasValue)
+ if (E.Type == GameEvent.EventType.Say)
{
- lastGameTime = E.GameTime.Value;
+ if (E.Data?.Length > 0)
+ {
+ string message = E.Data;
+ if (E.Data.IsQuickMessage())
+ {
+ try
+ {
+ message = _serviceProvider.GetRequiredService()
+ .QuickMessages
+ .First(_qm => _qm.Game == GameName)
+ .Messages[E.Data.Substring(1)];
+ }
+ catch
+ {
+ message = E.Data.Substring(1);
+ }
+ }
+
+ ChatHistory.Add(new ChatInfo()
+ {
+ Name = E.Origin.Name,
+ Message = message,
+ Time = DateTime.UtcNow,
+ IsHidden = !string.IsNullOrEmpty(GamePassword)
+ });
+ }
}
- }
- if (E.Type == GameEvent.EventType.MapEnd)
- {
- Logger.WriteInfo("Game ending...");
-
- if (E.GameTime.HasValue)
+ if (E.Type == GameEvent.EventType.MapChange)
{
- lastGameTime = E.GameTime.Value;
+ ServerLogger.LogInformation("New map loaded - {clientCount} active players", ClientNum);
+
+ // iw4 doesn't log the game info
+ if (E.Extra == null)
+ {
+ var dict = await this.GetInfoAsync(new TimeSpan(0, 0, 20));
+
+ if (dict == null)
+ {
+ ServerLogger.LogWarning("Map change event response doesn't have any data");
+ }
+
+ else
+ {
+ Gametype = dict["gametype"];
+ Hostname = dict["hostname"];
+
+ string mapname = dict["mapname"] ?? CurrentMap.Name;
+ UpdateMap(mapname);
+ }
+ }
+
+ else
+ {
+ var dict = (Dictionary) E.Extra;
+ Gametype = dict["g_gametype"];
+ Hostname = dict["sv_hostname"];
+ MaxClients = int.Parse(dict["sv_maxclients"]);
+
+ string mapname = dict["mapname"];
+ UpdateMap(mapname);
+ }
+
+ if (E.GameTime.HasValue)
+ {
+ lastGameTime = E.GameTime.Value;
+ }
}
- }
- if (E.Type == GameEvent.EventType.Tell)
- {
- await Tell(E.Message, E.Target);
- }
-
- if (E.Type == GameEvent.EventType.Broadcast)
- {
- if (!Utilities.IsDevelopment && E.Data != null) // hides broadcast when in development mode
+ if (E.Type == GameEvent.EventType.MapEnd)
{
- await E.Owner.ExecuteCommandAsync(E.Data);
- }
- }
+ ServerLogger.LogInformation("Game ending...");
- lock (ChatHistory)
- {
- while (ChatHistory.Count > Math.Ceiling(ClientNum / 2.0))
+ if (E.GameTime.HasValue)
+ {
+ lastGameTime = E.GameTime.Value;
+ }
+ }
+
+ if (E.Type == GameEvent.EventType.Tell)
{
- ChatHistory.RemoveAt(0);
+ await Tell(E.Message, E.Target);
}
- }
- // the last client hasn't fully disconnected yet
- // so there will still be at least 1 client left
- if (ClientNum < 2)
- {
- ChatHistory.Clear();
- }
+ if (E.Type == GameEvent.EventType.Broadcast)
+ {
+ if (!Utilities.IsDevelopment && E.Data != null) // hides broadcast when in development mode
+ {
+ await E.Owner.ExecuteCommandAsync(E.Data);
+ }
+ }
- return true;
+ lock (ChatHistory)
+ {
+ while (ChatHistory.Count > Math.Ceiling(ClientNum / 2.0))
+ {
+ ChatHistory.RemoveAt(0);
+ }
+ }
+
+ // the last client hasn't fully disconnected yet
+ // so there will still be at least 1 client left
+ if (ClientNum < 2)
+ {
+ ChatHistory.Clear();
+ }
+
+ return true;
+ }
}
private async Task OnClientUpdate(EFClient origin)
@@ -645,7 +729,7 @@ namespace IW4MAdmin
if (client == null)
{
- Logger.WriteWarning($"{origin} expected to exist in client list for update, but they do not");
+ ServerLogger.LogWarning("{origin} expected to exist in client list for update, but they do not", origin.ToString());
return;
}
@@ -659,21 +743,23 @@ namespace IW4MAdmin
{
try
{
- await client.OnJoin(origin.IPAddress);
+ await client.OnJoin(origin.IPAddress, Manager.GetApplicationSettings().Configuration().EnableImplicitAccountLinking);
}
catch (Exception e)
{
- Logger.WriteWarning($"Could not execute on join for {origin}");
- Logger.WriteDebug(e.GetExceptionInfo());
+ using(LogContext.PushProperty("Server", ToString()))
+ {
+ ServerLogger.LogError(e, "Could not execute on join for {origin}", origin.ToString());
+ }
}
}
else if ((client.IPAddress != null && client.State == ClientState.Disconnecting) ||
client.Level == Permission.Banned)
{
- Logger.WriteWarning($"{client} state is Unknown (probably kicked), but they are still connected. trying to kick again...");
- await client.CanConnect(client.IPAddress);
+ ServerLogger.LogWarning("{client} state is Unknown (probably kicked), but they are still connected. trying to kick again...", origin.ToString());
+ await client.CanConnect(client.IPAddress, Manager.GetApplicationSettings().Configuration().EnableImplicitAccountLinking);
}
}
@@ -684,36 +770,53 @@ namespace IW4MAdmin
/// array index 2 = updated clients
///
///
- async Task[]> PollPlayersAsync()
+ async Task[]> PollPlayersAsync()
{
-#if DEBUG
- var now = DateTime.Now;
-#endif
var currentClients = GetClientsAsList();
var statusResponse = (await this.GetStatusAsync());
- var polledClients = statusResponse.Item1.AsEnumerable();
+ var polledClients = statusResponse.Clients.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
{
polledClients = polledClients.Where(c => !c.IsBot);
}
-#if DEBUG
- Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms");
-#endif
var disconnectingClients = currentClients.Except(polledClients);
var connectingClients = polledClients.Except(currentClients);
var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients);
- UpdateMap(statusResponse.Item2);
- UpdateGametype(statusResponse.Item3);
+ UpdateMap(statusResponse.Map);
+ UpdateGametype(statusResponse.GameType);
+ UpdateHostname(statusResponse.Hostname);
+ UpdateMaxPlayers(statusResponse.MaxClients);
- return new List[]
+ return new []
{
connectingClients.ToList(),
disconnectingClients.ToList(),
updatedClients.ToList()
};
}
+
+ public override async Task GetIdForServer(Server server = null)
+ {
+ server ??= this;
+
+ if ($"{server.IP}:{server.Port.ToString()}" == "66.150.121.184:28965")
+ {
+ return 886229536;
+ }
+
+ // todo: this is not stable and will need to be migrated again...
+ long id = HashCode.Combine(server.IP, server.Port);
+ id = id < 0 ? Math.Abs(id) : id;
+
+ var serverId = (await _serverCache
+ .FirstAsync(_server => _server.ServerId == server.EndPoint ||
+ _server.EndPoint == server.ToString() ||
+ _server.ServerId == id))?.ServerId;
+
+ return !serverId.HasValue ? id : serverId.Value;
+ }
private void UpdateMap(string mapname)
{
@@ -735,6 +838,36 @@ namespace IW4MAdmin
}
}
+ private void UpdateHostname(string hostname)
+ {
+ if (string.IsNullOrEmpty(hostname) || Hostname == hostname)
+ {
+ return;
+ }
+
+ using(LogContext.PushProperty("Server", ToString()))
+ {
+ ServerLogger.LogDebug("Updating hostname to {HostName}", hostname);
+ }
+
+ Hostname = hostname;
+ }
+
+ private void UpdateMaxPlayers(int? maxPlayers)
+ {
+ if (maxPlayers == null || maxPlayers == MaxClients)
+ {
+ return;
+ }
+
+ using(LogContext.PushProperty("Server", ToString()))
+ {
+ ServerLogger.LogDebug("Updating max clients to {MaxPlayers}", maxPlayers);
+ }
+
+ MaxClients = maxPlayers.Value;
+ }
+
private async Task ShutdownInternal()
{
foreach (var client in GetClientsAsList())
@@ -763,9 +896,8 @@ namespace IW4MAdmin
DateTime playerCountStart = DateTime.Now;
DateTime lastCount = DateTime.Now;
- override public async Task ProcessUpdatesAsync(CancellationToken cts)
+ public override async Task ProcessUpdatesAsync(CancellationToken cts)
{
- bool notifyDisconnects = !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost;
try
{
if (cts.IsCancellationRequested)
@@ -776,12 +908,10 @@ namespace IW4MAdmin
try
{
-#if DEBUG
- if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue)
+ if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue && Utilities.IsDevelopment)
{
return true;
}
-#endif
var polledClients = await PollPlayersAsync();
@@ -838,7 +968,7 @@ namespace IW4MAdmin
Manager.AddEvent(e);
}
- if (ConnectionErrors > 0)
+ if (Throttled)
{
var _event = new GameEvent()
{
@@ -851,14 +981,12 @@ namespace IW4MAdmin
Manager.AddEvent(_event);
}
- ConnectionErrors = 0;
LastPoll = DateTime.Now;
}
catch (NetworkException e)
{
- ConnectionErrors++;
- if (ConnectionErrors == 3)
+ if (!Throttled)
{
var _event = new GameEvent()
{
@@ -872,16 +1000,20 @@ namespace IW4MAdmin
Manager.AddEvent(_event);
}
+
return true;
}
LastMessage = DateTime.Now - start;
lastCount = DateTime.Now;
+ var appConfig = _serviceProvider.GetService();
// update the player history
- if ((lastCount - playerCountStart).TotalMinutes >= PlayerHistory.UpdateInterval)
+ if (lastCount - playerCountStart >= appConfig.ServerDataCollectionInterval)
{
- while (ClientHistory.Count > ((60 / PlayerHistory.UpdateInterval) * 12)) // 12 times a hour for 12 hours
+ var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes /
+ appConfig.ServerDataCollectionInterval.TotalMinutes);
+ while ( ClientHistory.Count > maxItems)
{
ClientHistory.Dequeue();
}
@@ -916,44 +1048,53 @@ namespace IW4MAdmin
}
// this one is ok
- catch (ServerException e)
+ catch (Exception e) when(e is ServerException || e is RConException)
{
- if (e is NetworkException && !Throttled && notifyDisconnects)
+ using(LogContext.PushProperty("Server", ToString()))
{
- Logger.WriteError(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
- Logger.WriteDebug(e.GetExceptionInfo());
- }
- else
- {
- Logger.WriteError(e.Message);
+ ServerLogger.LogWarning(e, "Undesirable exception occured during processing updates");
}
return false;
}
- catch (Exception E)
+ catch (Exception e)
{
- Logger.WriteError(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]"));
- Logger.WriteDebug(E.GetExceptionInfo());
+ using(LogContext.PushProperty("Server", ToString()))
+ {
+ ServerLogger.LogError(e, "Unexpected exception occured during processing updates");
+ }
+ Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]"));
return false;
}
}
public async Task Initialize()
{
+ try
+ {
+ ResolvedIpEndPoint = new IPEndPoint((await Dns.GetHostAddressesAsync(IP)).First(), Port);
+ }
+ catch (Exception ex)
+ {
+ ServerLogger.LogWarning(ex, "Could not resolve hostname or IP for RCon connection {IP}:{Port}", IP, Port);
+ ResolvedIpEndPoint = new IPEndPoint(IPAddress.Parse(IP), Port);
+ }
+
RconParser = Manager.AdditionalRConParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.RConParserVersion);
EventParser = Manager.AdditionalEventParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion);
- RconParser = RconParser ?? Manager.AdditionalRConParsers[0];
- EventParser = EventParser ?? Manager.AdditionalEventParsers[0];
+ RconParser ??= Manager.AdditionalRConParsers[0];
+ EventParser ??= Manager.AdditionalEventParsers[0];
- RemoteConnection.SetConfiguration(RconParser.Configuration);
+ RemoteConnection = RConConnectionFactory.CreateConnection(ResolvedIpEndPoint, Password, RconParser.RConEngine);
+ RemoteConnection.SetConfiguration(RconParser);
var version = await this.GetMappedDvarValueOrDefaultAsync("version");
Version = version.Value;
- GameName = Utilities.GetGame(version?.Value ?? RconParser.Version);
+ GameName = Utilities.GetGame(version.Value ?? RconParser.Version);
if (GameName == Game.UKN)
{
@@ -972,7 +1113,7 @@ namespace IW4MAdmin
if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1")
{
- throw new ServerException(loc["SERVER_ERROR_NOT_RUNNING"]);
+ throw new ServerException(loc["SERVER_ERROR_NOT_RUNNING"].FormatExt(this.ToString()));
}
var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null;
@@ -981,8 +1122,9 @@ namespace IW4MAdmin
string mapname = (await this.GetMappedDvarValueOrDefaultAsync("mapname", infoResponse: infoResponse)).Value;
int maxplayers = (await this.GetMappedDvarValueOrDefaultAsync("sv_maxclients", infoResponse: infoResponse)).Value;
string gametype = (await this.GetMappedDvarValueOrDefaultAsync("g_gametype", "gametype", infoResponse)).Value;
- var basepath = (await this.GetMappedDvarValueOrDefaultAsync("fs_basepath"));
- var basegame = (await this.GetMappedDvarValueOrDefaultAsync("fs_basegame"));
+ var basepath = await this.GetMappedDvarValueOrDefaultAsync("fs_basepath");
+ var basegame = await this.GetMappedDvarValueOrDefaultAsync("fs_basegame");
+ var homepath = await this.GetMappedDvarValueOrDefaultAsync("fs_homepath");
var game = (await this.GetMappedDvarValueOrDefaultAsync("fs_game", infoResponse: infoResponse));
var logfile = await this.GetMappedDvarValueOrDefaultAsync("g_log");
var logsync = await this.GetMappedDvarValueOrDefaultAsync("g_logsync");
@@ -1012,8 +1154,20 @@ namespace IW4MAdmin
{
Website = loc["SERVER_WEBSITE_GENERIC"];
}
+
+ // todo: remove this once _website is weaned off
+ if (string.IsNullOrEmpty(Manager.GetApplicationSettings().Configuration().ContactUri))
+ {
+ Manager.GetApplicationSettings().Configuration().ContactUri = Website;
+ }
+
+ var defaultConfig = _serviceProvider.GetRequiredService();
+ var gameMaps = defaultConfig?.Maps?.FirstOrDefault(map => map.Game == GameName);
- InitializeMaps();
+ if (gameMaps != null)
+ {
+ Maps.AddRange(gameMaps.Maps);
+ }
WorkingDirectory = basepath.Value;
this.Hostname = hostname;
@@ -1042,9 +1196,10 @@ namespace IW4MAdmin
}
if (needsRestart)
- {
- Logger.WriteWarning("Game log file not properly initialized, restarting map...");
- await this.ExecuteCommandAsync("map_restart");
+ {
+ // disabling this for the time being
+ /*Logger.WriteWarning("Game log file not properly initialized, restarting map...");
+ await this.ExecuteCommandAsync("map_restart");*/
}
// this DVAR isn't set until the a map is loaded
@@ -1070,23 +1225,30 @@ namespace IW4MAdmin
{
BaseGameDirectory = basegame.Value,
BasePathDirectory = basepath.Value,
+ HomePathDirectory = homepath.Value,
GameDirectory = EventParser.Configuration.GameDirectory ?? "",
ModDirectory = game.Value ?? "",
LogFile = logfile.Value,
- IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
+ IsOneLog = RconParser.IsOneLog
};
LogPath = GenerateLogPath(logInfo);
+ ServerLogger.LogInformation("Game log information {@logInfo}", logInfo);
if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null)
{
- Logger.WriteError(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
+ Console.WriteLine(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
+ ServerLogger.LogCritical("Game log path does not exist {logPath}", LogPath);
throw new ServerException(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
}
}
- LogEvent = new GameLogEventDetection(this, GenerateUriForLog(LogPath, ServerConfig.GameLogServerUrl?.AbsoluteUri), gameLogReaderFactory);
- Logger.WriteInfo($"Log file is {LogPath}");
+ ServerLogger.LogInformation("Generated game log path is {logPath}", LogPath);
+ LogEvent = new GameLogEventDetection( _serviceProvider.GetRequiredService>(),
+ this,
+ GenerateUriForLog(LogPath, ServerConfig.GameLogServerUrl?.AbsoluteUri), gameLogReaderFactory);
+ await _serverCache.InitializeAsync();
_ = Task.Run(() => LogEvent.PollForChanges());
if (!Utilities.IsDevelopment)
@@ -1097,7 +1259,7 @@ namespace IW4MAdmin
public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl)
{
- var logUri = new Uri(logPath);
+ var logUri = new Uri(logPath, UriKind.Absolute);
if (string.IsNullOrEmpty(gameLogServerUrl))
{
@@ -1113,21 +1275,31 @@ namespace IW4MAdmin
public static string GenerateLogPath(LogPathGeneratorInfo logInfo)
{
string logPath;
- string workingDirectory = logInfo.BasePathDirectory;
+ var workingDirectory = logInfo.BasePathDirectory;
+
+ bool IsValidGamePath (string path)
+ {
+ var baseGameIsDirectory = !string.IsNullOrWhiteSpace(path) &&
+ path.IndexOfAny(Utilities.DirectorySeparatorChars) != -1;
- bool baseGameIsDirectory = !string.IsNullOrWhiteSpace(logInfo.BaseGameDirectory) &&
- logInfo.BaseGameDirectory.IndexOfAny(Utilities.DirectorySeparatorChars) != -1;
+ var baseGameIsRelative = path.FixDirectoryCharacters()
+ .Equals(logInfo.GameDirectory.FixDirectoryCharacters(), StringComparison.InvariantCultureIgnoreCase);
- bool baseGameIsRelative = logInfo.BaseGameDirectory.FixDirectoryCharacters()
- .Equals(logInfo.GameDirectory.FixDirectoryCharacters(), StringComparison.InvariantCultureIgnoreCase);
+ return baseGameIsDirectory && !baseGameIsRelative;
+ }
// we want to see if base game is provided and it 'looks' like a directory
- if (baseGameIsDirectory && !baseGameIsRelative)
+ if (IsValidGamePath(logInfo.HomePathDirectory))
+ {
+ workingDirectory = logInfo.HomePathDirectory;
+ }
+
+ else if (IsValidGamePath(logInfo.BaseGameDirectory))
{
workingDirectory = logInfo.BaseGameDirectory;
}
- if (string.IsNullOrWhiteSpace(logInfo.ModDirectory))
+ if (string.IsNullOrWhiteSpace(logInfo.ModDirectory) || logInfo.IsOneLog)
{
logPath = Path.Combine(workingDirectory, logInfo.GameDirectory, logInfo.LogFile);
}
@@ -1164,8 +1336,8 @@ namespace IW4MAdmin
Link = targetClient.AliasLink
};
- Logger.WriteDebug($"Creating warn penalty for {targetClient}");
- await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
+ ServerLogger.LogDebug("Creating warn penalty for {targetClient}", targetClient.ToString());
+ await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame)
{
@@ -1175,12 +1347,13 @@ namespace IW4MAdmin
return;
}
+ // todo: move to translation sheet
string message = $"^1{loc["SERVER_WARNING"]} ^7[^3{targetClient.Warnings}^7]: ^3{targetClient.Name}^7, {reason}";
targetClient.CurrentServer.Broadcast(message);
}
}
- public override async Task Kick(string Reason, EFClient targetClient, EFClient originClient)
+ public override async Task Kick(string reason, EFClient targetClient, EFClient originClient, EFPenalty previousPenalty)
{
targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients()
@@ -1192,13 +1365,13 @@ namespace IW4MAdmin
Type = EFPenalty.PenaltyType.Kick,
Expires = DateTime.UtcNow,
Offender = targetClient,
- Offense = Reason,
+ Offense = reason,
Punisher = originClient,
Link = targetClient.AliasLink
};
- Logger.WriteDebug($"Creating kick penalty for {targetClient}");
- await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
+ ServerLogger.LogDebug("Creating kick penalty for {targetClient}", targetClient.ToString());
+ await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame)
{
@@ -1211,7 +1384,15 @@ namespace IW4MAdmin
Manager.AddEvent(e);
- string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7");
+ var temporalClientId = targetClient.GetAdditionalProperty("ConnectionClientId");
+ var parsedClientId = string.IsNullOrEmpty(temporalClientId) ? (int?)null : int.Parse(temporalClientId);
+ var clientNumber = parsedClientId ?? targetClient.ClientNumber;
+
+ var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
+ clientNumber,
+ _messageFormatter.BuildFormattedMessage(RconParser.Configuration,
+ newPenalty,
+ previousPenalty));
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
}
@@ -1234,18 +1415,24 @@ namespace IW4MAdmin
Link = targetClient.AliasLink
};
- Logger.WriteDebug($"Creating tempban penalty for {targetClient}");
- await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
+ ServerLogger.LogDebug("Creating tempban penalty for {targetClient}", targetClient.ToString());
+ await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame)
{
- string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}");
- Logger.WriteDebug($"Executing tempban kick command for {targetClient}");
+ var temporalClientId = targetClient.GetAdditionalProperty("ConnectionClientId");
+ var parsedClientId = string.IsNullOrEmpty(temporalClientId) ? (int?)null : int.Parse(temporalClientId);
+ var clientNumber = parsedClientId ?? targetClient.ClientNumber;
+
+ var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
+ clientNumber,
+ _messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
+ ServerLogger.LogDebug("Executing tempban kick command for {targetClient}", targetClient.ToString());
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
}
- override public async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
+ public override async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
{
// ensure player gets kicked if command not performed on them in the same server
targetClient = targetClient.ClientNumber < 0 ?
@@ -1264,14 +1451,21 @@ namespace IW4MAdmin
IsEvadedOffense = isEvade
};
- Logger.WriteDebug($"Creating ban penalty for {targetClient}");
+ ServerLogger.LogDebug("Creating ban penalty for {targetClient}", targetClient.ToString());
targetClient.SetLevel(Permission.Banned, originClient);
- await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
+ await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame)
{
- Logger.WriteDebug($"Attempting to kicking newly banned client {targetClient}");
- string formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7");
+ ServerLogger.LogDebug("Attempting to kicking newly banned client {targetClient}", targetClient.ToString());
+
+ var temporalClientId = targetClient.GetAdditionalProperty("ConnectionClientId");
+ var parsedClientId = string.IsNullOrEmpty(temporalClientId) ? (int?)null : int.Parse(temporalClientId);
+ var clientNumber = parsedClientId ?? targetClient.ClientNumber;
+
+ var formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
+ clientNumber,
+ _messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString);
}
}
@@ -1290,6 +1484,7 @@ namespace IW4MAdmin
Link = Target.AliasLink
};
+ ServerLogger.LogDebug("Creating unban penalty for {targetClient}", Target.ToString());
Target.SetLevel(Permission.User, Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId);
await Manager.GetPenaltyService().Create(unbanPenalty);
diff --git a/Application/Localization/Configure.cs b/Application/Localization/Configure.cs
index 2a275c72..b2155e97 100644
--- a/Application/Localization/Configure.cs
+++ b/Application/Localization/Configure.cs
@@ -6,15 +6,22 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
+using Microsoft.Extensions.Logging;
+using SharedLibraryCore.Configuration;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Localization
{
- public class Configure
+ public static class Configure
{
- public static ITranslationLookup Initialize(bool useLocalTranslation, IMasterApi apiInstance, string customLocale = null)
+ public static ITranslationLookup Initialize(ILogger logger, IMasterApi apiInstance, ApplicationConfiguration applicationConfiguration)
{
- string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
- string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
+ var useLocalTranslation = applicationConfiguration?.UseLocalTranslations ?? true;
+ var customLocale = applicationConfiguration?.EnableCustomLocale ?? false
+ ? (applicationConfiguration.CustomLocale ?? "en-US")
+ : "en-US";
+ var currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
+ var localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
if (!useLocalTranslation)
{
@@ -25,9 +32,10 @@ namespace IW4MAdmin.Application.Localization
return localization.LocalizationIndex;
}
- catch (Exception)
+ catch (Exception ex)
{
// the online localization failed so will default to local files
+ logger.LogWarning(ex, "Could not download latest translations");
}
}
@@ -55,18 +63,20 @@ namespace IW4MAdmin.Application.Localization
{
var localizationContents = File.ReadAllText(filePath, Encoding.UTF8);
var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject(localizationContents);
+ if (eachLocalizationFile == null)
+ {
+ continue;
+ }
foreach (var item in eachLocalizationFile.LocalizationIndex.Set)
{
if (!localizationDict.TryAdd(item.Key, item.Value))
{
- Program.ServerManager.GetLogger(0).WriteError($"Could not add locale string {item.Key} to localization");
+ logger.LogError("Could not add locale string {key} to localization", item.Key);
}
}
}
- string localizationFile = $"{Path.Join(Utilities.OperatingDirectory, "Localization")}{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocale}-{currentLocale.ToUpper()}.json";
-
Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict)
{
LocalizationName = currentLocale,
diff --git a/Application/Main.cs b/Application/Main.cs
index 86732d3c..f57025c2 100644
--- a/Application/Main.cs
+++ b/Application/Main.cs
@@ -1,7 +1,6 @@
using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Factories;
-using IW4MAdmin.Application.Helpers;
using IW4MAdmin.Application.Meta;
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc;
@@ -18,12 +17,23 @@ using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Repositories;
using SharedLibraryCore.Services;
using Stats.Dtos;
-using StatsWeb;
using System;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Data.Abstractions;
+using Data.Helpers;
+using Integrations.Source.Extensions;
+using IW4MAdmin.Application.Extensions;
+using IW4MAdmin.Application.Localization;
+using Microsoft.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+using IW4MAdmin.Plugins.Stats.Client.Abstractions;
+using IW4MAdmin.Plugins.Stats.Client;
+using Stats.Client.Abstractions;
+using Stats.Client;
+using Stats.Helpers;
namespace IW4MAdmin.Application
{
@@ -65,7 +75,10 @@ namespace IW4MAdmin.Application
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
{
ServerManager?.Stop();
- await ApplicationTask;
+ if (ApplicationTask != null)
+ {
+ await ApplicationTask;
+ }
}
///
@@ -74,31 +87,40 @@ namespace IW4MAdmin.Application
///
private static async Task LaunchAsync(string[] args)
{
- restart:
+ restart:
ITranslationLookup translationLookup = null;
+ var logger = BuildDefaultLogger(new ApplicationConfiguration());
+ Utilities.DefaultLogger = logger;
+ IServiceCollection services = null;
+ logger.LogInformation("Begin IW4MAdmin startup. Version is {version} {@args}", Version, args);
+
try
{
// do any needed housekeeping file/folder migrations
ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories();
-
- var services = ConfigureServices(args);
+ ConfigurationMigration.RemoveObsoletePlugins20210322();
+ logger.LogDebug("Configuring services...");
+ services = ConfigureServices(args);
serviceProvider = services.BuildServiceProvider();
var versionChecker = serviceProvider.GetRequiredService();
- ServerManager = (ApplicationManager)serviceProvider.GetRequiredService();
+ ServerManager = (ApplicationManager) serviceProvider.GetRequiredService();
translationLookup = serviceProvider.GetRequiredService();
- ServerManager.Logger.WriteInfo(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_VERSION"].FormatExt(Version));
-
await versionChecker.CheckVersion();
await ServerManager.Init();
}
catch (Exception e)
{
- string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
- string exitMessage = translationLookup == null ? "Press enter to exit..." : translationLookup["MANAGER_EXIT"];
+ string failMessage = translationLookup == null
+ ? "Failed to initialize IW4MAdmin"
+ : translationLookup["MANAGER_INIT_FAIL"];
+ string exitMessage = translationLookup == null
+ ? "Press enter to exit..."
+ : translationLookup["MANAGER_EXIT"];
+ logger.LogCritical(e, "Failed to initialize IW4MAdmin");
Console.WriteLine(failMessage);
while (e.InnerException != null)
@@ -110,7 +132,8 @@ namespace IW4MAdmin.Application
{
if (translationLookup != null)
{
- Console.WriteLine(translationLookup[configException.Message].FormatExt(configException.ConfigurationFileName));
+ Console.WriteLine(translationLookup[configException.Message]
+ .FormatExt(configException.ConfigurationFileName));
}
foreach (string error in configException.Errors)
@@ -131,13 +154,16 @@ namespace IW4MAdmin.Application
try
{
- ApplicationTask = RunApplicationTasksAsync();
+ ApplicationTask = RunApplicationTasksAsync(logger, services);
await ApplicationTask;
}
catch (Exception e)
{
- string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
+ logger.LogCritical(e, "Failed to launch IW4MAdmin");
+ string failMessage = translationLookup == null
+ ? "Failed to launch IW4MAdmin"
+ : translationLookup["MANAGER_INIT_FAIL"];
Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}");
}
@@ -153,27 +179,33 @@ namespace IW4MAdmin.Application
/// runs the core application tasks
///
///
- private static async Task RunApplicationTasksAsync()
+ private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
{
- var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ?
- WebfrontCore.Program.Init(ServerManager, serviceProvider, ServerManager.CancellationToken) :
- Task.CompletedTask;
+ var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront
+ ? WebfrontCore.Program.Init(ServerManager, serviceProvider, services, ServerManager.CancellationToken)
+ : Task.CompletedTask;
+
+ var collectionService = serviceProvider.GetRequiredService();
// we want to run this one on a manual thread instead of letting the thread pool handle it,
// because we can't exit early from waiting on console input, and it prevents us from restarting
- var inputThread = new Thread(async () => await ReadConsoleInput());
+ var inputThread = new Thread(async () => await ReadConsoleInput(logger));
inputThread.Start();
var tasks = new[]
{
ServerManager.Start(),
webfrontTask,
- serviceProvider.GetRequiredService().RunUploadStatus(ServerManager.CancellationToken)
+ serviceProvider.GetRequiredService()
+ .RunUploadStatus(ServerManager.CancellationToken),
+ collectionService.BeginCollectionAsync(cancellationToken: ServerManager.CancellationToken)
};
+ logger.LogDebug("Starting webfront and input tasks");
await Task.WhenAll(tasks);
- ServerManager.Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
+ logger.LogInformation("Shutdown completed successfully");
+ Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
}
@@ -181,11 +213,11 @@ namespace IW4MAdmin.Application
/// reads input from the console and executes entered commands on the default server
///
///
- private static async Task ReadConsoleInput()
+ private static async Task ReadConsoleInput(ILogger logger)
{
if (Console.IsInputRedirected)
{
- ServerManager.Logger.WriteInfo("Disabling console input as it has been redirected");
+ logger.LogInformation("Disabling console input as it has been redirected");
return;
}
@@ -218,29 +250,133 @@ namespace IW4MAdmin.Application
}
}
catch (OperationCanceledException)
- { }
+ {
+ }
}
+ private static IServiceCollection HandlePluginRegistration(ApplicationConfiguration appConfig,
+ IServiceCollection serviceCollection,
+ IMasterApi masterApi)
+ {
+ var defaultLogger = BuildDefaultLogger(appConfig);
+ var pluginServiceProvider = new ServiceCollection()
+ .AddBaseLogger(appConfig)
+ .AddSingleton(appConfig)
+ .AddSingleton(masterApi)
+ .AddSingleton()
+ .AddSingleton()
+ .BuildServiceProvider();
+
+ var pluginImporter = pluginServiceProvider.GetRequiredService();
+
+ // we need to register the rest client with regular collection
+ serviceCollection.AddSingleton(masterApi);
+
+ // register the native commands
+ foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
+ .Concat(typeof(Program).Assembly.GetTypes().Where(type => type.Namespace == "IW4MAdmin.Application.Commands"))
+ .Where(_command => _command.BaseType == typeof(Command)))
+ {
+ defaultLogger.LogDebug("Registered native command type {name}", commandType.Name);
+ serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
+ }
+
+ // register the plugin implementations
+ var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
+ foreach (var pluginType in plugins)
+ {
+ defaultLogger.LogDebug("Registered plugin type {name}", pluginType.FullName);
+ serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
+ }
+
+ // register the plugin commands
+ foreach (var commandType in commands)
+ {
+ defaultLogger.LogDebug("Registered plugin command type {name}", commandType.FullName);
+ serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
+ }
+
+ foreach (var configurationType in configurations)
+ {
+ defaultLogger.LogDebug("Registered plugin config type {name}", configurationType.Name);
+ var configInstance = (IBaseConfiguration) Activator.CreateInstance(configurationType);
+ var handlerType = typeof(BaseConfigurationHandler<>).MakeGenericType(configurationType);
+ var handlerInstance = Activator.CreateInstance(handlerType, new[] {configInstance.Name()});
+ var genericInterfaceType = typeof(IConfigurationHandler<>).MakeGenericType(configurationType);
+
+ serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
+ }
+
+ // register any script plugins
+ foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins())
+ {
+ serviceCollection.AddSingleton(scriptPlugin);
+ }
+
+ // register any eventable types
+ foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
+ .Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))
+ .Union(plugins.SelectMany(_asm => _asm.Assembly.GetTypes())
+ .Distinct()
+ .Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
+ {
+ var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
+ serviceCollection.AddSingleton(instance);
+ }
+
+ return serviceCollection;
+ }
+
+
///
/// Configures the dependency injection services
///
private static IServiceCollection ConfigureServices(string[] args)
{
- var appConfigHandler = new BaseConfigurationHandler("IW4MAdminSettings");
- var appConfig = appConfigHandler.Configuration();
- var defaultLogger = new Logger("IW4MAdmin-Manager");
-
- var masterUri = Utilities.IsDevelopment ? new Uri("http://127.0.0.1:8080") : appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
- var apiClient = RestClient.For(masterUri);
- var pluginImporter = new PluginImporter(defaultLogger, appConfig, apiClient, new RemoteAssemblyHandler(defaultLogger, appConfig));
-
+ // setup the static resources (config/master api/translations)
var serviceCollection = new ServiceCollection();
- serviceCollection.AddSingleton(_serviceProvider => serviceCollection)
- .AddSingleton(appConfigHandler as IConfigurationHandler)
- .AddSingleton(new BaseConfigurationHandler("CommandConfiguration") as IConfigurationHandler)
- .AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService>().Configuration() ?? new ApplicationConfiguration())
- .AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService>().Configuration() ?? new CommandConfiguration())
- .AddSingleton(_serviceProvider => defaultLogger)
+ var appConfigHandler = new BaseConfigurationHandler("IW4MAdminSettings");
+ var defaultConfigHandler = new BaseConfigurationHandler("DefaultSettings");
+ var defaultConfig = defaultConfigHandler.Configuration();
+ var appConfig = appConfigHandler.Configuration();
+ var masterUri = Utilities.IsDevelopment
+ ? new Uri("http://127.0.0.1:8080")
+ : appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
+ var masterRestClient = RestClient.For(masterUri);
+ var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
+
+ if (appConfig == null)
+ {
+ appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
+ appConfigHandler.Set(appConfig);
+ appConfigHandler.Save();
+ }
+
+ // register override level names
+ foreach (var (key, value) in appConfig.OverridePermissionLevelNames)
+ {
+ if (!Utilities.PermissionLevelOverrides.ContainsKey(key))
+ {
+ Utilities.PermissionLevelOverrides.Add(key, value);
+ }
+ }
+
+ // build the dependency list
+ HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
+
+ serviceCollection
+ .AddBaseLogger(appConfig)
+ .AddSingleton(defaultConfig)
+ .AddSingleton(_serviceProvider => serviceCollection)
+ .AddSingleton, BaseConfigurationHandler>()
+ .AddSingleton((IConfigurationHandler) appConfigHandler)
+ .AddSingleton(
+ new BaseConfigurationHandler("CommandConfiguration") as
+ IConfigurationHandler)
+ .AddSingleton(appConfig)
+ .AddSingleton(_serviceProvider =>
+ _serviceProvider.GetRequiredService>()
+ .Configuration() ?? new CommandConfiguration())
.AddSingleton()
.AddSingleton()
.AddSingleton()
@@ -253,24 +389,36 @@ namespace IW4MAdmin.Application
.AddSingleton()
.AddSingleton, ClientService>()
.AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton, ReceivedPenaltyResourceQueryHelper>()
- .AddSingleton, AdministeredPenaltyResourceQueryHelper>()
- .AddSingleton, UpdatedAliasResourceQueryHelper>()
+ .AddSingleton,
+ ReceivedPenaltyResourceQueryHelper>()
+ .AddSingleton,
+ AdministeredPenaltyResourceQueryHelper>()
+ .AddSingleton,
+ UpdatedAliasResourceQueryHelper>()
.AddSingleton, ChatResourceQueryHelper>()
+ .AddSingleton, ConnectionsResourceQueryHelper>()
.AddTransient()
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton(apiClient)
- .AddSingleton(_serviceProvider =>
- {
- var config = _serviceProvider.GetRequiredService>().Configuration();
- return Localization.Configure.Initialize(useLocalTranslation: config?.UseLocalTranslations ?? false,
- apiInstance: _serviceProvider.GetRequiredService(),
- customLocale: config?.EnableCustomLocale ?? false ? (config.CustomLocale ?? "en-US") : "en-US");
- });
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton(typeof(ILookupCache<>), typeof(LookupCache<>))
+ .AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>))
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton(translationLookup)
+ .AddDatabaseContextOptions(appConfig);
if (args.Contains("serialevents"))
{
@@ -281,48 +429,18 @@ namespace IW4MAdmin.Application
serviceCollection.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);
- }
-
- // register any eventable types
- foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
- .Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))
- .Union(pluginImplementations
- .Item1.SelectMany(_asm => _asm.Assembly.GetTypes())
- .Distinct()
- .Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
- {
- var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
- serviceCollection.AddSingleton(instance);
- }
+ serviceCollection.AddSource();
return serviceCollection;
}
+
+ private static ILogger BuildDefaultLogger(ApplicationConfiguration appConfig)
+ {
+ var collection = new ServiceCollection()
+ .AddBaseLogger(appConfig)
+ .BuildServiceProvider();
+
+ return collection.GetRequiredService>();
+ }
}
-}
+}
\ No newline at end of file
diff --git a/Application/Meta/AdministeredPenaltyResourceQueryHelper.cs b/Application/Meta/AdministeredPenaltyResourceQueryHelper.cs
index 1276a4a6..da9f8015 100644
--- a/Application/Meta/AdministeredPenaltyResourceQueryHelper.cs
+++ b/Application/Meta/AdministeredPenaltyResourceQueryHelper.cs
@@ -1,11 +1,14 @@
using System.Linq;
using System.Threading.Tasks;
+using Data.Abstractions;
+using Data.Models;
using Microsoft.EntityFrameworkCore;
-using SharedLibraryCore.Database.Models;
+using Microsoft.Extensions.Logging;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta
{
@@ -18,7 +21,7 @@ namespace IW4MAdmin.Application.Meta
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
- public AdministeredPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
+ public AdministeredPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
_logger = logger;
@@ -26,7 +29,7 @@ namespace IW4MAdmin.Application.Meta
public async Task> QueryResource(ClientPaginationRequest query)
{
- using var ctx = _contextFactory.CreateContext(enableTracking: false);
+ await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => query.ClientId == _penalty.PunisherId)
diff --git a/Application/Meta/ConnectionsResourceQueryHelper.cs b/Application/Meta/ConnectionsResourceQueryHelper.cs
new file mode 100644
index 00000000..b732f735
--- /dev/null
+++ b/Application/Meta/ConnectionsResourceQueryHelper.cs
@@ -0,0 +1,60 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Data.Abstractions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using SharedLibraryCore.Dtos.Meta.Responses;
+using SharedLibraryCore.Helpers;
+using SharedLibraryCore.Interfaces;
+using SharedLibraryCore.QueryHelper;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace IW4MAdmin.Application.Meta
+{
+ public class
+ ConnectionsResourceQueryHelper : IResourceQueryHelper
+ {
+ private readonly ILogger _logger;
+ private readonly IDatabaseContextFactory _contextFactory;
+
+ public ConnectionsResourceQueryHelper(ILogger logger,
+ IDatabaseContextFactory contextFactory)
+ {
+ _contextFactory = contextFactory;
+ _logger = logger;
+ }
+
+ public async Task> QueryResource(
+ ClientPaginationRequest query)
+ {
+ _logger.LogDebug("{Class} {@Request}", nameof(ConnectionsResourceQueryHelper), query);
+
+ await using var context = _contextFactory.CreateContext(enableTracking: false);
+
+ var iqConnections = context.ConnectionHistory.AsNoTracking()
+ .Where(history => query.ClientId == history.ClientId)
+ .Where(history => history.CreatedDateTime < query.Before)
+ .OrderByDescending(history => history.CreatedDateTime);
+
+ var connections = await iqConnections.Select(history => new ConnectionHistoryResponse
+ {
+ MetaId = history.ClientConnectionId,
+ ClientId = history.ClientId,
+ Type = MetaType.ConnectionHistory,
+ ShouldDisplay = true,
+ When = history.CreatedDateTime,
+ ServerName = history.Server.HostName,
+ ConnectionType = history.ConnectionType
+ })
+ .ToListAsync();
+
+ _logger.LogDebug("{Class} retrieved {Number} items", nameof(ConnectionsResourceQueryHelper),
+ connections.Count);
+
+ return new ResourceQueryHelperResult
+ {
+ Results = connections
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/Meta/MetaRegistration.cs b/Application/Meta/MetaRegistration.cs
index 479f87bb..64a5a11b 100644
--- a/Application/Meta/MetaRegistration.cs
+++ b/Application/Meta/MetaRegistration.cs
@@ -6,6 +6,8 @@ using SharedLibraryCore.QueryHelper;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta
{
@@ -18,11 +20,14 @@ namespace IW4MAdmin.Application.Meta
private readonly IResourceQueryHelper _receivedPenaltyHelper;
private readonly IResourceQueryHelper _administeredPenaltyHelper;
private readonly IResourceQueryHelper _updatedAliasHelper;
+ private readonly IResourceQueryHelper
+ _connectionHistoryHelper;
- public MetaRegistration(ILogger logger, IMetaService metaService, ITranslationLookup transLookup, IEntityService clientEntityService,
+ public MetaRegistration(ILogger logger, IMetaService metaService, ITranslationLookup transLookup, IEntityService clientEntityService,
IResourceQueryHelper receivedPenaltyHelper,
IResourceQueryHelper administeredPenaltyHelper,
- IResourceQueryHelper updatedAliasHelper)
+ IResourceQueryHelper updatedAliasHelper,
+ IResourceQueryHelper connectionHistoryHelper)
{
_logger = logger;
_transLookup = transLookup;
@@ -31,6 +36,7 @@ namespace IW4MAdmin.Application.Meta
_receivedPenaltyHelper = receivedPenaltyHelper;
_administeredPenaltyHelper = administeredPenaltyHelper;
_updatedAliasHelper = updatedAliasHelper;
+ _connectionHistoryHelper = connectionHistoryHelper;
}
public void Register()
@@ -39,6 +45,7 @@ namespace IW4MAdmin.Application.Meta
_metaService.AddRuntimeMeta(MetaType.ReceivedPenalty, GetReceivedPenaltiesMeta);
_metaService.AddRuntimeMeta(MetaType.Penalized, GetAdministeredPenaltiesMeta);
_metaService.AddRuntimeMeta(MetaType.AliasUpdate, GetUpdatedAliasMeta);
+ _metaService.AddRuntimeMeta(MetaType.ConnectionHistory, GetConnectionHistoryMeta);
}
private async Task> GetProfileMeta(ClientPaginationRequest request)
@@ -82,7 +89,7 @@ namespace IW4MAdmin.Application.Meta
if (client == null)
{
- _logger.WriteWarning($"No client found with id {request.ClientId} when generating profile meta");
+ _logger.LogWarning("No client found with id {clientId} when generating profile meta", request.ClientId);
return metaList;
}
@@ -161,5 +168,11 @@ namespace IW4MAdmin.Application.Meta
var aliases = await _updatedAliasHelper.QueryResource(request);
return aliases.Results;
}
+
+ private async Task> GetConnectionHistoryMeta(ClientPaginationRequest request)
+ {
+ var connections = await _connectionHistoryHelper.QueryResource(request);
+ return connections.Results;
+ }
}
}
diff --git a/Application/Meta/ReceivedPenaltyResourceQueryHelper.cs b/Application/Meta/ReceivedPenaltyResourceQueryHelper.cs
index 82ed8f9b..681c83d4 100644
--- a/Application/Meta/ReceivedPenaltyResourceQueryHelper.cs
+++ b/Application/Meta/ReceivedPenaltyResourceQueryHelper.cs
@@ -1,13 +1,17 @@
-using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Data.Abstractions;
+using Data.Models;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
using SharedLibraryCore;
-using SharedLibraryCore.Database.Models;
+using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta
{
@@ -19,29 +23,57 @@ namespace IW4MAdmin.Application.Meta
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
+ private readonly ApplicationConfiguration _appConfig;
- public ReceivedPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
+ public ReceivedPenaltyResourceQueryHelper(ILogger logger,
+ IDatabaseContextFactory contextFactory, ApplicationConfiguration appConfig)
{
_contextFactory = contextFactory;
_logger = logger;
+ _appConfig = appConfig;
}
public async Task> QueryResource(ClientPaginationRequest query)
{
var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
- using var ctx = _contextFactory.CreateContext(enableTracking: false);
+ await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var linkId = await ctx.Clients.AsNoTracking()
.Where(_client => _client.ClientId == query.ClientId)
- .Select(_client => _client.AliasLinkId)
+ .Select(_client => new {_client.AliasLinkId, _client.CurrentAliasId })
.FirstOrDefaultAsync();
var iqPenalties = ctx.Penalties.AsNoTracking()
- .Where(_penalty => _penalty.OffenderId == query.ClientId || (linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId))
- .Where(_penalty => _penalty.When < query.Before)
- .OrderByDescending(_penalty => _penalty.When);
+ .Where(_penalty => _penalty.OffenderId == query.ClientId ||
+ linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId.AliasLinkId);
- var penalties = await iqPenalties
+ IQueryable iqIpLinkedPenalties = null;
+
+ if (!_appConfig.EnableImplicitAccountLinking)
+ {
+ var usedIps = await ctx.Aliases.AsNoTracking()
+ .Where(alias => (alias.LinkId == linkId.AliasLinkId || alias.AliasId == linkId.CurrentAliasId) && alias.IPAddress != null)
+ .Select(alias => alias.IPAddress).ToListAsync();
+
+ var aliasedIds = await ctx.Aliases.AsNoTracking().Where(alias => usedIps.Contains(alias.IPAddress))
+ .Select(alias => alias.LinkId)
+ .ToListAsync();
+
+ iqIpLinkedPenalties = ctx.Penalties.AsNoTracking()
+ .Where(penalty =>
+ linkedPenaltyType.Contains(penalty.Type) && aliasedIds.Contains(penalty.LinkId));
+ }
+
+ var iqAllPenalties = iqPenalties;
+
+ if (iqIpLinkedPenalties != null)
+ {
+ iqAllPenalties = iqPenalties.Union(iqIpLinkedPenalties);
+ }
+
+ var penalties = await iqAllPenalties
+ .Where(_penalty => _penalty.When < query.Before)
+ .OrderByDescending(_penalty => _penalty.When)
.Take(query.Count)
.Select(_penalty => new ReceivedPenaltyResponse()
{
diff --git a/Application/Meta/UpdatedAliasResourceQueryHelper.cs b/Application/Meta/UpdatedAliasResourceQueryHelper.cs
index d5ad9989..8dbce96e 100644
--- a/Application/Meta/UpdatedAliasResourceQueryHelper.cs
+++ b/Application/Meta/UpdatedAliasResourceQueryHelper.cs
@@ -6,6 +6,9 @@ using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System.Linq;
using System.Threading.Tasks;
+using Data.Abstractions;
+using Microsoft.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta
{
@@ -18,7 +21,7 @@ namespace IW4MAdmin.Application.Meta
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
- public UpdatedAliasResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
+ public UpdatedAliasResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_contextFactory = contextFactory;
@@ -26,7 +29,7 @@ namespace IW4MAdmin.Application.Meta
public async Task> QueryResource(ClientPaginationRequest query)
{
- using var ctx = _contextFactory.CreateContext(enableTracking: false);
+ await using var ctx = _contextFactory.CreateContext(enableTracking: false);
int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId;
var iqAliasUpdates = ctx.Aliases
diff --git a/Application/Migration/ConfigurationMigration.cs b/Application/Migration/ConfigurationMigration.cs
index d7a421b4..f6650031 100644
--- a/Application/Migration/ConfigurationMigration.cs
+++ b/Application/Migration/ConfigurationMigration.cs
@@ -1,11 +1,8 @@
using SharedLibraryCore;
-using SharedLibraryCore.Interfaces;
using System;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Migration
{
@@ -56,7 +53,6 @@ namespace IW4MAdmin.Application.Migration
if (!Directory.Exists(configDirectory))
{
- log?.WriteDebug($"Creating directory for configs {configDirectory}");
Directory.CreateDirectory(configDirectory);
}
@@ -66,7 +62,6 @@ namespace IW4MAdmin.Application.Migration
foreach (var configFile in configurationFiles)
{
- log?.WriteDebug($"Moving config file {configFile}");
string destinationPath = Path.Join("Configuration", configFile);
if (!File.Exists(destinationPath))
{
@@ -77,7 +72,6 @@ namespace IW4MAdmin.Application.Migration
if (!File.Exists(Path.Join("Database", "Database.db")) &&
File.Exists("Database.db"))
{
- log?.WriteDebug("Moving database file");
File.Move("Database.db", Path.Join("Database", "Database.db"));
}
}
@@ -91,5 +85,20 @@ namespace IW4MAdmin.Application.Migration
config.ManualLogPath = null;
}
}
+
+ public static void RemoveObsoletePlugins20210322()
+ {
+ var files = new[] {"StatsWeb.dll", "StatsWeb.Views.dll"};
+
+ foreach (var file in files)
+ {
+ var path = Path.Join(Utilities.OperatingDirectory, "Plugins", file);
+
+ if (File.Exists(path))
+ {
+ File.Delete(path);
+ }
+ }
+ }
}
}
diff --git a/Application/Migration/DatabaseHousekeeping.cs b/Application/Migration/DatabaseHousekeeping.cs
new file mode 100644
index 00000000..d8814675
--- /dev/null
+++ b/Application/Migration/DatabaseHousekeeping.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Data.Abstractions;
+using Data.Models.Client.Stats;
+
+namespace IW4MAdmin.Application.Migration
+{
+ public static class DatabaseHousekeeping
+ {
+ private static readonly DateTime CutoffDate = DateTime.UtcNow.AddMonths(-6);
+
+ public static async Task RemoveOldRatings(IDatabaseContextFactory contextFactory, CancellationToken token)
+ {
+ await using var context = contextFactory.CreateContext();
+ var dbSet = context.Set();
+ var itemsToDelete = dbSet.Where(rating => rating.When <= CutoffDate);
+ dbSet.RemoveRange(itemsToDelete);
+ await context.SaveChangesAsync(token);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/Misc/BaseConfigurationHandler.cs b/Application/Misc/BaseConfigurationHandler.cs
index 23d8295b..9aaedb06 100644
--- a/Application/Misc/BaseConfigurationHandler.cs
+++ b/Application/Misc/BaseConfigurationHandler.cs
@@ -22,6 +22,11 @@ namespace IW4MAdmin.Application.Misc
Build();
}
+ public BaseConfigurationHandler() : this(typeof(T).Name)
+ {
+
+ }
+
public string FileName { get; }
public void Build()
diff --git a/Application/Misc/ClientNoticeMessageFormatter.cs b/Application/Misc/ClientNoticeMessageFormatter.cs
new file mode 100644
index 00000000..3ab5c120
--- /dev/null
+++ b/Application/Misc/ClientNoticeMessageFormatter.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Data.Models;
+using SharedLibraryCore;
+using SharedLibraryCore.Configuration;
+using SharedLibraryCore.Interfaces;
+
+namespace IW4MAdmin.Application.Misc
+{
+ ///
+ /// implementation of IClientNoticeMessageFormatter
+ ///
+ public class ClientNoticeMessageFormatter : IClientNoticeMessageFormatter
+ {
+ private readonly ITranslationLookup _transLookup;
+ private readonly ApplicationConfiguration _appConfig;
+
+ public ClientNoticeMessageFormatter(ITranslationLookup transLookup, ApplicationConfiguration appConfig)
+ {
+ _transLookup = transLookup;
+ _appConfig = appConfig;
+ }
+
+ public string BuildFormattedMessage(IRConParserConfiguration config, EFPenalty currentPenalty, EFPenalty originalPenalty = null)
+ {
+ var isNewLineSeparator = config.NoticeLineSeparator == Environment.NewLine;
+ var penalty = originalPenalty ?? currentPenalty;
+ var builder = new StringBuilder();
+ // build the top level header
+ var header = _transLookup[$"SERVER_{penalty.Type.ToString().ToUpper()}_TEXT"];
+ builder.Append(header);
+ builder.Append(config.NoticeLineSeparator);
+ // build the reason
+ var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense);
+
+ if (isNewLineSeparator)
+ {
+ foreach (var splitReason in SplitOverMaxLength(reason, config.NoticeMaxCharactersPerLine))
+ {
+ builder.Append(splitReason);
+ builder.Append(config.NoticeLineSeparator);
+ }
+ }
+
+ else
+ {
+ builder.Append(reason);
+ builder.Append(config.NoticeLineSeparator);
+ }
+
+ if (penalty.Type == EFPenalty.PenaltyType.TempBan)
+ {
+ // build the time remaining if temporary
+ var timeRemainingValue = penalty.Expires.HasValue
+ ? (penalty.Expires - DateTime.UtcNow).Value.HumanizeForCurrentCulture()
+ : "--";
+ var timeRemaining = _transLookup["GAME_MESSAGE_PENALTY_TIME_REMAINING"].FormatExt(timeRemainingValue);
+
+ if (isNewLineSeparator)
+ {
+ foreach (var splitReason in SplitOverMaxLength(timeRemaining, config.NoticeMaxCharactersPerLine))
+ {
+ builder.Append(splitReason);
+ builder.Append(config.NoticeLineSeparator);
+ }
+ }
+
+ else
+ {
+ builder.Append(timeRemaining);
+ }
+ }
+
+ if (penalty.Type == EFPenalty.PenaltyType.Ban)
+ {
+ // provide a place to appeal the ban (should always be specified but including a placeholder just incase)
+ builder.Append(_transLookup["GAME_MESSAGE_PENALTY_APPEAL"].FormatExt(_appConfig.ContactUri ?? "--"));
+ }
+
+ // final format looks something like:
+ /*
+ * You are permanently banned
+ * Reason - toxic behavior
+ * Visit example.com to appeal
+ */
+
+ return builder.ToString();
+ }
+
+ private static IEnumerable SplitOverMaxLength(string source, int maxCharactersPerLine)
+ {
+ if (source.Length <= maxCharactersPerLine)
+ {
+ return new[] {source};
+ }
+
+ var segments = new List();
+ var currentLocation = 0;
+ while (currentLocation < source.Length)
+ {
+ var nextLocation = currentLocation + maxCharactersPerLine;
+ // there's probably a more efficient way to do this but this is readable
+ segments.Add(string.Concat(
+ source
+ .Skip(currentLocation)
+ .Take(Math.Min(maxCharactersPerLine, source.Length - currentLocation))));
+ currentLocation = nextLocation;
+ }
+
+ if (currentLocation < source.Length)
+ {
+ segments.Add(source.Substring(currentLocation, source.Length - currentLocation));
+ }
+
+ return segments;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/Misc/EventProfiler.cs b/Application/Misc/EventProfiler.cs
deleted file mode 100644
index 0b884f67..00000000
--- a/Application/Misc/EventProfiler.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using SharedLibraryCore;
-using SharedLibraryCore.Interfaces;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace IW4MAdmin.Application.Misc
-{
- internal class EventPerformance
- {
- public long ExecutionTime { get; set; }
- public GameEvent Event { get; set; }
- public string EventInfo => $"{Event.Type}, {Event.FailReason}, {Event.IsBlocking}, {Event.Data}, {Event.Message}, {Event.Extra}";
- }
-
- public class DuplicateKeyComparer : IComparer where TKey : IComparable
- {
- public int Compare(TKey x, TKey y)
- {
- int result = x.CompareTo(y);
-
- if (result == 0)
- return 1;
- else
- return result;
- }
- }
-
- internal class EventProfiler
- {
- public double AverageEventTime { get; private set; }
- public double MaxEventTime => Events.Values.Last().ExecutionTime;
- public double MinEventTime => Events.Values[0].ExecutionTime;
- public int TotalEventCount => Events.Count;
- public SortedList Events { get; private set; } = new SortedList(new DuplicateKeyComparer());
- private readonly ILogger _logger;
-
- public EventProfiler(ILogger logger)
- {
- _logger = logger;
- }
-
- public void Profile(DateTime start, DateTime end, GameEvent gameEvent)
- {
- _logger.WriteDebug($"Starting profile of event {gameEvent.Id}");
- long executionTime = (long)Math.Round((end - start).TotalMilliseconds);
-
- var perf = new EventPerformance()
- {
- Event = gameEvent,
- ExecutionTime = executionTime
- };
-
- lock (Events)
- {
- Events.Add(executionTime, perf);
- }
-
- AverageEventTime = (AverageEventTime * (TotalEventCount - 1) + executionTime) / TotalEventCount;
- _logger.WriteDebug($"Finished profile of event {gameEvent.Id}");
- }
- }
-}
diff --git a/Application/Misc/EventPublisher.cs b/Application/Misc/EventPublisher.cs
new file mode 100644
index 00000000..a200bf0a
--- /dev/null
+++ b/Application/Misc/EventPublisher.cs
@@ -0,0 +1,44 @@
+using System;
+using Microsoft.Extensions.Logging;
+using SharedLibraryCore;
+using SharedLibraryCore.Interfaces;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace IW4MAdmin.Application.Misc
+{
+ public class EventPublisher : IEventPublisher
+ {
+ public event EventHandler OnClientDisconnect;
+ public event EventHandler OnClientConnect;
+
+ private readonly ILogger _logger;
+
+ public EventPublisher(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public void Publish(GameEvent gameEvent)
+ {
+ _logger.LogDebug("Handling publishing event of type {EventType}", gameEvent.Type);
+
+ try
+ {
+ if (gameEvent.Type == GameEvent.EventType.Connect)
+ {
+ OnClientConnect?.Invoke(this, gameEvent);
+ }
+
+ if (gameEvent.Type == GameEvent.EventType.Disconnect)
+ {
+ OnClientDisconnect?.Invoke(this, gameEvent);
+ }
+ }
+
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not publish event of type {EventType}", gameEvent.Type);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/Misc/LogPathGeneratorInfo.cs b/Application/Misc/LogPathGeneratorInfo.cs
index d2cc778f..ae77e18a 100644
--- a/Application/Misc/LogPathGeneratorInfo.cs
+++ b/Application/Misc/LogPathGeneratorInfo.cs
@@ -19,6 +19,12 @@ namespace IW4MAdmin.Application.Misc
///
public string BasePathDirectory { get; set; } = "";
+ ///
+ /// directory for local storage
+ /// fs_homepath
+ ///
+ public string HomePathDirectory { get; set; } = "";
+
///
/// overide game directory
/// plugin driven
@@ -41,5 +47,11 @@ namespace IW4MAdmin.Application.Misc
/// indicates if running on windows
///
public bool IsWindows { get; set; } = true;
+
+ ///
+ /// indicates that the game does not log to the mods folder (when mod is loaded),
+ /// but rather always to the fs_basegame directory
+ ///
+ public bool IsOneLog { get; set; }
}
}
diff --git a/Application/Misc/Logger.cs b/Application/Misc/Logger.cs
index 96cbda54..8647118f 100644
--- a/Application/Misc/Logger.cs
+++ b/Application/Misc/Logger.cs
@@ -1,132 +1,47 @@
-using IW4MAdmin.Application.IO;
-using SharedLibraryCore;
-using SharedLibraryCore.Interfaces;
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Threading;
+using System;
+using Microsoft.Extensions.Logging;
+using ILogger = SharedLibraryCore.Interfaces.ILogger;
namespace IW4MAdmin.Application
{
+ [Obsolete]
public class Logger : ILogger
{
- enum LogType
+ private readonly Microsoft.Extensions.Logging.ILogger _logger;
+
+ public Logger(ILogger logger)
{
- Verbose,
- Info,
- Debug,
- Warning,
- Error,
- Assert
- }
-
- readonly string FileName;
- readonly ReaderWriterLockSlim WritingLock;
- static readonly short MAX_LOG_FILES = 10;
-
- public Logger(string fn)
- {
- FileName = Path.Join(Utilities.OperatingDirectory, "Log", $"{fn}.log");
- WritingLock = new ReaderWriterLockSlim();
- RotateLogs();
- }
-
- ~Logger()
- {
- WritingLock.Dispose();
- }
-
- ///
- /// rotates logs when log is initialized
- ///
- private void RotateLogs()
- {
- string maxLog = FileName + MAX_LOG_FILES;
-
- if (File.Exists(maxLog))
- {
- File.Delete(maxLog);
- }
-
- for (int i = MAX_LOG_FILES - 1; i >= 0; i--)
- {
- string logToMove = i == 0 ? FileName : FileName + i;
- string movedLogName = FileName + (i + 1);
-
- if (File.Exists(logToMove))
- {
- File.Move(logToMove, movedLogName);
- }
- }
- }
-
- void Write(string msg, LogType type)
- {
- WritingLock.EnterWriteLock();
-
- string stringType = type.ToString();
- msg = msg.StripColors();
-
- try
- {
- stringType = Utilities.CurrentLocalization.LocalizationIndex[$"GLOBAL_{type.ToString().ToUpper()}"];
- }
-
- catch (Exception) { }
-
- string LogLine = $"[{DateTime.Now.ToString("MM.dd.yyy HH:mm:ss.fff")}] - {stringType}: {msg}";
- try
- {
-#if DEBUG
- // lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
- Console.WriteLine(msg);
-#else
- if (type == LogType.Error || type == LogType.Verbose)
- {
- Console.WriteLine(LogLine);
- }
- File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
-#endif
- }
-
- catch (Exception ex)
- {
- Console.WriteLine("Well.. It looks like your machine can't event write to the log file. That's something else...");
- Console.WriteLine(ex.GetExceptionInfo());
- }
-
- WritingLock.ExitWriteLock();
+ _logger = logger;
}
public void WriteVerbose(string msg)
{
- Write(msg, LogType.Verbose);
+ _logger.LogInformation(msg);
}
public void WriteDebug(string msg)
{
- Write(msg, LogType.Debug);
+ _logger.LogDebug(msg);
}
public void WriteError(string msg)
{
- Write(msg, LogType.Error);
+ _logger.LogError(msg);
}
public void WriteInfo(string msg)
{
- Write(msg, LogType.Info);
+ WriteVerbose(msg);
}
public void WriteWarning(string msg)
{
- Write(msg, LogType.Warning);
+ _logger.LogWarning(msg);
}
public void WriteAssert(bool condition, string msg)
{
- if (!condition)
- Write(msg, LogType.Assert);
+ throw new NotImplementedException();
}
}
}
diff --git a/Application/Misc/MasterCommunication.cs b/Application/Misc/MasterCommunication.cs
index 60dc6891..e94b8089 100644
--- a/Application/Misc/MasterCommunication.cs
+++ b/Application/Misc/MasterCommunication.cs
@@ -8,6 +8,8 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
@@ -24,10 +26,9 @@ namespace IW4MAdmin.Application.Misc
private readonly ApplicationConfiguration _appConfig;
private readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99");
private readonly int _apiVersion = 1;
-
private bool firstHeartBeat = true;
- public MasterCommunication(ILogger logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager)
+ public MasterCommunication(ILogger logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager)
{
_logger = logger;
_transLookup = translationLookup;
@@ -55,13 +56,7 @@ namespace IW4MAdmin.Application.Misc
catch (Exception e)
{
- _logger.WriteWarning(_transLookup["MANAGER_VERSION_FAIL"]);
- while (e.InnerException != null)
- {
- e = e.InnerException;
- }
-
- _logger.WriteDebug(e.Message);
+ _logger.LogWarning(e, "Unable to retrieve IW4MAdmin version information");
}
if (version.CurrentVersionStable == _fallbackVersion)
@@ -110,12 +105,12 @@ namespace IW4MAdmin.Application.Misc
catch (System.Net.Http.HttpRequestException e)
{
- _logger.WriteWarning($"Could not send heartbeat - {e.Message}");
+ _logger.LogWarning(e, "Could not send heartbeat");
}
catch (AggregateException e)
{
- _logger.WriteWarning($"Could not send heartbeat - {e.Message}");
+ _logger.LogWarning(e, "Could not send heartbeat");
var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(ApiException));
foreach (var ex in exceptions)
@@ -129,7 +124,7 @@ namespace IW4MAdmin.Application.Misc
catch (ApiException e)
{
- _logger.WriteWarning($"Could not send heartbeat - {e.Message}");
+ _logger.LogWarning(e, "Could not send heartbeat");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
@@ -138,7 +133,7 @@ namespace IW4MAdmin.Application.Misc
catch (Exception e)
{
- _logger.WriteWarning($"Could not send heartbeat - {e.Message}");
+ _logger.LogWarning(e, "Could not send heartbeat");
}
@@ -202,7 +197,7 @@ namespace IW4MAdmin.Application.Misc
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
{
- _logger.WriteWarning($"Response code from master is {response.ResponseMessage.StatusCode}, message is {response.StringContent}");
+ _logger.LogWarning("Non success response code from master is {statusCode}, message is {message}", response.ResponseMessage.StatusCode, response.StringContent);
}
}
}
diff --git a/Application/Misc/MetaService.cs b/Application/Misc/MetaService.cs
index 0f98ad7c..47f0bd1d 100644
--- a/Application/Misc/MetaService.cs
+++ b/Application/Misc/MetaService.cs
@@ -7,6 +7,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Data.Abstractions;
+using Microsoft.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+using Data.Models;
namespace IW4MAdmin.Application.Misc
{
@@ -20,14 +24,14 @@ namespace IW4MAdmin.Application.Misc
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
- public MetaService(ILogger logger, IDatabaseContextFactory contextFactory)
+ public MetaService(ILogger logger, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_metaActions = new Dictionary>();
_contextFactory = contextFactory;
}
- public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client)
+ public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client, EFMeta linkedMeta = null)
{
// this seems to happen if the client disconnects before they've had time to authenticate and be added
if (client.ClientId < 1)
@@ -35,7 +39,7 @@ namespace IW4MAdmin.Application.Misc
return;
}
- using var ctx = _contextFactory.CreateContext();
+ await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey)
@@ -46,6 +50,7 @@ namespace IW4MAdmin.Application.Misc
{
existingMeta.Value = metaValue;
existingMeta.Updated = DateTime.UtcNow;
+ existingMeta.LinkedMetaId = linkedMeta?.MetaId;
}
else
@@ -55,16 +60,101 @@ namespace IW4MAdmin.Application.Misc
ClientId = client.ClientId,
Created = DateTime.UtcNow,
Key = metaKey,
- Value = metaValue
+ Value = metaValue,
+ LinkedMetaId = linkedMeta?.MetaId
});
}
await ctx.SaveChangesAsync();
}
+ public async Task AddPersistentMeta(string metaKey, string metaValue)
+ {
+ await using var ctx = _contextFactory.CreateContext();
+
+ var existingMeta = await ctx.EFMeta
+ .Where(meta => meta.Key == metaKey)
+ .Where(meta => meta.ClientId == null)
+ .ToListAsync();
+
+ var matchValues = existingMeta
+ .Where(meta => meta.Value == metaValue)
+ .ToArray();
+
+ if (matchValues.Any())
+ {
+ foreach (var meta in matchValues)
+ {
+ _logger.LogDebug("Updating existing meta with key {key} and id {id}", meta.Key, meta.MetaId);
+ meta.Value = metaValue;
+ meta.Updated = DateTime.UtcNow;
+ }
+
+ await ctx.SaveChangesAsync();
+ }
+
+ else
+ {
+ _logger.LogDebug("Adding new meta with key {key}", metaKey);
+
+ ctx.EFMeta.Add(new EFMeta()
+ {
+ Created = DateTime.UtcNow,
+ Key = metaKey,
+ Value = metaValue
+ });
+
+ await ctx.SaveChangesAsync();
+ }
+ }
+
+ public async Task RemovePersistentMeta(string metaKey, EFClient client)
+ {
+ await using var context = _contextFactory.CreateContext();
+
+ var existingMeta = await context.EFMeta
+ .FirstOrDefaultAsync(meta => meta.Key == metaKey && meta.ClientId == client.ClientId);
+
+ if (existingMeta == null)
+ {
+ _logger.LogDebug("No meta with key {key} found for client id {id}", metaKey, client.ClientId);
+ return;
+ }
+
+ _logger.LogDebug("Removing meta for key {key} with id {id}", metaKey, existingMeta.MetaId);
+ context.EFMeta.Remove(existingMeta);
+ await context.SaveChangesAsync();
+ }
+
+ public async Task RemovePersistentMeta(string metaKey, string metaValue = null)
+ {
+ await using var context = _contextFactory.CreateContext(enableTracking: false);
+ var existingMeta = await context.EFMeta
+ .Where(meta => meta.Key == metaKey)
+ .Where(meta => meta.ClientId == null)
+ .ToListAsync();
+
+ if (metaValue == null)
+ {
+ _logger.LogDebug("Removing all meta for key {key} with ids [{ids}] ", metaKey, string.Join(", ", existingMeta.Select(meta => meta.MetaId)));
+ existingMeta.ForEach(meta => context.Remove(existingMeta));
+ await context.SaveChangesAsync();
+ return;
+ }
+
+ var foundMeta = existingMeta.FirstOrDefault(meta => meta.Value == metaValue);
+
+ if (foundMeta != null)
+ {
+ _logger.LogDebug("Removing meta for key {key} with id {id}", metaKey, foundMeta.MetaId);
+ context.Remove(foundMeta);
+ await context.SaveChangesAsync();
+ }
+ }
+
public async Task GetPersistentMeta(string metaKey, EFClient client)
{
- using var ctx = _contextFactory.CreateContext(enableTracking: false);
+ await using var ctx = _contextFactory.CreateContext(enableTracking: false);
return await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey)
@@ -74,11 +164,34 @@ namespace IW4MAdmin.Application.Misc
MetaId = _meta.MetaId,
Key = _meta.Key,
ClientId = _meta.ClientId,
- Value = _meta.Value
+ Value = _meta.Value,
+ LinkedMetaId = _meta.LinkedMetaId,
+ LinkedMeta = _meta.LinkedMetaId != null ? new EFMeta()
+ {
+ MetaId = _meta.LinkedMeta.MetaId,
+ Key = _meta.LinkedMeta.Key,
+ Value = _meta.LinkedMeta.Value
+ } : null
})
.FirstOrDefaultAsync();
}
+ public async Task> GetPersistentMeta(string metaKey)
+ {
+ await using var context = _contextFactory.CreateContext(enableTracking: false);
+ return await context.EFMeta
+ .Where(meta => meta.Key == metaKey)
+ .Where(meta => meta.ClientId == null)
+ .Select(meta => new EFMeta
+ {
+ MetaId = meta.MetaId,
+ Key = meta.Key,
+ ClientId = meta.ClientId,
+ Value = meta.Value,
+ })
+ .ToListAsync();
+ }
+
public void AddRuntimeMeta(MetaType metaKey, Func>> metaAction) where T : PaginationRequest where V : IClientMeta
{
if (!_metaActions.ContainsKey(metaKey))
diff --git a/Application/Misc/MiddlewareActionHandler.cs b/Application/Misc/MiddlewareActionHandler.cs
index b038f23a..a3683623 100644
--- a/Application/Misc/MiddlewareActionHandler.cs
+++ b/Application/Misc/MiddlewareActionHandler.cs
@@ -1,8 +1,9 @@
-using SharedLibraryCore;
-using SharedLibraryCore.Interfaces;
+using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
@@ -11,7 +12,7 @@ namespace IW4MAdmin.Application.Misc
private readonly IDictionary> _actions;
private readonly ILogger _logger;
- public MiddlewareActionHandler(ILogger logger)
+ public MiddlewareActionHandler(ILogger logger)
{
_actions = new Dictionary>();
_logger = logger;
@@ -38,8 +39,7 @@ namespace IW4MAdmin.Application.Misc
}
catch (Exception e)
{
- _logger.WriteWarning($"Failed to invoke middleware action {name}");
- _logger.WriteDebug(e.GetExceptionInfo());
+ _logger.LogWarning(e, "Failed to invoke middleware action {name}", name);
}
}
diff --git a/Application/Misc/PluginImporter.cs b/Application/Misc/PluginImporter.cs
index 29723eba..453b6b98 100644
--- a/Application/Misc/PluginImporter.cs
+++ b/Application/Misc/PluginImporter.cs
@@ -5,11 +5,12 @@ using System.Reflection;
using SharedLibraryCore.Interfaces;
using System.Linq;
using SharedLibraryCore;
-using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.API.Master;
+using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
-namespace IW4MAdmin.Application.Helpers
+namespace IW4MAdmin.Application.Misc
{
///
/// implementation of IPluginImporter
@@ -24,7 +25,7 @@ namespace IW4MAdmin.Application.Helpers
private readonly IMasterApi _masterApi;
private readonly ApplicationConfiguration _appConfig;
- public PluginImporter(ILogger logger, ApplicationConfiguration appConfig, IMasterApi masterApi, IRemoteAssemblyHandler remoteAssemblyHandler)
+ public PluginImporter(ILogger logger, ApplicationConfiguration appConfig, IMasterApi masterApi, IRemoteAssemblyHandler remoteAssemblyHandler)
{
_logger = logger;
_masterApi = masterApi;
@@ -44,14 +45,14 @@ namespace IW4MAdmin.Application.Helpers
{
var scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts());
- _logger.WriteInfo($"Discovered {scriptPluginFiles.Count()} potential script plugins");
+ _logger.LogDebug("Discovered {count} potential script plugins", scriptPluginFiles.Count());
if (scriptPluginFiles.Count() > 0)
{
foreach (string fileName in scriptPluginFiles)
{
- _logger.WriteInfo($"Discovered script plugin {fileName}");
- var plugin = new ScriptPlugin(fileName);
+ _logger.LogDebug("Discovered script plugin {fileName}", fileName);
+ var plugin = new ScriptPlugin(_logger, fileName);
yield return plugin;
}
}
@@ -62,16 +63,17 @@ namespace IW4MAdmin.Application.Helpers
/// discovers all the C# assembly plugins and commands
///
///
- public (IEnumerable, IEnumerable) DiscoverAssemblyPluginImplementations()
+ public (IEnumerable, IEnumerable, IEnumerable) DiscoverAssemblyPluginImplementations()
{
- string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
+ var pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
var pluginTypes = Enumerable.Empty();
var commandTypes = Enumerable.Empty();
+ var configurationTypes = Enumerable.Empty();
if (Directory.Exists(pluginDir))
{
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
- _logger.WriteInfo($"Discovered {dllFileNames.Length} potential plugin assemblies");
+ _logger.LogDebug("Discovered {count} potential plugin assemblies", dllFileNames.Length);
if (dllFileNames.Length > 0)
{
@@ -84,17 +86,24 @@ namespace IW4MAdmin.Application.Helpers
.SelectMany(_asm => _asm.GetTypes())
.Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null);
- _logger.WriteInfo($"Discovered {pluginTypes.Count()} plugin implementations");
+ _logger.LogDebug("Discovered {count} plugin implementations", pluginTypes.Count());
commandTypes = assemblies
.SelectMany(_asm => _asm.GetTypes())
.Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command));
- _logger.WriteInfo($"Discovered {commandTypes.Count()} plugin commands");
+ _logger.LogDebug("Discovered {count} plugin commands", commandTypes.Count());
+
+ configurationTypes = assemblies
+ .SelectMany(asm => asm.GetTypes())
+ .Where(asmType =>
+ asmType.IsClass && asmType.GetInterface(nameof(IBaseConfiguration), false) != null);
+
+ _logger.LogDebug("Discovered {count} configuration implementations", configurationTypes.Count());
}
}
- return (pluginTypes, commandTypes);
+ return (pluginTypes, commandTypes, configurationTypes);
}
private IEnumerable GetRemoteAssemblies()
@@ -109,8 +118,7 @@ namespace IW4MAdmin.Application.Helpers
catch (Exception ex)
{
- _logger.WriteWarning("Could not load remote assemblies");
- _logger.WriteDebug(ex.GetExceptionInfo());
+ _logger.LogWarning(ex, "Could not load remote assemblies");
return Enumerable.Empty();
}
}
@@ -127,8 +135,7 @@ namespace IW4MAdmin.Application.Helpers
catch (Exception ex)
{
- _logger.WriteWarning("Could not load remote assemblies");
- _logger.WriteDebug(ex.GetExceptionInfo());
+ _logger.LogWarning(ex,"Could not load remote scripts");
return Enumerable.Empty();
}
}
diff --git a/Application/Misc/RemoteAssemblyHandler.cs b/Application/Misc/RemoteAssemblyHandler.cs
index 1733495c..f2214d99 100644
--- a/Application/Misc/RemoteAssemblyHandler.cs
+++ b/Application/Misc/RemoteAssemblyHandler.cs
@@ -1,5 +1,4 @@
-using SharedLibraryCore;
-using SharedLibraryCore.Configuration;
+using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
@@ -7,6 +6,8 @@ using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
+using Microsoft.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
@@ -20,7 +21,7 @@ namespace IW4MAdmin.Application.Misc
private readonly ApplicationConfiguration _appconfig;
private readonly ILogger _logger;
- public RemoteAssemblyHandler(ILogger logger, ApplicationConfiguration appconfig)
+ public RemoteAssemblyHandler(ILogger logger, ApplicationConfiguration appconfig)
{
_appconfig = appconfig;
_logger = logger;
@@ -41,7 +42,7 @@ namespace IW4MAdmin.Application.Misc
{
if (string.IsNullOrEmpty(_appconfig.Id) || string.IsNullOrWhiteSpace(_appconfig.SubscriptionId))
{
- _logger.WriteWarning($"{nameof(_appconfig.Id)} and {nameof(_appconfig.SubscriptionId)} must be provided to attempt loading remote assemblies/scripts");
+ _logger.LogWarning($"{nameof(_appconfig.Id)} and {nameof(_appconfig.SubscriptionId)} must be provided to attempt loading remote assemblies/scripts");
return new byte[0][];
}
@@ -63,8 +64,7 @@ namespace IW4MAdmin.Application.Misc
catch (CryptographicException ex)
{
- _logger.WriteError("Could not obtain remote plugin assemblies");
- _logger.WriteDebug(ex.GetExceptionInfo());
+ _logger.LogError(ex, "Could not decrypt remote plugin assemblies");
}
return decryptedContent;
diff --git a/Application/Misc/ScriptCommand.cs b/Application/Misc/ScriptCommand.cs
index 6f181bba..26d756e0 100644
--- a/Application/Misc/ScriptCommand.cs
+++ b/Application/Misc/ScriptCommand.cs
@@ -4,7 +4,10 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Threading.Tasks;
+using Data.Models.Client;
+using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Database.Models.EFClient;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
@@ -14,13 +17,15 @@ namespace IW4MAdmin.Application.Misc
public class ScriptCommand : Command
{
private readonly Action _executeAction;
+ private readonly ILogger _logger;
- public ScriptCommand(string name, string alias, string description, bool isTargetRequired, Permission permission,
- CommandArgument[] args, Action executeAction, CommandConfiguration config, ITranslationLookup layout)
+ public ScriptCommand(string name, string alias, string description, bool isTargetRequired, EFClient.Permission permission,
+ CommandArgument[] args, Action executeAction, CommandConfiguration config, ITranslationLookup layout, ILogger logger)
: base(config, layout)
{
_executeAction = executeAction;
+ _logger = logger;
Name = name;
Alias = alias;
Description = description;
@@ -29,14 +34,21 @@ namespace IW4MAdmin.Application.Misc
Arguments = args;
}
- public override Task ExecuteAsync(GameEvent E)
+ public override async Task ExecuteAsync(GameEvent e)
{
if (_executeAction == null)
{
throw new InvalidOperationException($"No execute action defined for command \"{Name}\"");
}
- return Task.Run(() => _executeAction(E));
+ try
+ {
+ await Task.Run(() => _executeAction(e));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to execute ScriptCommand action for command {command} {@event}", Name, e);
+ }
}
}
}
diff --git a/Application/Misc/ScriptPlugin.cs b/Application/Misc/ScriptPlugin.cs
index c8dd97da..dd8e02c9 100644
--- a/Application/Misc/ScriptPlugin.cs
+++ b/Application/Misc/ScriptPlugin.cs
@@ -1,4 +1,5 @@
-using Jint;
+using System;
+using Jint;
using Jint.Native;
using Jint.Runtime;
using Microsoft.CSharp.RuntimeBinder;
@@ -12,6 +13,9 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Serilog.Context;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
@@ -39,9 +43,11 @@ namespace IW4MAdmin.Application.Misc
private readonly SemaphoreSlim _onProcessing;
private bool successfullyLoaded;
private readonly List _registeredCommandNames;
+ private readonly ILogger _logger;
- public ScriptPlugin(string filename, string workingDirectory = null)
+ public ScriptPlugin(ILogger logger, string filename, string workingDirectory = null)
{
+ _logger = logger;
_fileName = filename;
Watcher = new FileSystemWatcher()
{
@@ -84,7 +90,7 @@ namespace IW4MAdmin.Application.Misc
foreach (string commandName in _registeredCommandNames)
{
- manager.GetLogger(0).WriteDebug($"Removing plugin registered command \"{commandName}\"");
+ _logger.LogDebug("Removing plugin registered command {command}", commandName);
manager.RemoveCommandByName(commandName);
}
@@ -112,7 +118,28 @@ namespace IW4MAdmin.Application.Misc
})
.CatchClrExceptions());
- _scriptEngine.Execute(script);
+ try
+ {
+ _scriptEngine.Execute(script);
+ }
+ catch (JavaScriptException ex)
+ {
+
+ _logger.LogError(ex,
+ "Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} at {@locationInfo}",
+ nameof(Initialize), _fileName, ex.Location);
+ throw new PluginException($"A JavaScript parsing error occured while initializing script plugin");
+ }
+
+ catch (Exception e)
+ {
+
+ _logger.LogError(e,
+ "Encountered unexpected error while running {methodName} for script plugin {plugin}",
+ nameof(Initialize), _fileName);
+ throw new PluginException($"An unexpected error occured while initialization script plugin");
+ }
+
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
@@ -129,7 +156,7 @@ namespace IW4MAdmin.Application.Misc
{
foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory))
{
- manager.GetLogger(0).WriteDebug($"Adding plugin registered command \"{command.Name}\"");
+ _logger.LogDebug("Adding plugin registered command {commandName}", command.Name);
manager.AddAdditionalCommand(command);
_registeredCommandNames.Add(command.Name);
}
@@ -141,6 +168,7 @@ namespace IW4MAdmin.Application.Misc
}
}
+ _scriptEngine.SetValue("_configHandler", new ScriptPluginConfigurationWrapper(Name, _scriptEngine));
await OnLoadAsync(manager);
try
@@ -167,12 +195,20 @@ namespace IW4MAdmin.Application.Misc
catch (JavaScriptException ex)
{
- throw new PluginException($"An error occured while initializing script plugin: {ex.Error} (Line: {ex.Location.Start.Line}, Character: {ex.Location.Start.Column})") { PluginFile = _fileName };
+ _logger.LogError(ex,
+ "Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} initialization {@locationInfo}",
+ nameof(OnLoadAsync), _fileName, ex.Location);
+
+ throw new PluginException("An error occured while initializing script plugin");
}
-
- catch
+
+ catch (Exception ex)
{
- throw;
+ _logger.LogError(ex,
+ "Encountered unexpected error while running {methodName} for script plugin {plugin}",
+ nameof(OnLoadAsync), _fileName);
+
+ throw new PluginException("An unexpected error occured while initializing script plugin");
}
finally
@@ -197,10 +233,29 @@ namespace IW4MAdmin.Application.Misc
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(S));
_scriptEngine.Execute("plugin.onEventAsync(_gameEvent, _server)").GetCompletionValue();
}
-
- catch
+
+ catch (JavaScriptException ex)
{
- throw;
+ using (LogContext.PushProperty("Server", S.ToString()))
+ {
+ _logger.LogError(ex,
+ "Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} with event type {eventType} {@locationInfo}",
+ nameof(OnEventAsync), _fileName, E.Type, ex.Location);
+ }
+
+ throw new PluginException($"An error occured while executing action for script plugin");
+ }
+
+ catch (Exception e)
+ {
+ using (LogContext.PushProperty("Server", S.ToString()))
+ {
+ _logger.LogError(e,
+ "Encountered unexpected error while running {methodName} for script plugin {plugin} with event type {eventType}",
+ nameof(OnEventAsync), _fileName, E.Type);
+ }
+
+ throw new PluginException($"An error occured while executing action for script plugin");
}
finally
@@ -215,7 +270,7 @@ namespace IW4MAdmin.Application.Misc
public Task OnLoadAsync(IManager manager)
{
- manager.GetLogger(0).WriteDebug($"OnLoad executing for {Name}");
+ _logger.LogDebug("OnLoad executing for {name}", Name);
_scriptEngine.SetValue("_manager", manager);
return Task.FromResult(_scriptEngine.Execute("plugin.onLoadAsync(_manager)").GetCompletionValue());
}
diff --git a/Application/Misc/ScriptPluginConfigurationWrapper.cs b/Application/Misc/ScriptPluginConfigurationWrapper.cs
new file mode 100644
index 00000000..11d58071
--- /dev/null
+++ b/Application/Misc/ScriptPluginConfigurationWrapper.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using IW4MAdmin.Application.Configuration;
+using Jint;
+using Jint.Native;
+using Newtonsoft.Json.Linq;
+
+namespace IW4MAdmin.Application.Misc
+{
+ public class ScriptPluginConfigurationWrapper
+ {
+ private readonly BaseConfigurationHandler _handler;
+ private readonly ScriptPluginConfiguration _config;
+ private readonly string _pluginName;
+ private readonly Engine _scriptEngine;
+
+ public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine)
+ {
+ _handler = new BaseConfigurationHandler("ScriptPluginSettings");
+ _config = _handler.Configuration() ??
+ (ScriptPluginConfiguration) new ScriptPluginConfiguration().Generate();
+ _pluginName = pluginName;
+ _scriptEngine = scriptEngine;
+ }
+
+ private static int? AsInteger(double d)
+ {
+ return int.TryParse(d.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : (int?) null;
+ }
+
+ public async Task SetValue(string key, object value)
+ {
+ var castValue = value;
+
+ if (value is double d)
+ {
+ castValue = AsInteger(d) ?? value;
+ }
+
+ if (value is object[] array && array.All(item => item is double d && AsInteger(d) != null))
+ {
+ castValue = array.Select(item => AsInteger((double)item)).ToArray();
+ }
+
+ if (!_config.ContainsKey(_pluginName))
+ {
+ _config.Add(_pluginName, new Dictionary());
+ }
+
+ var plugin = _config[_pluginName];
+
+ if (plugin.ContainsKey(key))
+ {
+ plugin[key] = castValue;
+ }
+
+ else
+ {
+ plugin.Add(key, castValue);
+ }
+
+ _handler.Set(_config);
+ await _handler.Save();
+ }
+
+ public JsValue GetValue(string key)
+ {
+ if (!_config.ContainsKey(_pluginName))
+ {
+ return JsValue.Undefined;
+ }
+
+ if (!_config[_pluginName].ContainsKey(key))
+ {
+ return JsValue.Undefined;
+ }
+
+ var item = _config[_pluginName][key];
+
+ if (item is JArray array)
+ {
+ item = array.ToObject>();
+ }
+
+ return JsValue.FromObject(_scriptEngine, item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/Misc/SerializationHelpers.cs b/Application/Misc/SerializationHelpers.cs
index d2753daf..bfb7e0b9 100644
--- a/Application/Misc/SerializationHelpers.cs
+++ b/Application/Misc/SerializationHelpers.cs
@@ -4,6 +4,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using System;
using System.Net;
+using Data.Models;
using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.GameEvent;
diff --git a/Application/Misc/ServerDataCollector.cs b/Application/Misc/ServerDataCollector.cs
new file mode 100644
index 00000000..46b93150
--- /dev/null
+++ b/Application/Misc/ServerDataCollector.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Data.Abstractions;
+using Data.Models;
+using Data.Models.Client;
+using Data.Models.Client.Stats.Reference;
+using Data.Models.Server;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using SharedLibraryCore;
+using SharedLibraryCore.Configuration;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+using SharedLibraryCore.Interfaces;
+
+namespace IW4MAdmin.Application.Misc
+{
+ ///
+ public class ServerDataCollector : IServerDataCollector
+ {
+ private readonly ILogger _logger;
+ private readonly IManager _manager;
+ private readonly IDatabaseContextFactory _contextFactory;
+ private readonly ApplicationConfiguration _appConfig;
+ private readonly IEventPublisher _eventPublisher;
+
+ private bool _inProgress;
+ private TimeSpan _period;
+
+ public ServerDataCollector(ILogger logger, ApplicationConfiguration appConfig,
+ IManager manager, IDatabaseContextFactory contextFactory, IEventPublisher eventPublisher)
+ {
+ _logger = logger;
+ _appConfig = appConfig;
+ _manager = manager;
+ _contextFactory = contextFactory;
+ _eventPublisher = eventPublisher;
+
+ _eventPublisher.OnClientConnect += SaveConnectionInfo;
+ _eventPublisher.OnClientDisconnect += SaveConnectionInfo;
+ }
+
+ ~ServerDataCollector()
+ {
+ _eventPublisher.OnClientConnect -= SaveConnectionInfo;
+ _eventPublisher.OnClientDisconnect -= SaveConnectionInfo;
+ }
+
+ public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default)
+ {
+ if (_inProgress)
+ {
+ throw new InvalidOperationException($"{nameof(ServerDataCollector)} is already collecting data");
+ }
+
+ _logger.LogDebug("Initializing data collection with {Name}", nameof(ServerDataCollector));
+ _inProgress = true;
+ _period = period ?? (Utilities.IsDevelopment
+ ? TimeSpan.FromMinutes(1)
+ : _appConfig.ServerDataCollectionInterval);
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ await Task.Delay(_period, cancellationToken);
+ _logger.LogDebug("{Name} is collecting server data", nameof(ServerDataCollector));
+
+ var data = await BuildCollectionData(cancellationToken);
+ await SaveData(data, cancellationToken);
+ }
+ catch (TaskCanceledException)
+ {
+ _logger.LogInformation("Shutdown requested for {Name}", nameof(ServerDataCollector));
+ return;
+ }
+
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Unexpected error encountered collecting server data for {Name}",
+ nameof(ServerDataCollector));
+ }
+ }
+ }
+
+ private async Task> BuildCollectionData(CancellationToken token)
+ {
+ var data = await Task.WhenAll(_manager.GetServers()
+ .Select(async server => new EFServerSnapshot
+ {
+ CapturedAt = DateTime.UtcNow,
+ PeriodBlock = (int) (DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch).TotalMinutes,
+ ServerId = await server.GetIdForServer(),
+ MapId = await GetOrCreateMap(server.CurrentMap.Name, (Reference.Game) server.GameName, token),
+ ClientCount = server.ClientNum
+ }));
+
+ return data;
+ }
+
+ private async Task GetOrCreateMap(string mapName, Reference.Game game, CancellationToken token)
+ {
+ await using var context = _contextFactory.CreateContext();
+ var existingMap =
+ await context.Maps.FirstOrDefaultAsync(map => map.Name == mapName && map.Game == game, token);
+
+ if (existingMap != null)
+ {
+ return existingMap.MapId;
+ }
+
+ var newMap = new EFMap
+ {
+ Name = mapName,
+ Game = game
+ };
+
+ context.Maps.Add(newMap);
+ await context.SaveChangesAsync(token);
+
+ return newMap.MapId;
+ }
+
+ private async Task SaveData(IEnumerable snapshots, CancellationToken token)
+ {
+ await using var context = _contextFactory.CreateContext();
+ context.ServerSnapshots.AddRange(snapshots);
+ await context.SaveChangesAsync(token);
+ }
+
+ private void SaveConnectionInfo(object sender, GameEvent gameEvent)
+ {
+ using var context = _contextFactory.CreateContext(enableTracking: false);
+ context.ConnectionHistory.Add(new EFClientConnectionHistory
+ {
+ ClientId = gameEvent.Origin.ClientId,
+ ServerId = gameEvent.Owner.GetIdForServer().Result,
+ ConnectionType = gameEvent.Type == GameEvent.EventType.Connect
+ ? Reference.ConnectionType.Connect
+ : Reference.ConnectionType.Disconnect
+ });
+ context.SaveChanges();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/Misc/ServerDataViewer.cs b/Application/Misc/ServerDataViewer.cs
new file mode 100644
index 00000000..a0043579
--- /dev/null
+++ b/Application/Misc/ServerDataViewer.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Data.Abstractions;
+using Data.Models.Client;
+using Data.Models.Server;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using SharedLibraryCore;
+using SharedLibraryCore.Dtos;
+using SharedLibraryCore.Interfaces;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace IW4MAdmin.Application.Misc
+{
+ ///
+ public class ServerDataViewer : IServerDataViewer
+ {
+ private readonly ILogger _logger;
+ private readonly IDataValueCache _snapshotCache;
+ private readonly IDataValueCache _serverStatsCache;
+ private readonly IDataValueCache> _clientHistoryCache;
+
+ private readonly TimeSpan? _cacheTimeSpan =
+ Utilities.IsDevelopment ? TimeSpan.FromSeconds(1) : (TimeSpan?) TimeSpan.FromMinutes(1);
+
+ public ServerDataViewer(ILogger logger, IDataValueCache snapshotCache,
+ IDataValueCache serverStatsCache,
+ IDataValueCache> clientHistoryCache)
+ {
+ _logger = logger;
+ _snapshotCache = snapshotCache;
+ _serverStatsCache = serverStatsCache;
+ _clientHistoryCache = clientHistoryCache;
+ }
+
+ public async Task<(int?, DateTime?)> MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null,
+ CancellationToken token = default)
+ {
+ _snapshotCache.SetCacheItem(async (snapshots, cancellationToken) =>
+ {
+ var oldestEntry = overPeriod.HasValue
+ ? DateTime.UtcNow - overPeriod.Value
+ : DateTime.UtcNow.AddDays(-1);
+
+ int? maxClients;
+ DateTime? maxClientsTime;
+
+ if (serverId != null)
+ {
+ var clients = await snapshots.Where(snapshot => snapshot.ServerId == serverId)
+ .Where(snapshot => snapshot.CapturedAt >= oldestEntry)
+ .OrderByDescending(snapshot => snapshot.ClientCount)
+ .Select(snapshot => new
+ {
+ snapshot.ClientCount,
+ snapshot.CapturedAt
+ })
+ .FirstOrDefaultAsync(cancellationToken);
+
+ maxClients = clients?.ClientCount;
+ maxClientsTime = clients?.CapturedAt;
+ }
+
+ else
+ {
+ var clients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
+ .GroupBy(snapshot => snapshot.PeriodBlock)
+ .Select(grp => new
+ {
+ ClientCount = grp.Sum(snapshot => (int?) snapshot.ClientCount),
+ Time = grp.Max(snapshot => (DateTime?) snapshot.CapturedAt)
+ })
+ .OrderByDescending(snapshot => snapshot.ClientCount)
+ .FirstOrDefaultAsync(cancellationToken);
+
+ maxClients = clients?.ClientCount;
+ maxClientsTime = clients?.Time;
+ }
+
+ _logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients);
+
+ return (maxClients, maxClientsTime);
+ }, nameof(MaxConcurrentClientsAsync), _cacheTimeSpan);
+
+ try
+ {
+ return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync), token);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not retrieve data for {Name}", nameof(MaxConcurrentClientsAsync));
+ return (null, null);
+ }
+ }
+
+ public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default)
+ {
+ _serverStatsCache.SetCacheItem(async (set, cancellationToken) =>
+ {
+ var count = await set.CountAsync(cancellationToken);
+ var startOfPeriod =
+ DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24);
+ var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod,
+ cancellationToken);
+
+ return (count, recentCount);
+ }, nameof(_serverStatsCache), _cacheTimeSpan);
+
+ try
+ {
+ return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), token);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not retrieve data for {Name}", nameof(ClientCountsAsync));
+ return (0, 0);
+ }
+ }
+
+ public async Task> ClientHistoryAsync(TimeSpan? overPeriod = null, CancellationToken token = default)
+ {
+ _clientHistoryCache.SetCacheItem(async (set, cancellationToken) =>
+ {
+ var oldestEntry = overPeriod.HasValue
+ ? DateTime.UtcNow - overPeriod.Value
+ : DateTime.UtcNow.AddHours(-12);
+
+ var history = await set.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
+ .Select(snapshot =>
+ new
+ {
+ snapshot.ServerId,
+ snapshot.CapturedAt,
+ snapshot.ClientCount
+ })
+ .OrderBy(snapshot => snapshot.CapturedAt)
+ .ToListAsync(cancellationToken);
+
+ return history.GroupBy(snapshot => snapshot.ServerId).Select(byServer => new ClientHistoryInfo
+ {
+ ServerId = byServer.Key,
+ ClientCounts = byServer.Select(snapshot => new ClientCountSnapshot()
+ {Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount}).ToList()
+ }).ToList();
+ }, nameof(_clientHistoryCache), TimeSpan.MaxValue);
+
+ try
+ {
+ return await _clientHistoryCache.GetCacheItem(nameof(_clientHistoryCache), token);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not retrieve data for {Name}", nameof(ClientHistoryAsync));
+ return Enumerable.Empty