diff --git a/Application/Application.csproj b/Application/Application.csproj
index 30ff32291..78ebd5fb8 100644
--- a/Application/Application.csproj
+++ b/Application/Application.csproj
@@ -21,7 +21,6 @@
IW4MAdmin.Application
false
- enable
diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs
index 165ad1f12..6c5e33c0b 100644
--- a/Application/ApplicationManager.cs
+++ b/Application/ApplicationManager.cs
@@ -16,6 +16,7 @@ using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
@@ -23,12 +24,18 @@ using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Context;
+using Data.Models;
+using IW4MAdmin.Application.Configuration;
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog.Context;
+using SharedLibraryCore.Events;
+using SharedLibraryCore.Events.Management;
+using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Formatting;
+using SharedLibraryCore.Interfaces.Events;
using static SharedLibraryCore.GameEvent;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
@@ -50,7 +57,7 @@ namespace IW4MAdmin.Application
public IList> CommandInterceptors { get; set; } =
new List>();
public ITokenAuthentication TokenAuthenticator { get; }
- public CancellationToken CancellationToken => _tokenSource.Token;
+ public CancellationToken CancellationToken => _isRunningTokenSource.Token;
public string ExternalIPAddress { get; private set; }
public bool IsRestartRequested { get; private set; }
public IMiddlewareActionHandler MiddlewareActionHandler { get; }
@@ -64,29 +71,30 @@ namespace IW4MAdmin.Application
public IConfigurationHandler ConfigHandler;
readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
- private CancellationTokenSource _tokenSource;
+ private CancellationTokenSource _isRunningTokenSource;
+ private CancellationTokenSource _eventHandlerTokenSource;
private readonly Dictionary> _operationLookup = new Dictionary>();
private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler _commandConfiguration;
private readonly IGameServerInstanceFactory _serverInstanceFactory;
private readonly IParserRegexFactory _parserRegexFactory;
private readonly IEnumerable _customParserEvents;
- private readonly IEventHandler _eventHandler;
+ private readonly ICoreEventHandler _coreEventHandler;
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 ConcurrentDictionary ProcessingEvents { get; } = new();
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,
+ ICoreEventHandler coreEventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
- ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration)
+ ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable v2PLugins)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag();
@@ -101,14 +109,14 @@ namespace IW4MAdmin.Application
AdditionalRConParsers = new List { new BaseRConParser(serviceProvider.GetRequiredService>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication();
_logger = logger;
- _tokenSource = new CancellationTokenSource();
+ _isRunningTokenSource = new CancellationTokenSource();
_commands = commands.ToList();
_translationLookup = translationLookup;
_commandConfiguration = commandConfiguration;
_serverInstanceFactory = serverInstanceFactory;
_parserRegexFactory = parserRegexFactory;
_customParserEvents = customParserEvents;
- _eventHandler = eventHandler;
+ _coreEventHandler = coreEventHandler;
_scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver;
@@ -117,6 +125,8 @@ namespace IW4MAdmin.Application
_appConfig = appConfig;
Plugins = plugins;
InteractionRegistration = interactionRegistration;
+
+ IManagementEventSubscriptions.ClientPersistentIdReceived += OnClientPersistentIdReceived;
}
public IEnumerable Plugins { get; }
@@ -124,7 +134,7 @@ namespace IW4MAdmin.Application
public async Task ExecuteEvent(GameEvent newEvent)
{
- ProcessingEvents.TryAdd(newEvent.Id, newEvent);
+ ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent);
// the event has failed already
if (newEvent.Failed)
@@ -142,12 +152,12 @@ namespace IW4MAdmin.Application
catch (TaskCanceledException)
{
- _logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
+ _logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
}
catch (OperationCanceledException)
{
- _logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
+ _logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
}
// this happens if a plugin requires login
@@ -186,11 +196,11 @@ namespace IW4MAdmin.Application
}
skip:
- if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null)
+ if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null && newEvent.CorrelationId is not null)
{
var correlatedEvents =
ProcessingEvents.Values.Where(ev =>
- ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id)
+ ev.CorrelationId == newEvent.CorrelationId && ev.IncrementalId != newEvent.IncrementalId)
.ToList();
await Task.WhenAll(correlatedEvents.Select(ev =>
@@ -199,14 +209,16 @@ namespace IW4MAdmin.Application
foreach (var correlatedEvent in correlatedEvents)
{
- ProcessingEvents.Remove(correlatedEvent.Id, out _);
+ ProcessingEvents.Remove(correlatedEvent.IncrementalId, out _);
}
}
// we don't want to remove events that are correlated to command
- if (ProcessingEvents.Values.ToList()?.Count(gameEvent => gameEvent.CorrelationId == newEvent.CorrelationId) == 1)
+ if (ProcessingEvents.Values.Count(gameEvent =>
+ newEvent.CorrelationId is not null && newEvent.CorrelationId == gameEvent.CorrelationId) == 1 ||
+ newEvent.CorrelationId is null)
{
- ProcessingEvents.Remove(newEvent.Id, out _);
+ ProcessingEvents.Remove(newEvent.IncrementalId, out _);
}
// tell anyone waiting for the output that we're done
@@ -226,75 +238,58 @@ namespace IW4MAdmin.Application
public IReadOnlyList Commands => _commands.ToImmutableList();
- public async Task UpdateServerStates()
+ private Task UpdateServerStates()
{
- // store the server hash code and task for it
- var runningUpdateTasks = new Dictionary();
- var timeout = TimeSpan.FromSeconds(60);
-
- while (!_tokenSource.IsCancellationRequested) // main shutdown requested
+ var index = 0;
+ return Task.WhenAll(_servers.Select(server =>
{
- // select the server ids that have completed the update task
- var serverTasksToRemove = runningUpdateTasks
- .Where(ut => ut.Value.task.IsCompleted)
- .Select(ut => ut.Key)
- .ToList();
-
- // 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);
- }
-
- // select the servers where the tasks have completed
- var newTaskServers = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
-
- foreach (var server in Servers.Where(s => newTaskServers.Contains(s.EndPoint)))
- {
- var firstTokenSource = new CancellationTokenSource();
- firstTokenSource.CancelAfter(timeout);
- var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(firstTokenSource.Token, _tokenSource.Token);
- runningUpdateTasks.Add(server.EndPoint, (ProcessUpdateHandler(server, linkedTokenSource.Token), linkedTokenSource, DateTime.Now));
- }
-
- try
- {
- await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
- }
- // if a cancellation is received, we want to return immediately after shutting down
- catch
- {
- foreach (var server in Servers.Where(s => newTaskServers.Contains(s.EndPoint)))
- {
- await server.ProcessUpdatesAsync(_tokenSource.Token);
- }
- break;
- }
- }
+ var thisIndex = index;
+ Interlocked.Increment(ref index);
+ return ProcessUpdateHandler(server, thisIndex);
+ }));
}
- private async Task ProcessUpdateHandler(Server server, CancellationToken token)
+ private async Task ProcessUpdateHandler(Server server, int index)
{
- try
+ const int delayScalar = 50; // Task.Delay is inconsistent enough there's no reason to try to prevent collisions
+ var timeout = TimeSpan.FromMinutes(2);
+
+ while (!_isRunningTokenSource.IsCancellationRequested)
{
- await server.ProcessUpdatesAsync(token);
- }
- catch (Exception ex)
- {
- using (LogContext.PushProperty("Server", server.ToString()))
+ try
{
- _logger.LogError(ex, "Failed to update status");
+ var delayFactor = Math.Min(_appConfig.RConPollRate, delayScalar * index);
+ await Task.Delay(delayFactor, _isRunningTokenSource.Token);
+
+ using var timeoutTokenSource = new CancellationTokenSource();
+ timeoutTokenSource.CancelAfter(timeout);
+ using var linkedTokenSource =
+ CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token,
+ _isRunningTokenSource.Token);
+ await server.ProcessUpdatesAsync(linkedTokenSource.Token);
+
+ await Task.Delay(Math.Max(1000, _appConfig.RConPollRate - delayFactor),
+ _isRunningTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ // ignored
+ }
+ catch (Exception ex)
+ {
+ using (LogContext.PushProperty("Server", server.Id))
+ {
+ _logger.LogError(ex, "Failed to update status");
+ }
+ }
+ finally
+ {
+ server.IsInitialized = true;
}
}
- finally
- {
- server.IsInitialized = true;
- }
+
+ // run the final updates to clean up server
+ await server.ProcessUpdatesAsync(_isRunningTokenSource.Token);
}
public async Task Init()
@@ -305,18 +300,24 @@ namespace IW4MAdmin.Application
#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);
+ await ContextSeed.Seed(_serviceProvider.GetRequiredService(), _isRunningTokenSource.Token);
+ await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService(), _isRunningTokenSource.Token);
_logger.LogInformation("Finished database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
#endregion
+
+ #region EVENTS
+ IGameServerEventSubscriptions.ServerValueRequested += OnServerValueRequested;
+ IGameServerEventSubscriptions.ServerValueSetRequested += OnServerValueSetRequested;
+ await IManagementEventSubscriptions.InvokeLoadAsync(this, CancellationToken);
+ # endregion
#region PLUGINS
foreach (var plugin in Plugins)
{
try
{
- if (plugin is ScriptPlugin scriptPlugin)
+ if (plugin is ScriptPlugin scriptPlugin && !plugin.IsParser)
{
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
_serviceProvider.GetService>());
@@ -391,13 +392,11 @@ namespace IW4MAdmin.Application
if (string.IsNullOrEmpty(_appConfig.Id))
{
_appConfig.Id = Guid.NewGuid().ToString();
- await ConfigHandler.Save();
}
if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl))
{
_appConfig.WebfrontBindUrl = "http://0.0.0.0:1624";
- await ConfigHandler.Save();
}
#pragma warning disable 618
@@ -442,8 +441,8 @@ namespace IW4MAdmin.Application
serverConfig.ModifyParsers();
}
- await ConfigHandler.Save();
}
+ await ConfigHandler.Save();
}
if (_appConfig.Servers.Length == 0)
@@ -468,7 +467,7 @@ namespace IW4MAdmin.Application
#endregion
#region COMMANDS
- if (await ClientSvc.HasOwnerAsync(_tokenSource.Token))
+ if (await ClientSvc.HasOwnerAsync(_isRunningTokenSource.Token))
{
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
}
@@ -526,7 +525,7 @@ namespace IW4MAdmin.Application
}
}
#endregion
-
+
Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
await InitializeServers();
IsInitialized = true;
@@ -543,26 +542,23 @@ namespace IW4MAdmin.Application
try
{
// todo: this might not always be an IW4MServer
- var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
- using (LogContext.PushProperty("Server", ServerInstance.ToString()))
+ var serverInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
+ using (LogContext.PushProperty("Server", serverInstance!.ToString()))
{
_logger.LogInformation("Beginning server communication initialization");
- await ServerInstance.Initialize();
+ await serverInstance.Initialize();
- _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);
+ _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);
}
- // add the start event for this server
- var e = new GameEvent()
+ QueueEvent(new MonitorStartEvent
{
- Type = EventType.Start,
- Data = $"{ServerInstance.GameName} started",
- Owner = ServerInstance
- };
-
- AddEvent(e);
+ Server = serverInstance,
+ Source = this
+ });
+
successServers++;
}
@@ -593,11 +589,27 @@ namespace IW4MAdmin.Application
}
}
- public async Task Start() => await UpdateServerStates();
+ public async Task Start()
+ {
+ _eventHandlerTokenSource = new CancellationTokenSource();
+
+ var eventHandlerThread = new Thread(() =>
+ {
+ _coreEventHandler.StartProcessing(_eventHandlerTokenSource.Token);
+ })
+ {
+ Name = nameof(CoreEventHandler)
+ };
+
+ eventHandlerThread.Start();
+ await UpdateServerStates();
+ _eventHandlerTokenSource.Cancel();
+ eventHandlerThread.Join();
+ }
public async Task Stop()
{
- foreach (var plugin in Plugins)
+ foreach (var plugin in Plugins.Where(plugin => !plugin.IsParser))
{
try
{
@@ -607,19 +619,32 @@ namespace IW4MAdmin.Application
{
_logger.LogError(ex, "Could not cleanly unload plugin {PluginName}", plugin.Name);
}
- }
-
- _tokenSource.Cancel();
-
+ }
+
+ _isRunningTokenSource.Cancel();
+
IsRunning = false;
}
- public void Restart()
+ public async Task Restart()
{
IsRestartRequested = true;
- Stop().GetAwaiter().GetResult();
- _tokenSource.Dispose();
- _tokenSource = new CancellationTokenSource();
+ await Stop();
+
+ using var subscriptionTimeoutToken = new CancellationTokenSource();
+ subscriptionTimeoutToken.CancelAfter(Utilities.DefaultCommandTimeout);
+
+ await IManagementEventSubscriptions.InvokeUnloadAsync(this, subscriptionTimeoutToken.Token);
+
+ IGameEventSubscriptions.ClearEventInvocations();
+ IGameServerEventSubscriptions.ClearEventInvocations();
+ IManagementEventSubscriptions.ClearEventInvocations();
+
+ _isRunningTokenSource.Dispose();
+ _isRunningTokenSource = new CancellationTokenSource();
+
+ _eventHandlerTokenSource.Dispose();
+ _eventHandlerTokenSource = new CancellationTokenSource();
}
[Obsolete]
@@ -661,9 +686,14 @@ namespace IW4MAdmin.Application
public void AddEvent(GameEvent gameEvent)
{
- _eventHandler.HandleEvent(this, gameEvent);
+ _coreEventHandler.QueueEvent(this, gameEvent);
}
+ public void QueueEvent(CoreEvent coreEvent)
+ {
+ _coreEventHandler.QueueEvent(this, coreEvent);
+ }
+
public IPageList GetPageList()
{
return PageList;
@@ -698,15 +728,132 @@ namespace IW4MAdmin.Application
public void AddAdditionalCommand(IManagerCommand command)
{
- if (_commands.Any(_command => _command.Name == command.Name || _command.Alias == command.Alias))
+ lock (_commands)
{
- throw new InvalidOperationException($"Duplicate command name or alias ({command.Name}, {command.Alias})");
- }
+ if (_commands.Any(cmd => cmd.Name == command.Name || cmd.Alias == command.Alias))
+ {
+ throw new InvalidOperationException(
+ $"Duplicate command name or alias ({command.Name}, {command.Alias})");
+ }
- _commands.Add(command);
+ _commands.Add(command);
+ }
}
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
public IAlertManager AlertManager => _alertManager;
+
+ private async Task OnServerValueRequested(ServerValueRequestEvent requestEvent, CancellationToken token)
+ {
+ if (requestEvent.Server is not IW4MServer server)
+ {
+ return;
+ }
+
+ Dvar serverValue = null;
+ try
+ {
+ if (requestEvent.DelayMs.HasValue)
+ {
+ await Task.Delay(requestEvent.DelayMs.Value, token);
+ }
+
+ var waitToken = token;
+ using var timeoutTokenSource = new CancellationTokenSource();
+ using var linkedTokenSource =
+ CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
+
+ if (requestEvent.TimeoutMs is not null)
+ {
+ timeoutTokenSource.CancelAfter(requestEvent.TimeoutMs.Value);
+ waitToken = linkedTokenSource.Token;
+ }
+
+ serverValue =
+ await server.GetDvarAsync(requestEvent.ValueName, requestEvent.FallbackValue, waitToken);
+ }
+ catch
+ {
+ // ignored
+ }
+ finally
+ {
+ QueueEvent(new ServerValueReceiveEvent
+ {
+ Server = server,
+ Source = server,
+ Response = serverValue ?? new Dvar { Name = requestEvent.ValueName },
+ Success = serverValue is not null
+ });
+ }
+ }
+
+ private async Task OnServerValueSetRequested(ServerValueSetRequestEvent requestEvent, CancellationToken token)
+ {
+ if (requestEvent.Server is not IW4MServer server)
+ {
+ return;
+ }
+
+ var completed = false;
+ try
+ {
+ if (requestEvent.DelayMs.HasValue)
+ {
+ await Task.Delay(requestEvent.DelayMs.Value, token);
+ }
+
+ if (requestEvent.TimeoutMs is not null)
+ {
+ using var timeoutTokenSource = new CancellationTokenSource(requestEvent.TimeoutMs.Value);
+ using var linkedTokenSource =
+ CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
+ token = linkedTokenSource.Token;
+ }
+
+ await server.SetDvarAsync(requestEvent.ValueName, requestEvent.Value, token);
+ completed = true;
+ }
+ catch
+ {
+ // ignored
+ }
+ finally
+ {
+ QueueEvent(new ServerValueSetCompleteEvent
+ {
+ Server = server,
+ Source = server,
+ Success = completed,
+ Value = requestEvent.Value,
+ ValueName = requestEvent.ValueName
+ });
+ }
+ }
+
+ private async Task OnClientPersistentIdReceived(ClientPersistentIdReceiveEvent receiveEvent, CancellationToken token)
+ {
+ var parts = receiveEvent.PersistentId.Split(",");
+
+ if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
+ int.TryParse(parts[1], out var low))
+ {
+ var guid = long.Parse(high.ToString("X") + low.ToString("X"), NumberStyles.HexNumber);
+
+ var penalties = await PenaltySvc
+ .GetActivePenaltiesByIdentifier(null, guid, receiveEvent.Client.GameName);
+ var banPenalty =
+ penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
+
+ if (banPenalty is not null && receiveEvent.Client.Level != Data.Models.Client.EFClient.Permission.Banned)
+ {
+ _logger.LogInformation(
+ "Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
+ receiveEvent.Client, guid);
+ receiveEvent.Client.Ban(_translationLookup["SERVER_BAN_EVADE"].FormatExt(guid),
+ receiveEvent.Client.CurrentServer.AsConsoleClient(), true);
+ }
+ }
+ }
}
}
diff --git a/Application/CoreEventHandler.cs b/Application/CoreEventHandler.cs
new file mode 100644
index 000000000..37f135f88
--- /dev/null
+++ b/Application/CoreEventHandler.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Concurrent;
+using SharedLibraryCore;
+using SharedLibraryCore.Events;
+using SharedLibraryCore.Interfaces;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using SharedLibraryCore.Events.Management;
+using SharedLibraryCore.Events.Server;
+using SharedLibraryCore.Interfaces.Events;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace IW4MAdmin.Application
+{
+ public class CoreEventHandler : ICoreEventHandler
+ {
+ private const int MaxCurrentEvents = 25;
+ private readonly ILogger _logger;
+ private readonly SemaphoreSlim _onProcessingEvents = new(MaxCurrentEvents, MaxCurrentEvents);
+ private readonly ManualResetEventSlim _onEventReady = new(false);
+ private readonly ConcurrentQueue<(IManager, CoreEvent)> _runningEventTasks = new();
+ private CancellationToken _cancellationToken;
+ private int _activeTasks;
+
+ private static readonly GameEvent.EventType[] OverrideEvents =
+ {
+ GameEvent.EventType.Connect,
+ GameEvent.EventType.Disconnect,
+ GameEvent.EventType.Quit,
+ GameEvent.EventType.Stop
+ };
+
+ public CoreEventHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public void QueueEvent(IManager manager, CoreEvent coreEvent)
+ {
+ _runningEventTasks.Enqueue((manager, coreEvent));
+ _onEventReady.Set();
+ }
+
+ public void StartProcessing(CancellationToken token)
+ {
+ _cancellationToken = token;
+
+ while (!_cancellationToken.IsCancellationRequested)
+ {
+ _onEventReady.Reset();
+
+ try
+ {
+ _onProcessingEvents.Wait(_cancellationToken);
+
+ if (!_runningEventTasks.TryDequeue(out var coreEvent))
+ {
+ if (_onProcessingEvents.CurrentCount < MaxCurrentEvents)
+ {
+ _onProcessingEvents.Release(1);
+ }
+
+ _onEventReady.Wait(_cancellationToken);
+ continue;
+ }
+
+ _logger.LogDebug("Start processing event {Name} {SemaphoreCount} - {QueuedTasks}",
+ coreEvent.Item2.GetType().Name, _onProcessingEvents.CurrentCount, _runningEventTasks.Count);
+
+ _ = Task.Factory.StartNew(() =>
+ {
+ Interlocked.Increment(ref _activeTasks);
+ _logger.LogDebug("[Start] Active Tasks = {TaskCount}", _activeTasks);
+ return HandleEventTaskExecute(coreEvent);
+ });
+ }
+ catch (OperationCanceledException)
+ {
+ // ignored
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not enqueue event for processing");
+ }
+ }
+ }
+
+ private async Task HandleEventTaskExecute((IManager, CoreEvent) coreEvent)
+ {
+ try
+ {
+ await GetEventTask(coreEvent.Item1, coreEvent.Item2);
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogWarning("Event timed out {Type}", coreEvent.Item2.GetType().Name);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not complete invoke for {EventType}",
+ coreEvent.Item2.GetType().Name);
+ }
+ finally
+ {
+ if (_onProcessingEvents.CurrentCount < MaxCurrentEvents)
+ {
+ _logger.LogDebug("Freeing up event semaphore for next event {SemaphoreCount}",
+ _onProcessingEvents.CurrentCount);
+ _onProcessingEvents.Release(1);
+ }
+
+ Interlocked.Decrement(ref _activeTasks);
+ _logger.LogDebug("[Complete] {Type}, Active Tasks = {TaskCount} - {Queue}", coreEvent.Item2.GetType(),
+ _activeTasks, _runningEventTasks.Count);
+ }
+ }
+
+ private Task GetEventTask(IManager manager, CoreEvent coreEvent)
+ {
+ return coreEvent switch
+ {
+ GameEvent gameEvent => BuildLegacyEventTask(manager, coreEvent, gameEvent),
+ GameServerEvent gameServerEvent => IGameServerEventSubscriptions.InvokeEventAsync(gameServerEvent,
+ manager.CancellationToken),
+ ManagementEvent managementEvent => IManagementEventSubscriptions.InvokeEventAsync(managementEvent,
+ manager.CancellationToken),
+ _ => Task.CompletedTask
+ };
+ }
+
+ private async Task BuildLegacyEventTask(IManager manager, CoreEvent coreEvent, GameEvent gameEvent)
+ {
+ if (manager.IsRunning || OverrideEvents.Contains(gameEvent.Type))
+ {
+ await manager.ExecuteEvent(gameEvent);
+ await IGameEventSubscriptions.InvokeEventAsync(coreEvent, manager.CancellationToken);
+ return;
+ }
+
+ _logger.LogDebug("Skipping event as we're shutting down {EventId}", gameEvent.IncrementalId);
+ }
+ }
+}
diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs
index 9b3fb5c16..dc0a5c7ce 100644
--- a/Application/IW4MServer.cs
+++ b/Application/IW4MServer.cs
@@ -9,7 +9,7 @@ using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
-using System.Globalization;
+using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net;
@@ -31,6 +31,9 @@ using IW4MAdmin.Application.Plugin.Script;
using IW4MAdmin.Plugins.Stats.Helpers;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Alerts;
+using SharedLibraryCore.Events.Management;
+using SharedLibraryCore.Events.Server;
+using SharedLibraryCore.Interfaces.Events;
using static Data.Models.Client.EFClient;
namespace IW4MAdmin
@@ -44,11 +47,12 @@ namespace IW4MAdmin
private const int REPORT_FLAG_COUNT = 4;
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;
+ private EFServer _cachedDatabaseServer;
+ private readonly StatManager _statManager;
public IW4MServer(
ServerConfiguration serverConfiguration,
@@ -72,6 +76,18 @@ namespace IW4MAdmin
_messageFormatter = messageFormatter;
_serverCache = serverCache;
_commandConfiguration = commandConfiguration;
+ _statManager = serviceProvider.GetRequiredService();
+
+ IGameServerEventSubscriptions.MonitoringStarted += async (gameEvent, token) =>
+ {
+ if (gameEvent.Server.Id != Id)
+ {
+ return;
+ }
+
+ await EnsureServerAdded();
+ await _statManager.EnsureServerAdded(gameEvent.Server, token);
+ };
}
public override async Task OnClientConnected(EFClient clientFromLog)
@@ -108,7 +124,7 @@ namespace IW4MAdmin
Clients[client.ClientNumber] = client;
ServerLogger.LogDebug("End PreConnect for {client}", client.ToString());
- var e = new GameEvent()
+ var e = new GameEvent
{
Origin = client,
Owner = this,
@@ -116,6 +132,11 @@ namespace IW4MAdmin
};
Manager.AddEvent(e);
+ Manager.QueueEvent(new ClientStateInitializeEvent
+ {
+ Client = client,
+ Source = this,
+ });
return client;
}
@@ -210,10 +231,17 @@ namespace IW4MAdmin
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name,
E.Origin.ToString());
await cmd.ExecuteAsync(E);
+ Manager.QueueEvent(new ClientExecuteCommandEvent
+ {
+ Command = cmd,
+ Client = E.Origin,
+ Source = this,
+ CommandText = E.Data
+ });
}
- var pluginTasks = Manager.Plugins
- .Select(async plugin => await CreatePluginTask(plugin, E));
+ var pluginTasks = Manager.Plugins.Where(plugin => !plugin.IsParser)
+ .Select(plugin => CreatePluginTask(plugin, E));
await Task.WhenAll(pluginTasks);
}
@@ -250,7 +278,7 @@ namespace IW4MAdmin
try
{
- await plugin.OnEventAsync(gameEvent, this).WithWaitCancellation(tokenSource.Token);
+ await plugin.OnEventAsync(gameEvent, this);
}
catch (OperationCanceledException)
{
@@ -277,29 +305,7 @@ namespace IW4MAdmin
{
ServerLogger.LogDebug("processing event of type {type}", E.Type);
- if (E.Type == GameEvent.EventType.Start)
- {
- var existingServer = (await _serverCache
- .FirstAsync(server => server.Id == EndPoint));
-
- var serverId = await GetIdForServer(E.Owner);
-
- if (existingServer == null)
- {
- var server = new EFServer()
- {
- Port = Port,
- EndPoint = ToString(),
- ServerId = serverId,
- GameName = (Reference.Game?)GameName,
- HostName = Hostname
- };
-
- await _serverCache.AddAsync(server);
- }
- }
-
- else if (E.Type == GameEvent.EventType.ConnectionLost)
+ if (E.Type == GameEvent.EventType.ConnectionLost)
{
var exception = E.Extra as Exception;
ServerLogger.LogError(exception,
@@ -350,9 +356,18 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.ChangePermission)
{
var newPermission = (Permission) E.Extra;
+ var oldPermission = E.Target.Level;
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);
+
+ Manager.QueueEvent(new ClientPermissionChangeEvent
+ {
+ Client = E.Origin,
+ Source = this,
+ OldPermission = oldPermission,
+ NewPermission = newPermission
+ });
}
else if (E.Type == GameEvent.EventType.Connect)
@@ -500,6 +515,12 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().Create(newPenalty);
E.Target.SetLevel(Permission.Flagged, E.Origin);
+
+ Manager.QueueEvent(new ClientPenaltyEvent
+ {
+ Client = E.Target,
+ Penalty = newPenalty
+ });
}
else if (E.Type == GameEvent.EventType.Unflag)
@@ -519,6 +540,12 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
E.Target.GameName, E.Target.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unflagPenalty);
+
+ Manager.QueueEvent(new ClientPenaltyRevokeEvent
+ {
+ Client = E.Target,
+ Penalty = unflagPenalty
+ });
}
else if (E.Type == GameEvent.EventType.Report)
@@ -554,6 +581,13 @@ namespace IW4MAdmin
Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"]
.FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner));
}
+
+ Manager.QueueEvent(new ClientPenaltyEvent
+ {
+ Client = E.Target,
+ Penalty = newReport,
+ Source = this
+ });
}
else if (E.Type == GameEvent.EventType.TempBan)
@@ -728,6 +762,11 @@ namespace IW4MAdmin
{
MaxClients = int.Parse(dict["com_maxclients"]);
}
+
+ else if (dict.ContainsKey("com_maxplayers"))
+ {
+ MaxClients = int.Parse(dict["com_maxplayers"]);
+ }
if (dict.ContainsKey("mapname"))
{
@@ -772,34 +811,6 @@ namespace IW4MAdmin
{
E.Origin.UpdateTeam(E.Extra as string);
}
-
- else if (E.Type == GameEvent.EventType.MetaUpdated)
- {
- if (E.Extra is "PersistentClientGuid")
- {
- var parts = E.Data.Split(",");
-
- if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
- int.TryParse(parts[1], out var low))
- {
- var guid = long.Parse(high.ToString("X") + low.ToString("X"), NumberStyles.HexNumber);
-
- var penalties = await Manager.GetPenaltyService()
- .GetActivePenaltiesByIdentifier(null, guid, (Reference.Game)GameName);
- var banPenalty =
- penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
-
- if (banPenalty is not null && E.Origin.Level != Permission.Banned)
- {
- ServerLogger.LogInformation(
- "Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
- E.Origin.ToString(), guid);
- E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(guid),
- Utilities.IW4MAdminClient(this), true);
- }
- }
- }
- }
lock (ChatHistory)
{
@@ -820,6 +831,53 @@ namespace IW4MAdmin
}
}
+ public async Task EnsureServerAdded()
+ {
+ var gameServer = await _serverCache
+ .FirstAsync(server => server.EndPoint == base.Id);
+
+ if (gameServer == null)
+ {
+ gameServer = new EFServer
+ {
+ Port = ListenPort,
+ EndPoint = base.Id,
+ ServerId = BuildLegacyDatabaseId(),
+ GameName = (Reference.Game?)GameName,
+ HostName = ServerName
+ };
+
+ await _serverCache.AddAsync(gameServer);
+ }
+
+ await using var context = _serviceProvider.GetRequiredService()
+ .CreateContext(enableTracking: false);
+
+ context.Servers.Attach(gameServer);
+
+ // we want to set the gamename up if it's never been set, or it changed
+ if (!gameServer.GameName.HasValue || gameServer.GameName.Value != GameCode)
+ {
+ gameServer.GameName = GameCode;
+ context.Entry(gameServer).Property(property => property.GameName).IsModified = true;
+ }
+
+ if (gameServer.HostName == null || gameServer.HostName != ServerName)
+ {
+ gameServer.HostName = ServerName;
+ context.Entry(gameServer).Property(property => property.HostName).IsModified = true;
+ }
+
+ if (gameServer.IsPasswordProtected != !string.IsNullOrEmpty(GamePassword))
+ {
+ gameServer.IsPasswordProtected = !string.IsNullOrEmpty(GamePassword);
+ context.Entry(gameServer).Property(property => property.IsPasswordProtected).IsModified = true;
+ }
+
+ await context.SaveChangesAsync();
+ _cachedDatabaseServer = gameServer;
+ }
+
private async Task OnClientUpdate(EFClient origin)
{
var client = GetClientsAsList().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
@@ -909,22 +967,15 @@ namespace IW4MAdmin
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;
+ return (await _serverCache.FirstAsync(cachedServer =>
+ cachedServer.EndPoint == server.Id || cachedServer.ServerId == server.EndPoint)).ServerId;
+ }
- 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 long BuildLegacyDatabaseId()
+ {
+ long id = HashCode.Combine(ListenAddress, ListenPort);
+ return id < 0 ? Math.Abs(id) : id;
}
private void UpdateMap(string mapname)
@@ -983,7 +1034,7 @@ namespace IW4MAdmin
{
await client.OnDisconnect();
- var e = new GameEvent()
+ var e = new GameEvent
{
Type = GameEvent.EventType.Disconnect,
Owner = this,
@@ -994,6 +1045,14 @@ namespace IW4MAdmin
await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token);
}
+
+ using var tokenSource = new CancellationTokenSource();
+ tokenSource.CancelAfter(Utilities.DefaultCommandTimeout);
+
+ Manager.QueueEvent(new MonitorStopEvent
+ {
+ Server = this
+ });
}
private DateTime _lastMessageSent = DateTime.Now;
@@ -1075,6 +1134,16 @@ namespace IW4MAdmin
Manager.AddEvent(gameEvent);
}
+ if (polledClients[2].Any())
+ {
+ Manager.QueueEvent(new ClientDataUpdateEvent
+ {
+ Clients = new ReadOnlyCollection(polledClients[2]),
+ Server = this,
+ Source = this,
+ });
+ }
+
if (Throttled)
{
var gameEvent = new GameEvent
@@ -1086,6 +1155,12 @@ namespace IW4MAdmin
};
Manager.AddEvent(gameEvent);
+
+ Manager.QueueEvent(new ConnectionRestoreEvent
+ {
+ Server = this,
+ Source = this
+ });
}
LastPoll = DateTime.Now;
@@ -1109,6 +1184,12 @@ namespace IW4MAdmin
};
Manager.AddEvent(gameEvent);
+ Manager.QueueEvent(new ConnectionInterruptEvent
+ {
+ Server = this,
+ Source = this
+ });
+
return true;
}
finally
@@ -1469,6 +1550,12 @@ namespace IW4MAdmin
.FormatExt(activeClient.Warnings, activeClient.Name, reason);
activeClient.CurrentServer.Broadcast(message);
}
+
+ Manager.QueueEvent(new ClientPenaltyEvent
+ {
+ Client = targetClient,
+ Penalty = newPenalty
+ });
}
public override async Task Kick(string reason, EFClient targetClient, EFClient originClient, EFPenalty previousPenalty)
@@ -1507,6 +1594,12 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
+
+ Manager.QueueEvent(new ClientPenaltyEvent
+ {
+ Client = targetClient,
+ Penalty = newPenalty
+ });
}
public override async Task TempBan(string reason, TimeSpan length, EFClient targetClient, EFClient originClient)
@@ -1540,6 +1633,12 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
+
+ Manager.QueueEvent(new ClientPenaltyEvent
+ {
+ Client = targetClient,
+ Penalty = newPenalty
+ });
}
public override async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
@@ -1575,6 +1674,12 @@ namespace IW4MAdmin
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
await activeClient.CurrentServer.ExecuteCommandAsync(formattedString);
}
+
+ Manager.QueueEvent(new ClientPenaltyEvent
+ {
+ Client = targetClient,
+ Penalty = newPenalty
+ });
}
public override async Task Unban(string reason, EFClient targetClient, EFClient originClient)
@@ -1596,6 +1701,12 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unbanPenalty);
+
+ Manager.QueueEvent(new ClientPenaltyRevokeEvent
+ {
+ Client = targetClient,
+ Penalty = unbanPenalty
+ });
}
public override void InitializeTokens()
@@ -1605,5 +1716,7 @@ namespace IW4MAdmin
Manager.GetMessageTokens().Add(new MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.NextMapCommand.GetNextMap(s, _translationLookup)));
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(ListAdminsCommand.OnlineAdmins(s, _translationLookup))));
}
+
+ public override long LegacyDatabaseId => _cachedDatabaseServer.ServerId;
}
}
diff --git a/Application/Main.cs b/Application/Main.cs
index 6cf779ea5..e9a855171 100644
--- a/Application/Main.cs
+++ b/Application/Main.cs
@@ -51,7 +51,7 @@ namespace IW4MAdmin.Application
public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString());
private static ApplicationManager _serverManager;
private static Task _applicationTask;
- private static ServiceProvider _serviceProvider;
+ private static IServiceProvider _serviceProvider;
///
/// entrypoint of the application
@@ -112,23 +112,24 @@ namespace IW4MAdmin.Application
ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories();
ConfigurationMigration.RemoveObsoletePlugins20210322();
+
logger.LogDebug("Configuring services...");
- var services = await ConfigureServices(args);
- _serviceProvider = services.BuildServiceProvider();
- var versionChecker = _serviceProvider.GetRequiredService();
- _serverManager = (ApplicationManager) _serviceProvider.GetRequiredService();
- translationLookup = _serviceProvider.GetRequiredService();
- _applicationTask = RunApplicationTasksAsync(logger, services);
- var tasks = new[]
- {
- versionChecker.CheckVersion(),
- _applicationTask
- };
+ var configHandler = new BaseConfigurationHandler("IW4MAdminSettings");
+ await configHandler.BuildAsync();
+ _serviceProvider = WebfrontCore.Program.InitializeServices(ConfigureServices,
+ (configHandler.Configuration() ?? new ApplicationConfiguration()).WebfrontBindUrl);
+
+ _serverManager = (ApplicationManager)_serviceProvider.GetRequiredService();
+ translationLookup = _serviceProvider.GetRequiredService();
await _serverManager.Init();
- await Task.WhenAll(tasks);
+ _applicationTask = Task.WhenAll(RunApplicationTasksAsync(logger, _serviceProvider),
+ _serverManager.Start());
+
+ await _applicationTask;
+ logger.LogInformation("Shutdown completed successfully");
}
catch (Exception e)
@@ -178,21 +179,20 @@ namespace IW4MAdmin.Application
{
goto restart;
}
-
- await _serviceProvider.DisposeAsync();
}
///
/// runs the core application tasks
///
///
- private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
+ private static Task RunApplicationTasksAsync(ILogger logger, IServiceProvider serviceProvider)
{
var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront
- ? WebfrontCore.Program.Init(_serverManager, _serviceProvider, services, _serverManager.CancellationToken)
+ ? WebfrontCore.Program.GetWebHostTask(_serverManager.CancellationToken)
: Task.CompletedTask;
- var collectionService = _serviceProvider.GetRequiredService();
+ var collectionService = serviceProvider.GetRequiredService();
+ var versionChecker = 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
@@ -203,18 +203,15 @@ namespace IW4MAdmin.Application
var tasks = new[]
{
+ versionChecker.CheckVersion(),
webfrontTask,
- _serverManager.Start(),
- _serviceProvider.GetRequiredService()
+ serviceProvider.GetRequiredService()
.RunUploadStatus(_serverManager.CancellationToken),
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
};
logger.LogDebug("Starting webfront and input tasks");
- await Task.WhenAll(tasks);
-
- logger.LogInformation("Shutdown completed successfully");
- Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
+ return Task.WhenAll(tasks);
}
///
@@ -302,8 +299,21 @@ namespace IW4MAdmin.Application
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in plugins)
{
- defaultLogger.LogDebug("Registered plugin type {Name}", pluginType.FullName);
- serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
+ var isV2 = pluginType.GetInterface(nameof(IPluginV2), false) != null;
+
+ defaultLogger.LogDebug("Registering plugin type {Name}", pluginType.FullName);
+
+ serviceCollection.AddSingleton(!isV2 ? typeof(IPlugin) : typeof(IPluginV2), pluginType);
+
+ try
+ {
+ var registrationMethod = pluginType.GetMethod(nameof(IPluginV2.RegisterDependencies));
+ registrationMethod?.Invoke(null, new object[] { serviceCollection });
+ }
+ catch (Exception ex)
+ {
+ defaultLogger.LogError(ex, "Could not register plugin of type {Type}", pluginType.Name);
+ }
}
// register the plugin commands
@@ -351,13 +361,11 @@ namespace IW4MAdmin.Application
///
/// Configures the dependency injection services
///
- private static async Task ConfigureServices(string[] args)
+ private static void ConfigureServices(IServiceCollection serviceCollection)
{
// todo: this is a quick fix
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
- // setup the static resources (config/master api/translations)
- var serviceCollection = new ServiceCollection();
serviceCollection.AddConfiguration("IW4MAdminSettings")
.AddConfiguration()
.AddConfiguration()
@@ -365,14 +373,10 @@ namespace IW4MAdmin.Application
// for legacy purposes. update at some point
var appConfigHandler = new BaseConfigurationHandler("IW4MAdminSettings");
- await appConfigHandler.BuildAsync();
- var defaultConfigHandler = new BaseConfigurationHandler("DefaultSettings");
- await defaultConfigHandler.BuildAsync();
+ appConfigHandler.BuildAsync().GetAwaiter().GetResult();
var commandConfigHandler = new BaseConfigurationHandler("CommandConfiguration");
- await commandConfigHandler.BuildAsync();
- var statsCommandHandler = new BaseConfigurationHandler("StatsPluginSettings");
- await statsCommandHandler.BuildAsync();
- var defaultConfig = defaultConfigHandler.Configuration();
+ commandConfigHandler.BuildAsync().GetAwaiter().GetResult();
+
var appConfig = appConfigHandler.Configuration();
var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080")
@@ -385,13 +389,6 @@ namespace IW4MAdmin.Application
var masterRestClient = RestClient.For(httpClient);
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
- if (appConfig == null)
- {
- appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
- appConfigHandler.Set(appConfig);
- await appConfigHandler.Save();
- }
-
// register override level names
foreach (var (key, value) in appConfig.OverridePermissionLevelNames)
{
@@ -402,17 +399,10 @@ namespace IW4MAdmin.Application
}
// build the dependency list
- HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
-
serviceCollection
.AddBaseLogger(appConfig)
- .AddSingleton(defaultConfig)
- .AddSingleton(serviceCollection)
- .AddSingleton, BaseConfigurationHandler>()
.AddSingleton((IConfigurationHandler) appConfigHandler)
.AddSingleton>(commandConfigHandler)
- .AddSingleton(appConfig)
- .AddSingleton(statsCommandHandler.Configuration() ?? new StatsConfiguration())
.AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService>()
.Configuration() ?? new CommandConfiguration())
@@ -464,7 +454,9 @@ namespace IW4MAdmin.Application
.AddSingleton()
.AddSingleton(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton()
+#pragma warning disable CS0618
.AddTransient()
+#pragma warning restore CS0618
.AddSingleton()
.AddSingleton()
.AddSingleton(new ConfigurationWatcher())
@@ -472,19 +464,10 @@ namespace IW4MAdmin.Application
.AddSingleton()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);
-
- if (args.Contains("serialevents"))
- {
- serviceCollection.AddSingleton();
- }
- else
- {
- serviceCollection.AddSingleton();
- }
-
+
+ serviceCollection.AddSingleton();
serviceCollection.AddSource();
-
- return serviceCollection;
+ HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
}
private static ILogger BuildDefaultLogger(ApplicationConfiguration appConfig)
diff --git a/Application/Misc/RemoteCommandService.cs b/Application/Misc/RemoteCommandService.cs
index ccd573d3f..343e66c8b 100644
--- a/Application/Misc/RemoteCommandService.cs
+++ b/Application/Misc/RemoteCommandService.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -27,7 +28,7 @@ public class RemoteCommandService : IRemoteCommandService
public async Task> Execute(int originId, int? targetId, string command,
IEnumerable arguments, Server server)
{
- var (success, result) = await ExecuteWithResult(originId, targetId, command, arguments, server);
+ var (_, result) = await ExecuteWithResult(originId, targetId, command, arguments, server);
return result;
}
@@ -56,7 +57,8 @@ public class RemoteCommandService : IRemoteCommandService
: $"{_appConfig.CommandPrefix}{command}",
Origin = client,
Owner = server,
- IsRemote = true
+ IsRemote = true,
+ CorrelationId = Guid.NewGuid()
};
server.Manager.AddEvent(remoteEvent);
@@ -72,7 +74,7 @@ public class RemoteCommandService : IRemoteCommandService
{
response = new[]
{
- new CommandResponseInfo()
+ new CommandResponseInfo
{
ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"]
@@ -90,7 +92,7 @@ public class RemoteCommandService : IRemoteCommandService
}
}
- catch (System.OperationCanceledException)
+ catch (OperationCanceledException)
{
response = new[]
{
diff --git a/Application/Misc/ServerDataCollector.cs b/Application/Misc/ServerDataCollector.cs
index ebe118982..ef6e9b78a 100644
--- a/Application/Misc/ServerDataCollector.cs
+++ b/Application/Misc/ServerDataCollector.cs
@@ -12,8 +12,10 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
+using SharedLibraryCore.Events.Management;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using SharedLibraryCore.Interfaces;
+using SharedLibraryCore.Interfaces.Events;
namespace IW4MAdmin.Application.Misc
{
@@ -24,28 +26,20 @@ namespace IW4MAdmin.Application.Misc
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)
+ IManager manager, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_appConfig = appConfig;
_manager = manager;
_contextFactory = contextFactory;
- _eventPublisher = eventPublisher;
- _eventPublisher.OnClientConnect += SaveConnectionInfo;
- _eventPublisher.OnClientDisconnect += SaveConnectionInfo;
- }
-
- ~ServerDataCollector()
- {
- _eventPublisher.OnClientConnect -= SaveConnectionInfo;
- _eventPublisher.OnClientDisconnect -= SaveConnectionInfo;
+ IManagementEventSubscriptions.ClientStateAuthorized += SaveConnectionInfo;
+ IManagementEventSubscriptions.ClientStateDisposed += SaveConnectionInfo;
}
public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default)
@@ -131,18 +125,19 @@ namespace IW4MAdmin.Application.Misc
await context.SaveChangesAsync(token);
}
- private void SaveConnectionInfo(object sender, GameEvent gameEvent)
+ private async Task SaveConnectionInfo(ClientStateEvent stateEvent, CancellationToken token)
{
- using var context = _contextFactory.CreateContext(enableTracking: false);
+ await 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
+ ClientId = stateEvent.Client.ClientId,
+ ServerId = await stateEvent.Client.CurrentServer.GetIdForServer(),
+ ConnectionType = stateEvent is ClientStateAuthorizeEvent
? Reference.ConnectionType.Connect
: Reference.ConnectionType.Disconnect
});
- context.SaveChanges();
+
+ await context.SaveChangesAsync();
}
}
}
diff --git a/Application/Misc/ServerDataViewer.cs b/Application/Misc/ServerDataViewer.cs
index 3a0bdabf8..e174f9ccf 100644
--- a/Application/Misc/ServerDataViewer.cs
+++ b/Application/Misc/ServerDataViewer.cs
@@ -176,7 +176,7 @@ namespace IW4MAdmin.Application.Misc
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken);
- }, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
+ }, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
try
{
diff --git a/Application/Plugin/Script/ScriptPluginV2.cs b/Application/Plugin/Script/ScriptPluginV2.cs
index be797f68a..63968ea3d 100644
--- a/Application/Plugin/Script/ScriptPluginV2.cs
+++ b/Application/Plugin/Script/ScriptPluginV2.cs
@@ -151,10 +151,12 @@ public class ScriptPluginV2 : IPluginV2
}
ScriptEngine.Execute(pluginScript);
+#pragma warning disable CS8974
var initResult = ScriptEngine.Call("init", JsValue.FromObject(ScriptEngine, EventCallbackWrapper),
JsValue.FromObject(ScriptEngine, _pluginServiceResolver),
JsValue.FromObject(ScriptEngine, _scriptPluginConfigurationWrapper),
JsValue.FromObject(ScriptEngine, new ScriptPluginHelper(manager, this)));
+#pragma warning restore CS8974
if (initResult.IsNull() || initResult.IsUndefined())
{
diff --git a/Application/QueryHelpers/ClientResourceQueryHelper.cs b/Application/QueryHelpers/ClientResourceQueryHelper.cs
index 44df86d9c..acee9f01e 100644
--- a/Application/QueryHelpers/ClientResourceQueryHelper.cs
+++ b/Application/QueryHelpers/ClientResourceQueryHelper.cs
@@ -142,12 +142,12 @@ public class ClientResourceQueryHelper : IResourceQueryHelper, DateTime> SearchByAliasLocal(string? clientName,
- string? ipAddress)
+ private static Func, DateTime> SearchByAliasLocal(string clientName,
+ string ipAddress)
{
return group =>
{
- ClientResourceResponse? match = null;
+ ClientResourceResponse match = null;
var lowercaseClientName = clientName?.ToLower();
if (!string.IsNullOrWhiteSpace(lowercaseClientName))
diff --git a/Plugins/LiveRadar/Controllers/RadarController.cs b/Plugins/LiveRadar/Controllers/RadarController.cs
index b82805b7c..505322816 100644
--- a/Plugins/LiveRadar/Controllers/RadarController.cs
+++ b/Plugins/LiveRadar/Controllers/RadarController.cs
@@ -43,7 +43,7 @@ namespace IW4MAdmin.Plugins.LiveRadar.Web.Controllers
[HttpGet]
[Route("Radar/{serverId}/Map")]
- public async Task Map(string serverId = null)
+ public IActionResult Map(string serverId = null)
{
var server = serverId == null
? _manager.GetServers().FirstOrDefault()
diff --git a/Plugins/Mute/Plugin.cs b/Plugins/Mute/Plugin.cs
index f2b0e2c54..242857da6 100644
--- a/Plugins/Mute/Plugin.cs
+++ b/Plugins/Mute/Plugin.cs
@@ -59,7 +59,9 @@ public class Plugin : IPluginV2
return true;
}
- var muteMeta = _muteManager.GetCurrentMuteState(gameEvent.Origin).GetAwaiter().GetResult();
+ var muteMeta = Task.Run(() => _muteManager.GetCurrentMuteState(gameEvent.Origin), cancellationToken)
+ .GetAwaiter().GetResult();
+
if (muteMeta.MuteState is not MuteState.Muted)
{
return true;
diff --git a/Plugins/ScriptPlugins/GameInterface.js b/Plugins/ScriptPlugins/GameInterface.js
index 44c7de553..79f49409b 100644
--- a/Plugins/ScriptPlugins/GameInterface.js
+++ b/Plugins/ScriptPlugins/GameInterface.js
@@ -196,7 +196,7 @@ const plugin = {
let data = [];
- const metaService = this.serviceResolver.ResolveService('IMetaServiceV2');
+ const metaService = this.serviceResolver.resolveService('IMetaServiceV2');
if (event.subType === 'Meta') {
const meta = (await metaService.getPersistentMeta(event.data, client.clientId, token)).result;
@@ -237,8 +237,8 @@ const plugin = {
this.logger.logDebug('ClientId={clientId}', clientId);
- if (clientId == null) {
- this.logger.logWarning('Could not find client slot {clientNumber} when processing {eventType}', event.clientNumber, event.eventType);
+ if (clientId == null || isNaN(clientId)) {
+ this.logger.logWarning('Could not find client slot {clientNumber} when processing {eventType}: {EventData}', event.clientNumber, event.eventType, event.data);
this.sendEventMessage(server, false, 'SetClientDataCompleted', 'Meta', {
ClientNumber: event.clientNumber
}, undefined, {
diff --git a/SharedLibraryCore/Events/CoreEvent.cs b/SharedLibraryCore/Events/CoreEvent.cs
new file mode 100644
index 000000000..17d44b49e
--- /dev/null
+++ b/SharedLibraryCore/Events/CoreEvent.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace SharedLibraryCore.Events;
+
+public abstract class CoreEvent
+{
+ public Guid Id { get; } = Guid.NewGuid();
+ public Guid? CorrelationId { get; init; }
+ public object Source { get; init; }
+ public DateTimeOffset CreatedAt { get; } = DateTimeOffset.UtcNow;
+ public DateTimeOffset? ProcessedAt { get; set; }
+}
diff --git a/SharedLibraryCore/Events/EventAPI.cs b/SharedLibraryCore/Events/EventAPI.cs
deleted file mode 100644
index 136225a49..000000000
--- a/SharedLibraryCore/Events/EventAPI.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using SharedLibraryCore.Dtos;
-
-namespace SharedLibraryCore.Events
-{
- public class EventApi
- {
- private const int MaxEvents = 25;
- private static readonly ConcurrentQueue RecentEvents = new ConcurrentQueue();
-
- public static IEnumerable GetEvents(bool shouldConsume)
- {
- var eventList = RecentEvents.ToArray();
-
- // clear queue if events should be consumed
- if (shouldConsume)
- {
- RecentEvents.Clear();
- }
-
- return eventList;
- }
-
- public static void OnGameEvent(GameEvent gameEvent)
- {
- var E = gameEvent;
- // don't want to clog up the api with unknown events
- if (E.Type == GameEvent.EventType.Unknown)
- {
- return;
- }
-
- var apiEvent = new EventInfo
- {
- ExtraInfo = E.Extra?.ToString() ?? E.Data,
- GameInfo = new EntityInfo
- {
- Name = E.Owner.GameName.ToString(),
- Id = (int)E.Owner.GameName
- },
- OwnerEntity = new EntityInfo
- {
- Name = E.Owner.Hostname,
- Id = E.Owner.EndPoint
- },
- OriginEntity = E.Origin == null
- ? null
- : new EntityInfo
- {
- Id = E.Origin.ClientId,
- Name = E.Origin.Name
- },
- TargetEntity = E.Target == null
- ? null
- : new EntityInfo
- {
- Id = E.Target.ClientId,
- Name = E.Target.Name
- },
- EventType = new EntityInfo
- {
- Id = (int)E.Type,
- Name = E.Type.ToString()
- },
- EventTime = E.Time
- };
-
- // add the new event to the list
- AddNewEvent(apiEvent);
- }
-
- ///
- /// Adds event to the list and removes first added if reached max capacity
- ///
- /// EventInfo to add
- private static void AddNewEvent(EventInfo info)
- {
- // remove the first added event
- if (RecentEvents.Count >= MaxEvents)
- {
- RecentEvents.TryDequeue(out _);
- }
-
- RecentEvents.Enqueue(info);
- }
- }
-}
\ No newline at end of file
diff --git a/SharedLibraryCore/Events/EventExtensions.cs b/SharedLibraryCore/Events/EventExtensions.cs
new file mode 100644
index 000000000..2d7813b2a
--- /dev/null
+++ b/SharedLibraryCore/Events/EventExtensions.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SharedLibraryCore.Events;
+
+public static class EventExtensions
+{
+ public static Task InvokeAsync(this Func function,
+ TEventType eventArgType, CancellationToken token)
+ {
+ if (function is null)
+ {
+ return Task.CompletedTask;
+ }
+
+ return Task.WhenAll(function.GetInvocationList().Cast>()
+ .Select(x => RunHandler(x, eventArgType, token)));
+ }
+
+ private static async Task RunHandler(Func handler,
+ TEventType eventArgType, CancellationToken token)
+ {
+ if (token == CancellationToken.None)
+ {
+ // special case to allow tasks like request after delay to run longer
+ await handler(eventArgType, token);
+ }
+
+ using var timeoutToken = new CancellationTokenSource(Utilities.DefaultCommandTimeout);
+ using var tokenSource =
+ CancellationTokenSource.CreateLinkedTokenSource(token, timeoutToken.Token);
+
+ try
+ {
+ await handler(eventArgType, tokenSource.Token);
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
+}
diff --git a/SharedLibraryCore/Events/Game/GameEventV2.cs b/SharedLibraryCore/Events/Game/GameEventV2.cs
index eb48e9aec..c27e31318 100644
--- a/SharedLibraryCore/Events/Game/GameEventV2.cs
+++ b/SharedLibraryCore/Events/Game/GameEventV2.cs
@@ -4,5 +4,5 @@ namespace SharedLibraryCore.Events.Game;
public abstract class GameEventV2 : GameEvent
{
- public IGameServer Server { get; init; }
+ public IGameServer Server => Owner;
}
diff --git a/SharedLibraryCore/Events/GameEvent.cs b/SharedLibraryCore/Events/GameEvent.cs
index dacf84565..c1a1f606e 100644
--- a/SharedLibraryCore/Events/GameEvent.cs
+++ b/SharedLibraryCore/Events/GameEvent.cs
@@ -5,10 +5,11 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore.Database.Models;
+using SharedLibraryCore.Events;
namespace SharedLibraryCore
{
- public class GameEvent
+ public class GameEvent : CoreEvent
{
public enum EventFailReason
{
@@ -133,6 +134,8 @@ namespace SharedLibraryCore
/// connection was restored to a server (the server began responding again)
///
ConnectionRestored,
+
+ SayTeam = 99,
// events "generated" by clients
///
@@ -246,7 +249,7 @@ namespace SharedLibraryCore
/// team info printed out by game script
///
JoinTeam = 304,
-
+
///
/// used for community generated plugin events
///
@@ -267,7 +270,7 @@ namespace SharedLibraryCore
public GameEvent()
{
Time = DateTime.UtcNow;
- Id = GetNextEventId();
+ IncrementalId = GetNextEventId();
}
~GameEvent()
@@ -275,8 +278,6 @@ namespace SharedLibraryCore
_eventFinishedWaiter.Dispose();
}
- public EventSource Source { get; set; }
-
///
/// suptype of the event for more detailed classification
///
@@ -293,11 +294,10 @@ namespace SharedLibraryCore
public bool IsRemote { get; set; }
public object Extra { get; set; }
public DateTime Time { get; set; }
- public long Id { get; }
+ public long IncrementalId { get; }
public EventFailReason FailReason { get; set; }
public bool Failed => FailReason != EventFailReason.None;
- public Guid CorrelationId { get; set; } = Guid.NewGuid();
- public List Output { get; set; } = new List();
+ public List Output { get; set; } = new();
///
/// Indicates if the event should block until it is complete
@@ -328,23 +328,31 @@ namespace SharedLibraryCore
/// waitable task
public async Task WaitAsync(TimeSpan timeSpan, CancellationToken token)
{
- var processed = false;
- Utilities.DefaultLogger.LogDebug("Begin wait for event {Id}", Id);
- try
+ if (FailReason == EventFailReason.Timeout)
{
- processed = await _eventFinishedWaiter.WaitAsync(timeSpan, token);
+ return this;
}
- catch (TaskCanceledException)
+ var processed = false;
+ Utilities.DefaultLogger.LogDebug("Begin wait for {Name}, {Type}, {Id}", Type, GetType().Name,
+ IncrementalId);
+
+ try
{
+ await _eventFinishedWaiter.WaitAsync(timeSpan, token);
processed = true;
}
+ catch (OperationCanceledException)
+ {
+ processed = false;
+ }
+
finally
{
- if (_eventFinishedWaiter.CurrentCount == 0)
+ if (processed)
{
- _eventFinishedWaiter.Release();
+ Complete();
}
}
diff --git a/SharedLibraryCore/Events/Management/ClientPersistentIdReceiveEvent.cs b/SharedLibraryCore/Events/Management/ClientPersistentIdReceiveEvent.cs
new file mode 100644
index 000000000..e8c896ad6
--- /dev/null
+++ b/SharedLibraryCore/Events/Management/ClientPersistentIdReceiveEvent.cs
@@ -0,0 +1,14 @@
+using SharedLibraryCore.Database.Models;
+
+namespace SharedLibraryCore.Events.Management;
+
+public class ClientPersistentIdReceiveEvent : ClientStateEvent
+{
+ public ClientPersistentIdReceiveEvent(EFClient client, string persistentId)
+ {
+ Client = client;
+ PersistentId = persistentId;
+ }
+
+ public string PersistentId { get; init; }
+}
diff --git a/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs b/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs
new file mode 100644
index 000000000..b4807712e
--- /dev/null
+++ b/SharedLibraryCore/Interfaces/Events/IGameEventSubscriptions.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using SharedLibraryCore.Events;
+using SharedLibraryCore.Events.Game;
+
+namespace SharedLibraryCore.Interfaces.Events;
+
+public interface IGameEventSubscriptions
+{
+ ///
+ /// Raised when game log prints that match has started
+ /// InitGame
+ ///
+ ///
+ static event Func MatchStarted;
+
+ ///
+ /// Raised when game log prints that match has ended
+ /// ShutdownGame:
+ ///
+ ///
+ static event Func MatchEnded;
+
+ ///
+ /// Raised when game log printed that client has entered the match
+ /// J;clientNetworkId;clientSlotNumber;clientName
+ /// J;110000100000000;0;bot
+ ///
+ ///
+ public static event Func ClientEnteredMatch;
+
+ ///
+ /// Raised when game log prints that client has exited the match
+ /// Q;clientNetworkId;clientSlotNumber;clientName
+ /// Q;110000100000000;0;bot
+ ///
+ ///
+ static event Func ClientExitedMatch;
+
+ ///
+ /// Raised when game log prints that client has joined a team
+ /// JT;clientNetworkId;clientSlotNumber;clientTeam;clientName
+ /// JT;110000100000000;0;axis;bot
+ ///
+ ///
+ static event Func ClientJoinedTeam;
+
+ ///
+ /// Raised when game log prints that client has been damaged
+ /// D;victimNetworkId;victimSlotNumber;victimTeam;victimName;attackerNetworkId;attackerSlotNumber;attackerTeam;attackerName;weapon;damage;meansOfDeath;hitLocation
+ /// D;110000100000000;17;axis;bot_0;110000100000001;4;allies;bot_1;scar_mp;38;MOD_HEAD_SHOT;head
+ ///
+ ///
+ static event Func ClientDamaged;
+
+ ///
+ /// Raised when game log prints that client has been killed
+ /// K;victimNetworkId;victimSlotNumber;victimTeam;victimName;attackerNetworkId;attackerSlotNumber;attackerTeam;attackerName;weapon;damage;meansOfDeath;hitLocation
+ /// K;110000100000000;17;axis;bot_0;110000100000001;4;allies;bot_1;scar_mp;100;MOD_HEAD_SHOT;head
+ ///
+ ///
+ static event Func ClientKilled;
+
+ ///
+ /// Raised when game log prints that client entered a chat message
+ /// say;clientNetworkId;clientSlotNumber;clientName;message
+ /// say;110000100000000;0;bot;hello world!
+ ///
+ ///
+ static event Func ClientMessaged;
+
+ ///
+ /// Raised when game log prints that client entered a command (chat message prefixed with command character(s))
+ /// say;clientNetworkId;clientSlotNumber;clientName;command
+ /// say;110000100000000;0;bot;!command
+ ///
+ ///
+ static event Func ClientEnteredCommand;
+
+ ///
+ /// Raised when game log prints user generated script event
+ /// GSE;data
+ /// GSE;loadBank=1
+ ///
+ ///
+ static event Func ScriptEventTriggered;
+
+ static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token)
+ {
+ return coreEvent switch
+ {
+ MatchStartEvent matchStartEvent => MatchStarted?.InvokeAsync(matchStartEvent, token) ?? Task.CompletedTask,
+ MatchEndEvent matchEndEvent => MatchEnded?.InvokeAsync(matchEndEvent, token) ?? Task.CompletedTask,
+ ClientEnterMatchEvent clientEnterMatchEvent => ClientEnteredMatch?.InvokeAsync(clientEnterMatchEvent, token) ?? Task.CompletedTask,
+ ClientExitMatchEvent clientExitMatchEvent => ClientExitedMatch?.InvokeAsync(clientExitMatchEvent, token) ?? Task.CompletedTask,
+ ClientJoinTeamEvent clientJoinTeamEvent => ClientJoinedTeam?.InvokeAsync(clientJoinTeamEvent, token) ?? Task.CompletedTask,
+ ClientKillEvent clientKillEvent => ClientKilled?.InvokeAsync(clientKillEvent, token) ?? Task.CompletedTask,
+ ClientDamageEvent clientDamageEvent => ClientDamaged?.InvokeAsync(clientDamageEvent, token) ?? Task.CompletedTask,
+ ClientCommandEvent clientCommandEvent => ClientEnteredCommand?.InvokeAsync(clientCommandEvent, token) ?? Task.CompletedTask,
+ ClientMessageEvent clientMessageEvent => ClientMessaged?.InvokeAsync(clientMessageEvent, token) ?? Task.CompletedTask,
+ GameScriptEvent gameScriptEvent => ScriptEventTriggered?.InvokeAsync(gameScriptEvent, token) ?? Task.CompletedTask,
+ _ => Task.CompletedTask
+ };
+ }
+
+ static void ClearEventInvocations()
+ {
+ MatchStarted = null;
+ MatchEnded = null;
+ ClientEnteredMatch = null;
+ ClientExitedMatch = null;
+ ClientJoinedTeam = null;
+ ClientDamaged = null;
+ ClientKilled = null;
+ ClientMessaged = null;
+ ClientEnteredCommand = null;
+ ScriptEventTriggered = null;
+ }
+}
diff --git a/SharedLibraryCore/Interfaces/Events/IGameServerEventSubscriptions.cs b/SharedLibraryCore/Interfaces/Events/IGameServerEventSubscriptions.cs
new file mode 100644
index 000000000..b750ecbfc
--- /dev/null
+++ b/SharedLibraryCore/Interfaces/Events/IGameServerEventSubscriptions.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using SharedLibraryCore.Events;
+using SharedLibraryCore.Events.Server;
+
+namespace SharedLibraryCore.Interfaces.Events;
+
+public interface IGameServerEventSubscriptions
+{
+ ///
+ /// Raised when IW4MAdmin starts monitoring a game server
+ ///
+ ///
+ static event Func MonitoringStarted;
+
+ ///
+ /// Raised when IW4MAdmin stops monitoring a game server
+ ///
+ ///
+ static event Func MonitoringStopped;
+
+ ///
+ /// Raised when communication was interrupted with a game server
+ ///
+ ///
+ static event Func ConnectionInterrupted;
+
+ ///
+ /// Raised when communication was resumed with a game server
+ ///
+ ///
+ static event Func ConnectionRestored;
+
+ ///
+ /// Raised when updated client data was received from a game server
+ ///
+ ///
+ static event Func ClientDataUpdated;
+
+ ///
+ /// Raised when a command was executed on a game server
+ ///
+ ///
+ static event Func ServerCommandExecuted;
+
+ ///
+ /// Raised when a server value is requested for a game server
+ ///
+ ///
+ static event Func ServerValueRequested;
+
+ ///
+ /// Raised when a server value was received from a game server (success or fail)
+ ///
+ ///
+ static event Func ServerValueReceived;
+
+ ///
+ /// Raised when a request to set a server value on a game server is received
+ ///
+ ///
+ static event Func ServerValueSetRequested;
+
+ ///
+ /// Raised when a setting server value on a game server is completed (success or fail)
+ ///
+ ///
+ static event Func ServerValueSetCompleted;
+
+ static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token)
+ {
+ return coreEvent switch
+ {
+ MonitorStartEvent monitoringStartEvent => MonitoringStarted?.InvokeAsync(monitoringStartEvent, token) ?? Task.CompletedTask,
+ MonitorStopEvent monitorStopEvent => MonitoringStopped?.InvokeAsync(monitorStopEvent, token) ?? Task.CompletedTask,
+ ConnectionInterruptEvent connectionInterruptEvent => ConnectionInterrupted?.InvokeAsync(connectionInterruptEvent, token) ?? Task.CompletedTask,
+ ConnectionRestoreEvent connectionRestoreEvent => ConnectionRestored?.InvokeAsync(connectionRestoreEvent, token) ?? Task.CompletedTask,
+ ClientDataUpdateEvent clientDataUpdateEvent => ClientDataUpdated?.InvokeAsync(clientDataUpdateEvent, token) ?? Task.CompletedTask,
+ ServerCommandExecuteEvent dataReceiveEvent => ServerCommandExecuted?.InvokeAsync(dataReceiveEvent, token) ?? Task.CompletedTask,
+ ServerValueRequestEvent serverValueRequestEvent => ServerValueRequested?.InvokeAsync(serverValueRequestEvent, token) ?? Task.CompletedTask,
+ ServerValueReceiveEvent serverValueReceiveEvent => ServerValueReceived?.InvokeAsync(serverValueReceiveEvent, token) ?? Task.CompletedTask,
+ ServerValueSetRequestEvent serverValueSetRequestEvent => ServerValueSetRequested?.InvokeAsync(serverValueSetRequestEvent, token) ?? Task.CompletedTask,
+ ServerValueSetCompleteEvent serverValueSetCompleteEvent => ServerValueSetCompleted?.InvokeAsync(serverValueSetCompleteEvent, token) ?? Task.CompletedTask,
+ _ => Task.CompletedTask
+ };
+ }
+
+ static void ClearEventInvocations()
+ {
+ MonitoringStarted = null;
+ MonitoringStopped = null;
+ ConnectionInterrupted = null;
+ ConnectionRestored = null;
+ ClientDataUpdated = null;
+ ServerCommandExecuted = null;
+ ServerValueReceived = null;
+ ServerValueRequested = null;
+ ServerValueSetRequested = null;
+ ServerValueSetCompleted = null;
+ }
+}
diff --git a/SharedLibraryCore/Interfaces/Events/IManagementEventSubscriptions.cs b/SharedLibraryCore/Interfaces/Events/IManagementEventSubscriptions.cs
new file mode 100644
index 000000000..45779b2a1
--- /dev/null
+++ b/SharedLibraryCore/Interfaces/Events/IManagementEventSubscriptions.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using SharedLibraryCore.Events;
+using SharedLibraryCore.Events.Management;
+
+namespace SharedLibraryCore.Interfaces.Events;
+
+public interface IManagementEventSubscriptions
+{
+ ///
+ /// Raised when is loading
+ ///
+ static event Func Load;
+
+ ///
+ /// Raised when is restarting
+ ///
+ static event Func Unload;
+
+ ///
+ /// Raised when client enters a tracked state
+ ///
+ /// At this point, the client is not guaranteed to be allowed to play on the server.
+ /// See for final state.
+ ///
+ ///
+ ///
+ static event Func ClientStateInitialized;
+
+ ///
+ /// Raised when client enters an authorized state (valid data and no bans)
+ ///
+ ///
+ static event Func ClientStateAuthorized;
+
+ ///
+ /// Raised when client is no longer tracked (unknown state)
+ /// At this point any references to the client should be dropped
+ ///
+ ///
+ static event Func ClientStateDisposed;
+
+ ///
+ /// Raised when a client receives a penalty
+ ///
+ ///
+ static event Func ClientPenaltyAdministered;
+
+ ///
+ /// Raised when a client penalty is revoked (eg unflag/unban)
+ ///
+ ///
+ static event Func ClientPenaltyRevoked;
+
+ ///
+ /// Raised when a client command is executed (after completion of the command)
+ ///
+ ///
+ static event Func ClientCommandExecuted;
+
+ ///
+ /// Raised when a client's permission level changes
+ ///
+ ///
+ static event Func ClientPermissionChanged;
+
+ ///
+ /// Raised when a client logs in to the webfront or ingame
+ ///
+ ///
+ static event Func ClientLoggedIn;
+
+ ///
+ /// Raised when a client logs out of the webfront
+ ///
+ ///
+ static event Func ClientLoggedOut;
+
+ ///
+ /// Raised when a client's persistent id (stats file marker) is received
+ ///
+ ///
+ static event Func ClientPersistentIdReceived;
+
+ static Task InvokeEventAsync(CoreEvent coreEvent, CancellationToken token)
+ {
+ return coreEvent switch
+ {
+ ClientStateInitializeEvent clientStateInitializeEvent => ClientStateInitialized?.InvokeAsync(
+ clientStateInitializeEvent, token) ?? Task.CompletedTask,
+ ClientStateDisposeEvent clientStateDisposedEvent => ClientStateDisposed?.InvokeAsync(
+ clientStateDisposedEvent, token) ?? Task.CompletedTask,
+ ClientStateAuthorizeEvent clientStateAuthorizeEvent => ClientStateAuthorized?.InvokeAsync(
+ clientStateAuthorizeEvent, token) ?? Task.CompletedTask,
+ ClientPenaltyRevokeEvent clientPenaltyRevokeEvent => ClientPenaltyRevoked?.InvokeAsync(
+ clientPenaltyRevokeEvent, token) ?? Task.CompletedTask,
+ ClientPenaltyEvent clientPenaltyEvent =>
+ ClientPenaltyAdministered?.InvokeAsync(clientPenaltyEvent, token) ?? Task.CompletedTask,
+ ClientPermissionChangeEvent clientPermissionChangeEvent => ClientPermissionChanged?.InvokeAsync(
+ clientPermissionChangeEvent, token) ?? Task.CompletedTask,
+ ClientExecuteCommandEvent clientExecuteCommandEvent => ClientCommandExecuted?.InvokeAsync(
+ clientExecuteCommandEvent, token) ?? Task.CompletedTask,
+ LogoutEvent logoutEvent => ClientLoggedOut?.InvokeAsync(logoutEvent, token) ?? Task.CompletedTask,
+ LoginEvent loginEvent => ClientLoggedIn?.InvokeAsync(loginEvent, token) ?? Task.CompletedTask,
+ ClientPersistentIdReceiveEvent clientPersistentIdReceiveEvent => ClientPersistentIdReceived?.InvokeAsync(
+ clientPersistentIdReceiveEvent, token) ?? Task.CompletedTask,
+ _ => Task.CompletedTask
+ };
+ }
+
+ static Task InvokeLoadAsync(IManager manager, CancellationToken token) => Load?.InvokeAsync(manager, token) ?? Task.CompletedTask;
+ static Task InvokeUnloadAsync(IManager manager, CancellationToken token) => Unload?.InvokeAsync(manager, token) ?? Task.CompletedTask;
+
+ static void ClearEventInvocations()
+ {
+ Load = null;
+ Unload = null;
+ ClientStateInitialized = null;
+ ClientStateAuthorized = null;
+ ClientStateDisposed = null;
+ ClientPenaltyAdministered = null;
+ ClientPenaltyRevoked = null;
+ ClientCommandExecuted = null;
+ ClientPermissionChanged = null;
+ ClientLoggedIn = null;
+ ClientLoggedOut = null;
+ ClientPersistentIdReceived = null;
+ }
+}
diff --git a/SharedLibraryCore/Interfaces/ICoreEventHandler.cs b/SharedLibraryCore/Interfaces/ICoreEventHandler.cs
new file mode 100644
index 000000000..7e264d8be
--- /dev/null
+++ b/SharedLibraryCore/Interfaces/ICoreEventHandler.cs
@@ -0,0 +1,19 @@
+using System.Threading;
+using SharedLibraryCore.Events;
+
+namespace SharedLibraryCore.Interfaces;
+
+///
+/// Handles games events (from log, manual events, etc)
+///
+public interface ICoreEventHandler
+{
+ ///
+ /// Add a core event event to the queue to be processed
+ ///
+ ///
+ ///
+ void QueueEvent(IManager manager, CoreEvent coreEvent);
+
+ void StartProcessing(CancellationToken token);
+}
diff --git a/SharedLibraryCore/Interfaces/IGameServer.cs b/SharedLibraryCore/Interfaces/IGameServer.cs
index 707ee865c..907b68659 100644
--- a/SharedLibraryCore/Interfaces/IGameServer.cs
+++ b/SharedLibraryCore/Interfaces/IGameServer.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using Data.Models;
using SharedLibraryCore.Database.Models;
@@ -16,7 +17,70 @@ namespace SharedLibraryCore.Interfaces
/// previous penalty the kick is occuring for (if applicable)
///
Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null);
+
+ ///
+ /// Time the most recent match ended
+ ///
DateTime? MatchEndTime { get; }
+
+ ///
+ /// Time the current match started
+ ///
DateTime? MatchStartTime { get; }
+
+ ///
+ /// List of connected clients
+ ///
+ IReadOnlyList ConnectedClients { get; }
+
+ ///
+ /// Game code corresponding to the development studio project
+ ///
+ Reference.Game GameCode { get; }
+
+ ///
+ /// Indicates if the anticheat/custom callbacks/live radar integration is enabled
+ ///
+ bool IsLegacyGameIntegrationEnabled { get; }
+
+ ///
+ /// Unique identifier for the server (typically ip:port)
+ ///
+ string Id { get; }
+
+ ///
+ /// Network address the server is listening on
+ ///
+ string ListenAddress { get; }
+
+ ///
+ /// Network port the server is listening on
+ ///
+ int ListenPort { get; }
+
+ ///
+ /// Name of the server (hostname)
+ ///
+ string ServerName { get; }
+
+ ///
+ /// Current gametype
+ ///
+ string Gametype { get; }
+
+ ///
+ /// Game password (required to join)
+ ///
+ string GamePassword { get; }
+
+ ///
+ /// Current map the game server is running
+ ///
+ Map Map { get; }
+
+ ///
+ /// Database id for EFServer table and references
+ ///
+ long LegacyDatabaseId { get; }
}
}
diff --git a/SharedLibraryCore/Interfaces/IManager.cs b/SharedLibraryCore/Interfaces/IManager.cs
index 049266ae7..3f0b4c6ec 100644
--- a/SharedLibraryCore/Interfaces/IManager.cs
+++ b/SharedLibraryCore/Interfaces/IManager.cs
@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
+using SharedLibraryCore.Events;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Services;
@@ -34,7 +35,7 @@ namespace SharedLibraryCore.Interfaces
Task Init();
Task Start();
Task Stop();
- void Restart();
+ Task Restart();
[Obsolete]
ILogger GetLogger(long serverId);
@@ -87,6 +88,11 @@ namespace SharedLibraryCore.Interfaces
/// event to be processed
void AddEvent(GameEvent gameEvent);
+ ///
+ /// queues an event for processing
+ ///
+ void QueueEvent(CoreEvent coreEvent);
+
///
/// adds an additional (script) command to the command list
///
diff --git a/SharedLibraryCore/Interfaces/IModularAssembly.cs b/SharedLibraryCore/Interfaces/IModularAssembly.cs
new file mode 100644
index 000000000..a6c96c727
--- /dev/null
+++ b/SharedLibraryCore/Interfaces/IModularAssembly.cs
@@ -0,0 +1,11 @@
+namespace SharedLibraryCore.Interfaces;
+
+public interface IModularAssembly
+{
+ string Name { get; }
+ string Author { get; }
+ string Version { get; }
+ string Scope => string.Empty;
+ string Role => string.Empty;
+ string[] Claims => System.Array.Empty();
+}
diff --git a/SharedLibraryCore/Interfaces/IRConParser.cs b/SharedLibraryCore/Interfaces/IRConParser.cs
index 3737fdbac..deee71d63 100644
--- a/SharedLibraryCore/Interfaces/IRConParser.cs
+++ b/SharedLibraryCore/Interfaces/IRConParser.cs
@@ -55,8 +55,6 @@ namespace SharedLibraryCore.Interfaces
///
///
Task> GetDvarAsync(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default);
-
- void BeginGetDvar(IRConConnection connection, string dvarName, AsyncCallback callback, CancellationToken token = default);
///
/// set value of DVAR by name
@@ -67,9 +65,7 @@ namespace SharedLibraryCore.Interfaces
///
///
Task SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default);
-
- void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback, CancellationToken token = default);
-
+
///
/// executes a console command on the server
///
diff --git a/SharedLibraryCore/PartialEntities/EFClient.cs b/SharedLibraryCore/PartialEntities/EFClient.cs
index a4a881e72..bfa35930d 100644
--- a/SharedLibraryCore/PartialEntities/EFClient.cs
+++ b/SharedLibraryCore/PartialEntities/EFClient.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Data.Models;
using Microsoft.Extensions.Logging;
using Serilog.Context;
+using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Localization;
namespace SharedLibraryCore.Database.Models
@@ -603,7 +604,13 @@ namespace SharedLibraryCore.Database.Models
LastConnection = DateTime.UtcNow;
Utilities.DefaultLogger.LogInformation("Client {client} is leaving the game", ToString());
-
+
+ CurrentServer?.Manager.QueueEvent(new ClientStateDisposeEvent
+ {
+ Source = CurrentServer,
+ Client = this
+ });
+
try
{
await CurrentServer.Manager.GetClientService().Update(this);
@@ -658,6 +665,11 @@ namespace SharedLibraryCore.Database.Models
};
CurrentServer.Manager.AddEvent(e);
+ CurrentServer.Manager.QueueEvent(new ClientStateAuthorizeEvent
+ {
+ Source = CurrentServer,
+ Client = this
+ });
}
}
diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs
index fd5f704e2..c3e5009ae 100644
--- a/SharedLibraryCore/Server.cs
+++ b/SharedLibraryCore/Server.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Threading;
@@ -63,7 +64,7 @@ namespace SharedLibraryCore
{
Password = config.Password;
IP = config.IPAddress;
- Port = config.Port;
+ ListenPort = config.Port;
Manager = mgr;
#pragma warning disable CS0612
Logger = deprecatedLogger ?? throw new ArgumentNullException(nameof(deprecatedLogger));
@@ -89,8 +90,6 @@ namespace SharedLibraryCore
? Convert.ToInt64($"{ListenAddress!.Replace(".", "")}{ListenPort}")
: $"{ListenAddress!.Replace(".", "")}{ListenPort}".GetStableHashCode();
- public long LegacyEndpoint => EndPoint;
-
public abstract long LegacyDatabaseId { get; }
public string Id => $"{ListenAddress}:{ListenPort}";
@@ -105,6 +104,7 @@ namespace SharedLibraryCore
public List ChatHistory { get; protected set; }
public ClientHistoryInfo ClientHistory { get; }
public Game GameName { get; set; }
+ public Reference.Game GameCode => (Reference.Game)GameName;
public DateTime? MatchEndTime { get; protected set; }
public DateTime? MatchStartTime { get; protected set; }
@@ -114,14 +114,17 @@ namespace SharedLibraryCore
protected set => hostname = value;
}
+ public string ServerName => Hostname;
+
public string Website { get; protected set; }
public string Gametype { get; set; }
- public string GametypeName => DefaultSettings.Gametypes.FirstOrDefault(gt => gt.Game == GameName)?.Gametypes
+ public string GametypeName => DefaultSettings.Gametypes?.FirstOrDefault(gt => gt.Game == GameName)?.Gametypes
?.FirstOrDefault(gt => gt.Name == Gametype)?.Alias ?? Gametype;
public string GamePassword { get; protected set; }
public Map CurrentMap { get; set; }
+ public Map Map => CurrentMap;
public int ClientNum
{
@@ -130,9 +133,13 @@ namespace SharedLibraryCore
public int MaxClients { get; protected set; }
public List Clients { get; protected set; }
+
+ public IReadOnlyList ConnectedClients =>
+ new ReadOnlyCollection(GetClientsAsList());
public string Password { get; }
public bool Throttled { get; protected set; }
public bool CustomCallback { get; protected set; }
+ public bool IsLegacyGameIntegrationEnabled => CustomCallback;
public string WorkingDirectory { get; protected set; }
public IRConConnection RemoteConnection { get; protected set; }
public IRConParser RconParser { get; set; }
@@ -143,7 +150,7 @@ namespace SharedLibraryCore
// Internal
///
- /// this is actually the hostname now
+ /// this is actually the listen address now
///
public string IP { get; protected set; }
@@ -153,7 +160,7 @@ namespace SharedLibraryCore
public string Version { get; protected set; }
public bool IsInitialized { get; set; }
- public int Port { get; }
+ public int ListenPort { get; }
public abstract Task Kick(string reason, EFClient target, EFClient origin, EFPenalty originalPenalty);
///
@@ -416,35 +423,6 @@ namespace SharedLibraryCore
public abstract Task GetIdForServer(Server server = null);
- public string GetServerDvar(string dvarName, int timeoutMs = 1000)
- {
- using var tokenSource = new CancellationTokenSource();
- tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs));
- try
- {
- return this.GetDvarAsync(dvarName, token: tokenSource.Token).GetAwaiter().GetResult().Value;
- }
- catch
- {
- return null;
- }
- }
-
- public bool SetServerDvar(string dvarName, string dvarValue, int timeoutMs = 1000)
- {
- using var tokenSource = new CancellationTokenSource();
- tokenSource.CancelAfter(TimeSpan.FromSeconds(timeoutMs));
- try
- {
- this.SetDvarAsync(dvarName, dvarValue, tokenSource.Token).GetAwaiter().GetResult();
- return true;
- }
- catch
- {
- return false;
- }
- }
-
public EFClient GetClientByNumber(int clientNumber) =>
GetClientsAsList().FirstOrDefault(client => client.ClientNumber == clientNumber);
}
diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj
index b173fb3df..7c6d139e9 100644
--- a/SharedLibraryCore/SharedLibraryCore.csproj
+++ b/SharedLibraryCore/SharedLibraryCore.csproj
@@ -4,22 +4,22 @@
Library
net6.0
RaidMax.IW4MAdmin.SharedLibraryCore
- 2022.10.13.1
+ 2023.4.5.1
RaidMax
Forever None
Debug;Release;Prerelease
false
- default
+ Preview
IW4MAdmin
https://github.com/RaidMax/IW4M-Admin/
https://www.raidmax.org/IW4MAdmin/
- 2022
+ 2023
true
true
true
MIT
Shared Library for IW4MAdmin
- 2022.10.13.1
+ 2023.4.5.1
true
$(NoWarn);1591
diff --git a/SharedLibraryCore/Utilities.cs b/SharedLibraryCore/Utilities.cs
index fc5ae1b1e..f0ceafa62 100644
--- a/SharedLibraryCore/Utilities.cs
+++ b/SharedLibraryCore/Utilities.cs
@@ -14,10 +14,13 @@ using System.Threading.Tasks;
using Data.Models;
using Humanizer;
using Humanizer.Localisation;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta;
+using SharedLibraryCore.Events.Server;
+using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Localization;
@@ -26,7 +29,6 @@ using static SharedLibraryCore.Server;
using static Data.Models.Client.EFClient;
using static Data.Models.EFPenalty;
using ILogger = Microsoft.Extensions.Logging.ILogger;
-using RegionInfo = System.Globalization.RegionInfo;
namespace SharedLibraryCore
{
@@ -43,7 +45,7 @@ namespace SharedLibraryCore
public static Encoding EncodingType;
public static Layout CurrentLocalization = new Layout(new Dictionary());
- public static TimeSpan DefaultCommandTimeout { get; set; } = new(0, 0, Utilities.IsDevelopment ? 360 : 25);
+ public static TimeSpan DefaultCommandTimeout { get; set; } = new(0, 0, /*Utilities.IsDevelopment ? 360 : */25);
public static char[] DirectorySeparatorChars = { '\\', '/' };
public static char CommandPrefix { get; set; } = '!';
@@ -66,6 +68,22 @@ namespace SharedLibraryCore
};
}
+ public static EFClient AsConsoleClient(this IGameServer server)
+ {
+ return new EFClient
+ {
+ ClientId = 1,
+ State = EFClient.ClientState.Connected,
+ Level = Permission.Console,
+ CurrentServer = server as Server,
+ CurrentAlias = new EFAlias
+ {
+ Name = "IW4MAdmin"
+ },
+ AdministeredPenalties = new List()
+ };
+ }
+
///
/// fallback id for world events
///
@@ -95,14 +113,19 @@ namespace SharedLibraryCore
///
/// caps client name to the specified character length - 3
- /// and adds ellipses to the end of the reamining client name
+ /// and adds ellipses to the end of the remaining client name
///
- /// client name
+ /// client name
/// max number of characters for the name
///
- public static string CapClientName(this string str, int maxLength)
+ public static string CapClientName(this string name, int maxLength)
{
- return str.Length > maxLength ? $"{str.Substring(0, maxLength - 3)}..." : str;
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ return "-";
+ }
+
+ return name.Length > maxLength ? $"{name[..(maxLength - 3)]}..." : name;
}
public static Permission MatchPermission(string str)
@@ -712,15 +735,21 @@ namespace SharedLibraryCore
public static Dictionary DictionaryFromKeyValue(this string eventLine)
{
- var values = eventLine.Substring(1).Split('\\');
+ var values = eventLine[1..].Split('\\');
- Dictionary dict = null;
+ Dictionary dict = new();
- if (values.Length > 1)
+ if (values.Length <= 1)
{
- dict = new Dictionary();
- for (var i = values.Length % 2 == 0 ? 0 : 1; i < values.Length; i += 2)
+ return dict;
+ }
+
+ for (var i = values.Length % 2 == 0 ? 0 : 1; i < values.Length; i += 2)
+ {
+ if (!dict.ContainsKey(values[i]))
+ {
dict.Add(values[i], values[i + 1]);
+ }
}
return dict;
@@ -771,11 +800,6 @@ namespace SharedLibraryCore
{
return await server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue, token);
}
-
- public static void BeginGetDvar(this Server server, string dvarName, AsyncCallback callback, CancellationToken token = default)
- {
- server.RconParser.BeginGetDvar(server.RemoteConnection, dvarName, callback, token);
- }
public static async Task> GetDvarAsync(this Server server, string dvarName,
T fallbackValue = default)
@@ -807,30 +831,36 @@ namespace SharedLibraryCore
return await server.GetDvarAsync(mappedKey, defaultValue, token: token);
}
- public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue, CancellationToken token = default)
+ public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue,
+ CancellationToken token)
{
await server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue, token);
}
- public static void BeginSetDvar(this Server server, string dvarName, object dvarValue,
- AsyncCallback callback, CancellationToken token = default)
- {
- server.RconParser.BeginSetDvar(server.RemoteConnection, dvarName, dvarValue, callback, token);
- }
-
public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue)
{
await SetDvarAsync(server, dvarName, dvarValue, default);
}
- public static async Task ExecuteCommandAsync(this Server server, string commandName, CancellationToken token = default)
+ public static async Task ExecuteCommandAsync(this Server server, string commandName,
+ CancellationToken token)
{
- return await server.RconParser.ExecuteCommandAsync(server.RemoteConnection, commandName, token);
+ var response = await server.RconParser.ExecuteCommandAsync(server.RemoteConnection, commandName, token);
+
+ server.Manager.QueueEvent(new ServerCommandExecuteEvent
+ {
+ Server = server,
+ Source = server,
+ Command = commandName,
+ Output = response
+ });
+
+ return response;
}
-
+
public static async Task ExecuteCommandAsync(this Server server, string commandName)
{
- return await ExecuteCommandAsync(server, commandName, default);
+ return await ExecuteCommandAsync(server, commandName, default);
}
public static async Task GetStatusAsync(this Server server, CancellationToken token)
@@ -1262,5 +1292,44 @@ namespace SharedLibraryCore
public static string MakeAbbreviation(string gameName) => string.Join("",
gameName.Split(' ').Select(word => char.ToUpper(word.First())).ToArray());
+
+ public static IServiceCollection AddConfiguration(
+ this IServiceCollection serviceCollection, string fileName = null, TConfigurationType defaultConfig = null)
+ where TConfigurationType : class
+ {
+ serviceCollection.AddSingleton(serviceProvider =>
+ {
+ var configurationHandler =
+ serviceProvider.GetRequiredService>();
+
+ var configuration =
+ Task.Run(() => configurationHandler.Get(fileName ?? typeof(TConfigurationType).Name, defaultConfig))
+ .GetAwaiter().GetResult();
+
+ if (typeof(TConfigurationType).GetInterface(nameof(IBaseConfiguration)) is not null &&
+ defaultConfig is null && configuration is null)
+ {
+ defaultConfig =
+ (TConfigurationType)((IBaseConfiguration)Activator.CreateInstance())
+ .Generate();
+ }
+
+ if (defaultConfig is not null && configuration is null)
+ {
+ Task.Run(() => configurationHandler.Set(defaultConfig)).GetAwaiter().GetResult();
+ configuration = defaultConfig;
+ }
+
+ if (configuration is null)
+ {
+ throw new ConfigurationException(
+ $"Could not register configuration {typeof(TConfigurationType).Name}. Configuration file does not exist and no default configuration was provided.");
+ }
+
+ return configuration;
+ });
+
+ return serviceCollection;
+ }
}
}
diff --git a/WebfrontCore/Controllers/API/ClientController.cs b/WebfrontCore/Controllers/API/ClientController.cs
index 503725029..764e26621 100644
--- a/WebfrontCore/Controllers/API/ClientController.cs
+++ b/WebfrontCore/Controllers/API/ClientController.cs
@@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
+using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Services;
using WebfrontCore.Controllers.API.Dtos;
@@ -136,6 +137,16 @@ namespace WebfrontCore.Controllers.API
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress.ToString()
});
+
+ Manager.QueueEvent(new LoginEvent
+ {
+ Source = this,
+ LoginSource = LoginEvent.LoginSourceType.Webfront,
+ EntityId = Client.ClientId.ToString(),
+ Identifier = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
+ ? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
+ : HttpContext.Connection.RemoteIpAddress?.ToString()
+ });
return Ok();
}
@@ -165,6 +176,16 @@ namespace WebfrontCore.Controllers.API
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress.ToString()
});
+
+ Manager.QueueEvent(new LogoutEvent
+ {
+ Source = this,
+ LoginSource = LoginEvent.LoginSourceType.Webfront,
+ EntityId = Client.ClientId.ToString(),
+ Identifier = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
+ ? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
+ : HttpContext.Connection.RemoteIpAddress?.ToString()
+ });
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
diff --git a/WebfrontCore/Controllers/API/Server.cs b/WebfrontCore/Controllers/API/Server.cs
index ab1324bcc..66ca820aa 100644
--- a/WebfrontCore/Controllers/API/Server.cs
+++ b/WebfrontCore/Controllers/API/Server.cs
@@ -91,7 +91,7 @@ namespace WebfrontCore.Controllers.API
var start = DateTime.Now;
Client.CurrentServer = foundServer;
- var commandEvent = new GameEvent()
+ var commandEvent = new GameEvent
{
Type = GameEvent.EventType.Command,
Owner = foundServer,
diff --git a/WebfrontCore/Controllers/AboutController.cs b/WebfrontCore/Controllers/AboutController.cs
index 2928a2aa5..8119491c5 100644
--- a/WebfrontCore/Controllers/AboutController.cs
+++ b/WebfrontCore/Controllers/AboutController.cs
@@ -47,4 +47,4 @@ namespace WebfrontCore.Controllers
return View(info);
}
}
-}
\ No newline at end of file
+}
diff --git a/WebfrontCore/Controllers/AccountController.cs b/WebfrontCore/Controllers/AccountController.cs
index 01b21a96b..a436b16d7 100644
--- a/WebfrontCore/Controllers/AccountController.cs
+++ b/WebfrontCore/Controllers/AccountController.cs
@@ -7,6 +7,7 @@ using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
+using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Helpers;
namespace WebfrontCore.Controllers
@@ -72,6 +73,16 @@ namespace WebfrontCore.Controllers
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress?.ToString()
});
+
+ Manager.QueueEvent(new LoginEvent
+ {
+ Source = this,
+ LoginSource = LoginEvent.LoginSourceType.Webfront,
+ EntityId = privilegedClient.ClientId.ToString(),
+ Identifier = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
+ ? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
+ : HttpContext.Connection.RemoteIpAddress?.ToString()
+ });
return Ok(Localization["WEBFRONT_ACTION_LOGIN_SUCCESS"].FormatExt(privilegedClient.CleanedName));
}
@@ -99,6 +110,16 @@ namespace WebfrontCore.Controllers
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress?.ToString()
});
+
+ Manager.QueueEvent(new LogoutEvent
+ {
+ Source = this,
+ LoginSource = LoginEvent.LoginSourceType.Webfront,
+ EntityId = Client.ClientId.ToString(),
+ Identifier = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
+ ? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
+ : HttpContext.Connection.RemoteIpAddress?.ToString()
+ });
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
diff --git a/WebfrontCore/Controllers/Client/ClientController.cs b/WebfrontCore/Controllers/Client/ClientController.cs
index 27e2d33e3..033774d0e 100644
--- a/WebfrontCore/Controllers/Client/ClientController.cs
+++ b/WebfrontCore/Controllers/Client/ClientController.cs
@@ -7,7 +7,6 @@ using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
diff --git a/WebfrontCore/Controllers/Client/ClientStatisticsController.cs b/WebfrontCore/Controllers/Client/ClientStatisticsController.cs
index a49d8c3ca..55de0e4eb 100644
--- a/WebfrontCore/Controllers/Client/ClientStatisticsController.cs
+++ b/WebfrontCore/Controllers/Client/ClientStatisticsController.cs
@@ -1,7 +1,6 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using IW4MAdmin.Plugins.Stats.Helpers;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
@@ -41,12 +40,12 @@ namespace WebfrontCore.Controllers
return NotFound();
}
- var server = Manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
+ var server = Manager.GetServers().FirstOrDefault(server => server.Id == serverId) as IGameServer;
long? matchedServerId = null;
if (server != null)
{
- matchedServerId = StatManager.GetIdForServer(server);
+ matchedServerId = server.LegacyDatabaseId;
}
hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
diff --git a/WebfrontCore/Program.cs b/WebfrontCore/Program.cs
index 728b4db47..12e6784bf 100644
--- a/WebfrontCore/Program.cs
+++ b/WebfrontCore/Program.cs
@@ -3,9 +3,8 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Serilog;
+using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using WebfrontCore.Middleware;
@@ -14,33 +13,36 @@ namespace WebfrontCore
public class Program
{
public static IManager Manager;
- public static IServiceCollection Services;
- public static IServiceProvider ApplicationServiceProvider;
+ private static IWebHost _webHost;
- public static Task Init(IManager mgr, IServiceProvider existingServiceProvider, IServiceCollection services, CancellationToken cancellationToken)
+ public static IServiceProvider InitializeServices(Action registerDependenciesAction, string bindUrl)
{
- Services = services;
- Manager = mgr;
- ApplicationServiceProvider = existingServiceProvider;
- var config = Manager.GetApplicationSettings().Configuration();
- Manager.MiddlewareActionHandler.Register(null, new CustomCssAccentMiddlewareAction("#007ACC", "#fd7e14", config.WebfrontPrimaryColor, config.WebfrontSecondaryColor), "custom_css_accent");
- return BuildWebHost().RunAsync(cancellationToken);
+ _webHost = BuildWebHost(registerDependenciesAction, bindUrl);
+ Manager = _webHost.Services.GetRequiredService();
+ return _webHost.Services;
}
- private static IWebHost BuildWebHost()
+ public static Task GetWebHostTask(CancellationToken cancellationToken)
+ {
+ var config = _webHost.Services.GetRequiredService();
+ Manager.MiddlewareActionHandler.Register(null,
+ new CustomCssAccentMiddlewareAction("#007ACC", "#fd7e14", config.WebfrontPrimaryColor,
+ config.WebfrontSecondaryColor), "custom_css_accent");
+
+ return _webHost?.RunAsync(cancellationToken);
+ }
+
+ private static IWebHost BuildWebHost(Action registerDependenciesAction, string bindUrl)
{
- var config = new ConfigurationBuilder()
- .AddEnvironmentVariables()
- .Build();
-
return new WebHostBuilder()
#if DEBUG
.UseContentRoot(Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), @"..\..\..\..\", "WebfrontCore")))
#else
.UseContentRoot(SharedLibraryCore.Utilities.OperatingDirectory)
#endif
- .UseUrls(Manager.GetApplicationSettings().Configuration().WebfrontBindUrl)
+ .UseUrls(bindUrl)
.UseKestrel()
+ .ConfigureServices(registerDependenciesAction)
.UseStartup()
.Build();
}
diff --git a/WebfrontCore/Startup.cs b/WebfrontCore/Startup.cs
index 9c8e44077..2eb8ddd8f 100644
--- a/WebfrontCore/Startup.cs
+++ b/WebfrontCore/Startup.cs
@@ -24,6 +24,7 @@ using System.Reflection;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Helpers;
+using IW4MAdmin.Plugins.Stats.Helpers;
using Stats.Client.Abstractions;
using Stats.Config;
using WebfrontCore.Controllers.API.Validation;
@@ -64,23 +65,8 @@ namespace WebfrontCore
}
// Add framework services.
- var mvcBuilder = services.AddMvc(_options => _options.SuppressAsyncSuffixInActionNames = false)
- .AddFluentValidation()
- .ConfigureApplicationPartManager(_partManager =>
- {
- foreach (var assembly in pluginAssemblies())
- {
- if (assembly.FullName.Contains("Views"))
- {
- _partManager.ApplicationParts.Add(new CompiledRazorAssemblyPart(assembly));
- }
-
- else if (assembly.FullName.Contains("Web"))
- {
- _partManager.ApplicationParts.Add(new AssemblyPart(assembly));
- }
- }
- });
+ var mvcBuilder = services.AddMvc(options => options.SuppressAsyncSuffixInActionNames = false);
+ services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters();
#if DEBUG
{
@@ -109,42 +95,13 @@ namespace WebfrontCore
options.Events.OnSignedIn += ClaimsPermissionRemoval.OnSignedIn;
});
- services.AddSingleton(Program.Manager);
services.AddSingleton, ChatResourceQueryHelper>();
services.AddTransient, FindClientRequestValidator>();
services.AddSingleton, ClientService>();
services.AddSingleton, StatsResourceQueryHelper>();
services.AddSingleton, AdvancedClientStatsResourceQueryHelper>();
- services.AddScoped(sp =>
- Program.ApplicationServiceProvider
- .GetRequiredService>());
services.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>));
- // todo: this needs to be handled more gracefully
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService>());
-#pragma warning disable CS0618
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
-#pragma warning restore CS0618
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton, BanInfoResourceQueryHelper>();
- services.AddSingleton(
- Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider
- .GetRequiredService>());
- services.AddSingleton(Program.ApplicationServiceProvider
- .GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider
- .GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
- services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService());
+ services.AddSingleton, BanInfoResourceQueryHelper>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.