Compare commits

..

No commits in common. "release/pre" and "2022.10.17.2-prerelease" have entirely different histories.

297 changed files with 7566 additions and 23756 deletions

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using IW4MAdmin.Application.Plugin;
using IW4MAdmin.Application.Misc;
using Newtonsoft.Json;
using RestEase;
using SharedLibraryCore.Helpers;

View File

@ -2,7 +2,6 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration;
@ -16,7 +15,6 @@ public class AlertManager : IAlertManager
private readonly ApplicationConfiguration _appConfig;
private readonly ConcurrentDictionary<int, List<Alert.AlertState>> _states = new();
private readonly List<Func<Task<IEnumerable<Alert.AlertState>>>> _staticSources = new();
private readonly SemaphoreSlim _onModifyingAlerts = new(1, 1);
public AlertManager(ApplicationConfiguration appConfig)
{
@ -40,9 +38,8 @@ public class AlertManager : IAlertManager
public IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client)
{
try
lock (_states)
{
_onModifyingAlerts.Wait();
var alerts = Enumerable.Empty<Alert.AlertState>();
if (client.Level > Data.Models.Client.EFClient.Permission.Trusted)
{
@ -55,22 +52,14 @@ public class AlertManager : IAlertManager
alerts = alerts.Concat(_states[client.ClientId].AsReadOnly());
}
return alerts.OrderByDescending(alert => alert.OccuredAt).ToList();
}
finally
{
if (_onModifyingAlerts.CurrentCount == 0)
{
_onModifyingAlerts.Release(1);
}
return alerts.OrderByDescending(alert => alert.OccuredAt);
}
}
public void MarkAlertAsRead(Guid alertId)
{
try
lock (_states)
{
_onModifyingAlerts.Wait();
foreach (var items in _states.Values)
{
var matchingEvent = items.FirstOrDefault(item => item.AlertId == alertId);
@ -84,20 +73,12 @@ public class AlertManager : IAlertManager
OnAlertConsumed?.Invoke(this, matchingEvent);
}
}
finally
{
if (_onModifyingAlerts.CurrentCount == 0)
{
_onModifyingAlerts.Release(1);
}
}
}
public void MarkAllAlertsAsRead(int recipientId)
{
try
lock (_states)
{
_onModifyingAlerts.Wait();
foreach (var items in _states.Values)
{
items.RemoveAll(item =>
@ -112,20 +93,12 @@ public class AlertManager : IAlertManager
});
}
}
finally
{
if (_onModifyingAlerts.CurrentCount == 0)
{
_onModifyingAlerts.Release(1);
}
}
}
public void AddAlert(Alert.AlertState alert)
{
try
lock (_states)
{
_onModifyingAlerts.Wait();
if (alert.RecipientId is null)
{
_states[0].Add(alert);
@ -146,14 +119,6 @@ public class AlertManager : IAlertManager
PruneOldAlerts();
}
finally
{
if (_onModifyingAlerts.CurrentCount == 0)
{
_onModifyingAlerts.Release(1);
}
}
}
public void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource)

View File

@ -24,7 +24,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2049" />
<PackageReference Include="Jint" Version="3.0.0-beta-2041" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<PrivateAssets>all</PrivateAssets>
@ -32,13 +32,11 @@
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="RestEase" Version="1.5.7" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.CommandLine.DragonFruit" Version="0.4.0-alpha.22272.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup>
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation>
<LangVersion>Latest</LangVersion>

View File

@ -16,7 +16,6 @@ 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;
@ -24,18 +23,11 @@ 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;
@ -57,7 +49,7 @@ namespace IW4MAdmin.Application
public IList<Func<GameEvent, bool>> CommandInterceptors { get; set; } =
new List<Func<GameEvent, bool>>();
public ITokenAuthentication TokenAuthenticator { get; }
public CancellationToken CancellationToken => _isRunningTokenSource.Token;
public CancellationToken CancellationToken => _tokenSource.Token;
public string ExternalIPAddress { get; private set; }
public bool IsRestartRequested { get; private set; }
public IMiddlewareActionHandler MiddlewareActionHandler { get; }
@ -71,30 +63,29 @@ namespace IW4MAdmin.Application
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private CancellationTokenSource _isRunningTokenSource;
private CancellationTokenSource _eventHandlerTokenSource;
private CancellationTokenSource _tokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
private readonly IGameServerInstanceFactory _serverInstanceFactory;
private readonly IParserRegexFactory _parserRegexFactory;
private readonly IEnumerable<IRegisterEvent> _customParserEvents;
private readonly ICoreEventHandler _coreEventHandler;
private readonly IEventHandler _eventHandler;
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<long, GameEvent> ProcessingEvents { get; } = new();
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new ConcurrentDictionary<long, GameEvent>();
public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
ICoreEventHandler coreEventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable<IPluginV2> v2PLugins)
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
@ -109,14 +100,14 @@ namespace IW4MAdmin.Application
AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication();
_logger = logger;
_isRunningTokenSource = new CancellationTokenSource();
_tokenSource = new CancellationTokenSource();
_commands = commands.ToList();
_translationLookup = translationLookup;
_commandConfiguration = commandConfiguration;
_serverInstanceFactory = serverInstanceFactory;
_parserRegexFactory = parserRegexFactory;
_customParserEvents = customParserEvents;
_coreEventHandler = coreEventHandler;
_eventHandler = eventHandler;
_scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver;
@ -125,8 +116,6 @@ namespace IW4MAdmin.Application
_appConfig = appConfig;
Plugins = plugins;
InteractionRegistration = interactionRegistration;
IManagementEventSubscriptions.ClientPersistentIdReceived += OnClientPersistentIdReceived;
}
public IEnumerable<IPlugin> Plugins { get; }
@ -134,7 +123,7 @@ namespace IW4MAdmin.Application
public async Task ExecuteEvent(GameEvent newEvent)
{
ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent);
ProcessingEvents.TryAdd(newEvent.Id, newEvent);
// the event has failed already
if (newEvent.Failed)
@ -152,12 +141,12 @@ namespace IW4MAdmin.Application
catch (TaskCanceledException)
{
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
_logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
}
catch (OperationCanceledException)
{
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
_logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
}
// this happens if a plugin requires login
@ -196,11 +185,11 @@ namespace IW4MAdmin.Application
}
skip:
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null && newEvent.CorrelationId is not null)
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null)
{
var correlatedEvents =
ProcessingEvents.Values.Where(ev =>
ev.CorrelationId == newEvent.CorrelationId && ev.IncrementalId != newEvent.IncrementalId)
ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id)
.ToList();
await Task.WhenAll(correlatedEvents.Select(ev =>
@ -209,16 +198,14 @@ namespace IW4MAdmin.Application
foreach (var correlatedEvent in correlatedEvents)
{
ProcessingEvents.Remove(correlatedEvent.IncrementalId, out _);
ProcessingEvents.Remove(correlatedEvent.Id, out _);
}
}
// we don't want to remove events that are correlated to command
if (ProcessingEvents.Values.Count(gameEvent =>
newEvent.CorrelationId is not null && newEvent.CorrelationId == gameEvent.CorrelationId) == 1 ||
newEvent.CorrelationId is null)
if (ProcessingEvents.Values.ToList()?.Count(gameEvent => gameEvent.CorrelationId == newEvent.CorrelationId) == 1)
{
ProcessingEvents.Remove(newEvent.IncrementalId, out _);
ProcessingEvents.Remove(newEvent.Id, out _);
}
// tell anyone waiting for the output that we're done
@ -238,58 +225,77 @@ namespace IW4MAdmin.Application
public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList();
private Task UpdateServerStates()
public async Task UpdateServerStates()
{
var index = 0;
return Task.WhenAll(_servers.Select(server =>
// store the server hash code and task for it
var runningUpdateTasks = new Dictionary<long, (Task task, CancellationTokenSource tokenSource, DateTime startTime)>();
while (!_tokenSource.IsCancellationRequested)
{
var thisIndex = index;
Interlocked.Increment(ref index);
return ProcessUpdateHandler(server, thisIndex);
}));
// select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks
.Where(ut => ut.Value.task.Status == TaskStatus.RanToCompletion ||
ut.Value.task.Status == TaskStatus.Canceled || // we want to cancel if a task takes longer than 5 minutes
ut.Value.task.Status == TaskStatus.Faulted || DateTime.Now - ut.Value.startTime > TimeSpan.FromMinutes(5))
.Select(ut => ut.Key)
.ToList();
// 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();
}
private async Task ProcessUpdateHandler(Server server, int index)
{
const int delayScalar = 50; // Task.Delay is inconsistent enough there's no reason to try to prevent collisions
var timeout = TimeSpan.FromMinutes(2);
runningUpdateTasks.Remove(serverId);
}
while (!_isRunningTokenSource.IsCancellationRequested)
// select the servers where the tasks have completed
var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
{
var tokenSource = new CancellationTokenSource();
runningUpdateTasks.Add(server.EndPoint, (Task.Run(async () =>
{
try
{
var delayFactor = Math.Min(_appConfig.RConPollRate, delayScalar * index);
await Task.Delay(delayFactor, _isRunningTokenSource.Token);
if (runningUpdateTasks.ContainsKey(server.EndPoint))
{
await server.ProcessUpdatesAsync(_tokenSource.Token)
.WithWaitCancellation(runningUpdateTasks[server.EndPoint].tokenSource.Token);
}
}
using var timeoutTokenSource = new CancellationTokenSource();
timeoutTokenSource.CancelAfter(timeout);
using var linkedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token,
_isRunningTokenSource.Token);
await server.ProcessUpdatesAsync(linkedTokenSource.Token);
catch (Exception e)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(e, "Failed to update status");
}
}
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;
}
}, tokenSource.Token), tokenSource, DateTime.Now));
}
// run the final updates to clean up server
await server.ProcessUpdatesAsync(_isRunningTokenSource.Token);
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 => serverIds.Contains(s.EndPoint)))
{
await server.ProcessUpdatesAsync(_tokenSource.Token);
}
break;
}
}
}
public async Task Init()
@ -300,34 +306,25 @@ namespace IW4MAdmin.Application
#region DATABASE
_logger.LogInformation("Beginning database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]);
await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _isRunningTokenSource.Token);
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _isRunningTokenSource.Token);
await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
_logger.LogInformation("Finished database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
#endregion
#region EVENTS
IGameServerEventSubscriptions.ServerValueRequested += OnServerValueRequested;
IGameServerEventSubscriptions.ServerValueSetRequested += OnServerValueSetRequested;
IGameServerEventSubscriptions.ServerCommandExecuteRequested += OnServerCommandExecuteRequested;
await IManagementEventSubscriptions.InvokeLoadAsync(this, CancellationToken);
# endregion
#region PLUGINS
foreach (var plugin in Plugins)
{
try
{
if (plugin is ScriptPlugin scriptPlugin && !plugin.IsParser)
if (plugin is ScriptPlugin scriptPlugin)
{
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
scriptPlugin.Watcher.Changed += async (sender, e) =>
{
try
{
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
}
catch (Exception ex)
@ -393,11 +390,13 @@ 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,9 +441,9 @@ namespace IW4MAdmin.Application
serverConfig.ModifyParsers();
}
}
await ConfigHandler.Save();
}
}
if (_appConfig.Servers.Length == 0)
{
@ -468,7 +467,7 @@ namespace IW4MAdmin.Application
#endregion
#region COMMANDS
if (await ClientSvc.HasOwnerAsync(_isRunningTokenSource.Token))
if (await ClientSvc.HasOwnerAsync(_tokenSource.Token))
{
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
}
@ -543,23 +542,26 @@ 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);
}
QueueEvent(new MonitorStartEvent
// add the start event for this server
var e = new GameEvent()
{
Server = serverInstance,
Source = this
});
Type = EventType.Start,
Data = $"{ServerInstance.GameName} started",
Owner = ServerInstance
};
AddEvent(e);
successServers++;
}
@ -581,36 +583,20 @@ namespace IW4MAdmin.Application
throw lastException;
}
if (successServers != config.Servers.Length && !AppContext.TryGetSwitch("NoConfirmPrompt", out _))
if (successServers != config.Servers.Length)
{
if (!Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"].PromptBool())
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
{
throw lastException;
}
}
}
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 Start() => await UpdateServerStates();
public async Task Stop()
{
foreach (var plugin in Plugins.Where(plugin => !plugin.IsParser))
foreach (var plugin in Plugins)
{
try
{
@ -622,30 +608,16 @@ namespace IW4MAdmin.Application
}
}
_isRunningTokenSource.Cancel();
_tokenSource.Cancel();
IsRunning = false;
}
public async Task Restart()
public void Restart()
{
IsRestartRequested = true;
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();
Stop().GetAwaiter().GetResult();
_tokenSource = new CancellationTokenSource();
}
[Obsolete]
@ -687,12 +659,7 @@ namespace IW4MAdmin.Application
public void AddEvent(GameEvent gameEvent)
{
_coreEventHandler.QueueEvent(this, gameEvent);
}
public void QueueEvent(CoreEvent coreEvent)
{
_coreEventHandler.QueueEvent(this, coreEvent);
_eventHandler.HandleEvent(this, gameEvent);
}
public IPageList GetPageList()
@ -729,166 +696,15 @@ namespace IW4MAdmin.Application
public void AddAdditionalCommand(IManagerCommand command)
{
lock (_commands)
if (_commands.Any(_command => _command.Name == command.Name || _command.Alias == 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})");
throw new InvalidOperationException($"Duplicate command name or alias ({command.Name}, {command.Alias})");
}
_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<string> 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<string> { Name = requestEvent.ValueName },
Success = serverValue is not null
});
}
}
private Task OnServerValueSetRequested(ServerValueSetRequestEvent requestEvent, CancellationToken token)
{
return ExecuteWrapperForServerQuery(requestEvent, token, async (innerEvent) =>
{
if (innerEvent.DelayMs.HasValue)
{
await Task.Delay(innerEvent.DelayMs.Value, token);
}
if (innerEvent.TimeoutMs is not null)
{
using var timeoutTokenSource = new CancellationTokenSource(innerEvent.TimeoutMs.Value);
using var linkedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
token = linkedTokenSource.Token;
}
await innerEvent.Server.SetDvarAsync(innerEvent.ValueName, innerEvent.Value, token);
}, (completed, innerEvent) =>
{
QueueEvent(new ServerValueSetCompleteEvent
{
Server = innerEvent.Server,
Source = innerEvent.Server,
Success = completed,
Value = innerEvent.Value,
ValueName = innerEvent.ValueName
});
return Task.CompletedTask;
});
}
private Task OnServerCommandExecuteRequested(ServerCommandRequestExecuteEvent executeEvent, CancellationToken token)
{
return ExecuteWrapperForServerQuery(executeEvent, token, async (innerEvent) =>
{
if (innerEvent.DelayMs.HasValue)
{
await Task.Delay(innerEvent.DelayMs.Value, token);
}
if (innerEvent.TimeoutMs is not null)
{
using var timeoutTokenSource = new CancellationTokenSource(innerEvent.TimeoutMs.Value);
using var linkedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, token);
token = linkedTokenSource.Token;
}
await innerEvent.Server.ExecuteCommandAsync(innerEvent.Command, token);
}, (_, __) => Task.CompletedTask);
}
private async Task ExecuteWrapperForServerQuery<TEventType>(TEventType serverEvent, CancellationToken token,
Func<TEventType, Task> action, Func<bool, TEventType, Task> complete) where TEventType : GameServerEvent
{
if (serverEvent.Server is not IW4MServer)
{
return;
}
var completed = false;
try
{
await action(serverEvent);
completed = true;
}
catch
{
// ignored
}
finally
{
await complete(completed, serverEvent);
}
}
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);
}
}
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -34,7 +33,7 @@ namespace IW4MAdmin.Application.Commands
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
public override Task ExecuteAsync(GameEvent gameEvent)
{
var searchTerm = gameEvent.Data.Trim();
var availableCommands = gameEvent.Owner.Manager.Commands.Distinct().Where(command =>
@ -71,25 +70,24 @@ namespace IW4MAdmin.Application.Commands
});
var helpResponse = new StringBuilder();
var messageList = new List<string>();
foreach (var item in commandStrings)
{
helpResponse.Append(item.response);
if (item.index == 0 || item.index % 4 != 0)
{
continue;
}
messageList.Add(helpResponse.ToString());
gameEvent.Origin.Tell(helpResponse.ToString());
helpResponse = new StringBuilder();
}
messageList.Add(helpResponse.ToString());
gameEvent.Origin.Tell(helpResponse.ToString());
gameEvent.Origin.Tell(_translationLookup["COMMANDS_HELP_MOREINFO"]);
}
await gameEvent.Origin.TellAsync(messageList);
}
return Task.CompletedTask;
}
}
}

View File

@ -49,13 +49,20 @@ namespace IW4MAdmin.Application.Commands
return;
}
await gameEvent.Origin.TellAsync(inboxItems.Select((inboxItem, index) =>
var index = 1;
foreach (var inboxItem in inboxItems)
{
var header = _translationLookup["COMMANDS_READ_MESSAGE_SUCCESS"]
.FormatExt($"{index + 1}/{inboxItems.Count}", inboxItem.SourceClient.CurrentAlias.Name);
await gameEvent.Origin.Tell(_translationLookup["COMMANDS_READ_MESSAGE_SUCCESS"]
.FormatExt($"{index}/{inboxItems.Count}", inboxItem.SourceClient.CurrentAlias.Name))
.WaitAsync();
return new[] { header }.Union(inboxItem.Message.FragmentMessageForDisplay());
}).SelectMany(item => item));
foreach (var messageFragment in inboxItem.Message.FragmentMessageForDisplay())
{
await gameEvent.Origin.Tell(messageFragment).WaitAsync();
}
index++;
}
inboxItems.ForEach(item => { item.IsDelivered = true; });

View File

@ -1,80 +0,0 @@
using System;
using System.Threading.Tasks;
using Data.Models.Client;
using Serilog.Core;
using Serilog.Events;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands;
public class SetLogLevelCommand : Command
{
private readonly Func<string, LoggingLevelSwitch> _levelSwitchResolver;
public SetLogLevelCommand(CommandConfiguration config, ITranslationLookup layout, Func<string, LoggingLevelSwitch> levelSwitchResolver) : base(config, layout)
{
_levelSwitchResolver = levelSwitchResolver;
Name = "loglevel";
Alias = "ll";
Description = "set minimum logging level";
Permission = EFClient.Permission.Owner;
Arguments = new CommandArgument[]
{
new()
{
Name = "Log Level",
Required = true
},
new()
{
Name = "Override",
Required = false
},
new()
{
Name = "IsDevelopment",
Required = false
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var args = gameEvent.Data.Split(" ");
if (!Enum.TryParse<LogEventLevel>(args[0], out var minLevel))
{
await gameEvent.Origin.TellAsync(new[]
{
$"Valid log values: {string.Join(",", Enum.GetValues<LogEventLevel>())}"
});
return;
}
var context = string.Empty;
if (args.Length > 1)
{
context = args[1];
}
var loggingSwitch = _levelSwitchResolver(context);
loggingSwitch.MinimumLevel = minLevel;
if (args.Length > 2 && (args[2] == "1" || args[2].ToLower() == "true"))
{
AppContext.SetSwitch("IsDevelop", true);
}
else
{
AppContext.SetSwitch("IsDevelop", false);
}
await gameEvent.Origin.TellAsync(new[]
{ $"Set minimum log level to {loggingSwitch.MinimumLevel.ToString()}" });
}
}

View File

@ -1,145 +0,0 @@
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<CoreEventHandler> 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);
}
}
}

View File

@ -69,39 +69,6 @@
}
],
"Gametypes": [
{
"Game": "IW3",
"Gametypes": [
{
"Name": "ctf",
"Alias": "Capture The Flag"
},
{
"Name": "dm",
"Alias": "Free For All"
},
{
"Name": "dom",
"Alias": "Domination"
},
{
"Name": "koth",
"Alias": "Headquarters"
},
{
"Name": "sab",
"Alias": "Sabotage"
},
{
"Name": "sd",
"Alias": "Search & Destroy"
},
{
"Name": "war",
"Alias": "Team Deathmatch"
}
]
},
{
"Game": "IW4",
"Gametypes": [
@ -185,10 +152,6 @@
{
"Name": "twar",
"Alias": "War"
},
{
"Name": "cmp",
"Alias": "Zombies"
}
]
},
@ -196,12 +159,12 @@
"Game": "IW5",
"Gametypes": [
{
"Name": "dom",
"Alias": "Domination"
"Name": "tdm",
"Alias": "Team Deathmatch"
},
{
"Name": "conf",
"Alias": "Kill Confirmed"
"Name": "dom",
"Alias": "Domination"
},
{
"Name": "ctf",
@ -212,29 +175,37 @@
"Alias": "Demolition"
},
{
"Name": "dm",
"Alias": "Free For All"
},
{
"Name": "grnd",
"Name": "dz",
"Alias": "Drop Zone"
},
{
"Name": "gun",
"Name": "ffa",
"Alias": "Free For All"
},
{
"Name": "gg",
"Alias": "Gun Game"
},
{
"Name": "hq",
"Alias": "Headquarters"
},
{
"Name": "koth",
"Alias": "Headquarters"
},
{
"Name": "infect",
"Name": "inf",
"Alias": "Infected"
},
{
"Name": "jugg",
"Name": "jug",
"Alias": "Juggernaut"
},
{
"Name": "kc",
"Alias": "Kill Confirmed"
},
{
"Name": "oic",
"Alias": "One In The Chamber"
@ -252,12 +223,8 @@
"Alias": "Team Defender"
},
{
"Name": "tjugg",
"Name": "tj",
"Alias": "Team Juggernaut"
},
{
"Name": "war",
"Alias": "Team Deathmatch"
}
]
},
@ -311,10 +278,6 @@
{
"Name": "tdm",
"Alias": "Team Deathmatch"
},
{
"Name": "zom",
"Alias": "Zombies"
}
]
},
@ -445,14 +408,6 @@
{
"Name": "tdm",
"Alias": "Team Deathmatch"
},
{
"Name": "zclassic",
"Alias": "Zombies Classic"
},
{
"Name": "zstandard",
"Alias": "Zombies"
}
]
},
@ -554,10 +509,6 @@
{
"Name": "hc_tdm",
"Alias": "Hardcore Team Deathmatch"
},
{
"Name": "zclassic",
"Alias": "Zombies Classic"
}
]
},
@ -848,22 +799,6 @@
{
"Alias": "Upheaval",
"Name": "mp_suburban"
},
{
"Alias": "Nacht Der Untoten",
"Name": "nazi_zombie_prototype"
},
{
"Alias": "Verrückt",
"Name": "nazi_zombie_asylum"
},
{
"Alias": "Shi No Numa",
"Name": "nazi_zombie_sumpf"
},
{
"Alias": "Der Riese",
"Name": "nazi_zombie_factory"
}
]
},
@ -1049,82 +984,6 @@
{
"Alias": "Village",
"Name": "co_hunted"
},
{
"Alias": "Broadcast",
"Name": "mp_broadcast"
},
{
"Alias": "Showdown",
"Name": "mp_showdown"
},
{
"Alias": "Ambush",
"Name": "mp_convoy"
},
{
"Alias": "District",
"Name": "mp_citystreets"
},
{
"Alias": "Backlot",
"Name": "mp_backlot"
},
{
"Alias": "Pipeline",
"Name": "mp_pipeline"
},
{
"Alias": "Chinatown",
"Name": "mp_carentan"
},
{
"Alias": "Winter Crash",
"Name": "mp_crash_snow"
},
{
"Alias": "Countdown",
"Name": "mp_countdown"
},
{
"Alias": "Downpour",
"Name": "mp_farm"
},
{
"Alias": "Dome",
"Name": "mp_dome"
},
{
"Alias": "Hardhat",
"Name": "mp_hardhat"
},
{
"Alias": "Resistance",
"Name": "mp_paris"
},
{
"Alias": "Seatown",
"Name": "mp_seatown"
},
{
"Alias": "Mission",
"Name": "mp_bravo"
},
{
"Alias": "Underground",
"Name": "mp_underground"
},
{
"Alias": "Arkaden",
"Name": "mp_plaza2"
},
{
"Alias": "Village",
"Name": "mp_village"
},
{
"Alias": "Lockdown",
"Name": "mp_alpha"
}
]
},
@ -1234,46 +1093,6 @@
{
"Alias": "Zoo",
"Name": "mp_zoo"
},
{
"Alias": "Kino der Toten",
"Name": "zombie_theater"
},
{
"Alias": "Five",
"Name": "zombie_pentagon"
},
{
"Alias": "Ascension",
"Name": "zombie_cosmodrome"
},
{
"Alias": "Call of the Dead",
"Name": "zombie_coast"
},
{
"Alias": "Shangri-La",
"Name": "zombie_temple"
},
{
"Alias": "Moon",
"Name": "zombie_moon"
},
{
"Alias": "Nacht Der Untoten",
"Name": "zombie_cod5_prototype"
},
{
"Alias": "Verrückt",
"Name": "zombie_cod5_asylum"
},
{
"Alias": "Shi No Numa",
"Name": "zombie_cod5_sumpf"
},
{
"Alias": "Der Riese",
"Name": "zombie_cod5_factory"
}
]
},
@ -1721,70 +1540,6 @@
{
"Alias": "Outlaw",
"Name": "mp_western"
},
{
"Alias": "Fringe Night",
"Name": "mp_veiled_heyday"
},
{
"Alias": "Redwood Snow",
"Name": "mp_redwood_ice"
},
{
"Alias": "Shadows of Evil",
"Name": "zm_zod"
},
{
"Alias": "Der Eisendrache",
"Name": "zm_castle"
},
{
"Alias": "Zetsubou No Shima",
"Name": "zm_island"
},
{
"Alias": "Gorod Krovi",
"Name": "zm_stalingrad"
},
{
"Alias": "Revelations",
"Name": "zm_genesis"
},
{
"Alias": "Ascension",
"Name": "zm_cosmodrome"
},
{
"Alias": "Kino der Toten",
"Name": "zm_theater"
},
{
"Alias": "Moon",
"Name": "zm_moon"
},
{
"Alias": "Nacht der Untoten",
"Name": "zm_prototype"
},
{
"Alias": "Origins",
"Name": "zm_tomb"
},
{
"Alias": "Shangri-La",
"Name": "zm_temple"
},
{
"Alias": "Shi No Numa",
"Name": "zm_sumpf"
},
{
"Alias": "The Giant",
"Name": "zm_factory"
},
{
"Alias": "Verrückt",
"Name": "zm_asylum"
}
]
},

View File

@ -7,8 +7,6 @@ using System.Collections.Generic;
using System.Linq;
using Data.Models;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Events.Game;
using static System.Int32;
using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -16,26 +14,21 @@ namespace IW4MAdmin.Application.EventParsers
{
public class BaseEventParser : IEventParser
{
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>
_customEventRegistrations;
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations;
private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig;
private readonly Dictionary<ParserRegex, GameEvent.EventType> _regexMap;
private readonly Dictionary<string, GameEvent.EventType> _eventTypeMap;
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger,
ApplicationConfiguration appConfig)
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig)
{
_customEventRegistrations =
new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
_customEventRegistrations = new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
_logger = logger;
_appConfig = appConfig;
Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
{
GameDirectory = "main",
LocalizeText = "\x15",
};
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);([^;]*);(.*)$";
@ -64,8 +57,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginTeam, 4);
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginName, 5);
Configuration.Damage.Pattern =
@"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -80,8 +72,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Kill.Pattern =
@"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -113,9 +104,9 @@ namespace IW4MAdmin.Application.EventParsers
_eventTypeMap = new Dictionary<string, GameEvent.EventType>
{
{"say", GameEvent.EventType.Say},
{ "sayteam", GameEvent.EventType.SayTeam },
{"sayteam", GameEvent.EventType.Say},
{"chat", GameEvent.EventType.Say},
{ "chatteam", GameEvent.EventType.SayTeam },
{"chatteam", GameEvent.EventType.Say},
{"K", GameEvent.EventType.Kill},
{"D", GameEvent.EventType.Damage},
{"J", GameEvent.EventType.PreConnect},
@ -134,536 +125,13 @@ namespace IW4MAdmin.Application.EventParsers
public string Name { get; set; } = "Call of Duty";
public virtual GameEvent GenerateGameEvent(string logLine)
{
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
var gameTime = 0L;
if (timeMatch.Success)
{
if (timeMatch.Values[0].Contains(':'))
{
gameTime = timeMatch
.Values
.Skip(2)
// this converts the timestamp into seconds passed
.Select((value, index) => long.Parse(value.ToString()) * (index == 0 ? 60 : 1))
.Sum();
}
else
{
gameTime = long.Parse(timeMatch.Values[0]);
}
// we want to strip the time from the log line
logLine = logLine[timeMatch.Values.First().Length..].Trim();
}
var (eventType, eventKey) = GetEventTypeFromLine(logLine);
switch (eventType)
{
case GameEvent.EventType.Say or GameEvent.EventType.SayTeam:
return ParseMessageEvent(logLine, gameTime, eventType) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.Kill:
return ParseKillEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.Damage:
return ParseDamageEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.PreConnect:
return ParseClientEnterMatchEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.JoinTeam:
return ParseJoinTeamEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.PreDisconnect:
return ParseClientExitMatchEvent(logLine, gameTime) ?? GenerateDefaultEvent(logLine, gameTime);
case GameEvent.EventType.MapEnd:
return ParseMatchEndEvent(logLine, gameTime);
case GameEvent.EventType.MapChange:
return ParseMatchStartEvent(logLine, gameTime);
}
if (logLine.StartsWith("GSE;"))
{
return new GameScriptEvent
{
ScriptData = logLine,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
if (eventKey is null || !_customEventRegistrations.ContainsKey(eventKey))
{
return GenerateDefaultEvent(logLine, gameTime);
}
var eventModifier = _customEventRegistrations[eventKey];
try
{
return eventModifier.Item2(logLine, Configuration, new GameEvent()
{
Type = GameEvent.EventType.Other,
Data = logLine,
Subtype = eventModifier.Item1,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not handle custom log event generation");
}
return GenerateDefaultEvent(logLine, gameTime);
}
private static GameEvent GenerateDefaultEvent(string logLine, long gameTime)
{
return new GameEvent
{
Type = GameEvent.EventType.Unknown,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
private static GameEvent ParseMatchStartEvent(string logLine, long gameTime)
{
var dump = logLine.Replace("InitGame: ", "").DictionaryFromKeyValue();
return new MatchStartEvent
{
Type = GameEvent.EventType.MapChange,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
Extra = dump,
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
SessionData = dump
};
}
private static GameEvent ParseMatchEndEvent(string logLine, long gameTime)
{
return new MatchEndEvent
{
Type = GameEvent.EventType.MapEnd,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
SessionData = logLine
};
}
private GameEvent ParseClientExitMatchEvent(string logLine, long gameTime)
{
var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString =
match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var originClientNumber =
Convert.ToInt32(
match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var networkId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new ClientExitMatchEvent
{
Type = GameEvent.EventType.PreDisconnect,
Data = logLine,
Origin = new EFClient
{
CurrentAlias = new EFAlias
{
Name = originName
},
NetworkId = networkId,
ClientNumber = originClientNumber,
State = EFClient.ClientState.Disconnecting
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber
};
}
private GameEvent ParseJoinTeamEvent(string logLine, long gameTime)
{
var match = Configuration.JoinTeam.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString =
match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var team = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginTeam]];
var clientSlotNumber =
Parse(match.Values[
Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
if (Configuration.TeamMapping.ContainsKey(team))
{
team = Configuration.TeamMapping[team].ToString();
}
var networkId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new ClientJoinTeamEvent
{
Type = GameEvent.EventType.JoinTeam,
Data = logLine,
Origin = new EFClient
{
CurrentAlias = new EFAlias
{
Name = originName
},
NetworkId = networkId,
ClientNumber = clientSlotNumber,
State = EFClient.ClientState.Connected,
},
Extra = team,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
TeamName = team,
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = clientSlotNumber
};
}
private GameEvent ParseClientEnterMatchEvent(string logLine, long gameTime)
{
var match = Configuration.Join.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]]
.TrimNewLine();
var originClientNumber =
Convert.ToInt32(
match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var networkId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new ClientEnterMatchEvent
{
Type = GameEvent.EventType.PreConnect,
Data = logLine,
Origin = new EFClient
{
CurrentAlias = new EFAlias
{
Name = originName
},
NetworkId = networkId,
ClientNumber = originClientNumber,
State = EFClient.ClientState.Connecting,
},
Extra = originIdString,
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber
};
}
#region DAMAGE
private GameEvent ParseDamageEvent(string logLine, long gameTime)
{
var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
var originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]]
?.TrimNewLine();
var originId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var targetId = targetIdString.IsBotGuid()
? targetName.GenerateGuidFromString()
: targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var originClientNumber =
Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber =
Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
var originTeamName =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginTeam]];
var targetTeamName =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetTeam]];
if (Configuration.TeamMapping.ContainsKey(originTeamName))
{
originTeamName = Configuration.TeamMapping[originTeamName].ToString();
}
if (Configuration.TeamMapping.ContainsKey(targetTeamName))
{
targetTeamName = Configuration.TeamMapping[targetTeamName].ToString();
}
var weaponName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.Weapon]];
TryParse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.Damage]],
out var damage);
var meansOfDeath =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.MeansOfDeath]];
var hitLocation =
match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.HitLocation]];
return new ClientDamageEvent
{
Type = GameEvent.EventType.Damage,
Data = logLine,
Origin = new EFClient { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber,
AttackerTeamName = originTeamName,
VictimClientName = targetName,
VictimNetworkId = targetIdString,
VictimClientSlotNumber = targetClientNumber,
VictimTeamName = targetTeamName,
WeaponName = weaponName,
Damage = damage,
MeansOfDeath = meansOfDeath,
HitLocation = hitLocation
};
}
private GameEvent ParseKillEvent(string logLine, long gameTime)
{
var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (!match.Success)
{
return null;
}
var originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
var originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]]
?.TrimNewLine();
var originId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var targetId = targetIdString.IsBotGuid()
? targetName.GenerateGuidFromString()
: targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var originClientNumber =
Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber =
Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
var originTeamName =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginTeam]];
var targetTeamName =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetTeam]];
if (Configuration.TeamMapping.ContainsKey(originTeamName))
{
originTeamName = Configuration.TeamMapping[originTeamName].ToString();
}
if (Configuration.TeamMapping.ContainsKey(targetTeamName))
{
targetTeamName = Configuration.TeamMapping[targetTeamName].ToString();
}
var weaponName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.Weapon]];
TryParse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.Damage]],
out var damage);
var meansOfDeath =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.MeansOfDeath]];
var hitLocation =
match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.HitLocation]];
return new ClientKillEvent
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = new EFClient { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
// V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = originClientNumber,
AttackerTeamName = originTeamName,
VictimClientName = targetName,
VictimNetworkId = targetIdString,
VictimClientSlotNumber = targetClientNumber,
VictimTeamName = targetTeamName,
WeaponName = weaponName,
Damage = damage,
MeansOfDeath = meansOfDeath,
HitLocation = hitLocation
};
}
#endregion
#region MESSAGE
private GameEvent ParseMessageEvent(string logLine, long gameTime, GameEvent.EventType eventType)
{
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
if (!matchResult.Success)
{
return null;
}
var message = new string(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.Where(c => !char.IsControl(c)).ToArray());
if (message.StartsWith("/"))
{
message = message[1..];
}
if (String.IsNullOrEmpty(message))
{
return null;
}
var originIdString =
matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]]
?.TrimNewLine();
var clientNumber =
Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var originId = originIdString.IsBotGuid()
? originName.GenerateGuidFromString()
: originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
{
return new ClientCommandEvent
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = new EFClient { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
//V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = clientNumber,
IsTeamMessage = eventType == GameEvent.EventType.SayTeam
};
}
return new ClientMessageEvent
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = new EFClient { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log,
//V2
ClientName = originName,
ClientNetworkId = originIdString,
ClientSlotNumber = clientNumber,
IsTeamMessage = eventType == GameEvent.EventType.SayTeam
};
}
#endregion
private (GameEvent.EventType type, string eventKey) GetEventTypeFromLine(string logLine)
{
var lineSplit = logLine.Split(';');
if (lineSplit.Length > 1)
{
var type = lineSplit[0];
return _eventTypeMap.ContainsKey(type)
? (_eventTypeMap[type], type)
: (GameEvent.EventType.Unknown, lineSplit[0]);
return _eventTypeMap.ContainsKey(type) ? (_eventTypeMap[type], type): (GameEvent.EventType.Unknown, lineSplit[0]);
}
foreach (var (key, value) in _regexMap)
@ -678,9 +146,347 @@ namespace IW4MAdmin.Application.EventParsers
return (GameEvent.EventType.Unknown, null);
}
public virtual GameEvent GenerateGameEvent(string logLine)
{
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
var gameTime = 0L;
if (timeMatch.Success)
{
if (timeMatch.Values[0].Contains(":"))
{
gameTime = timeMatch
.Values
.Skip(2)
// this converts the timestamp into seconds passed
.Select((value, index) => long.Parse(value.ToString()) * (index == 0 ? 60 : 1))
.Sum();
}
else
{
gameTime = long.Parse(timeMatch.Values[0]);
}
// we want to strip the time from the log line
logLine = logLine.Substring(timeMatch.Values.First().Length).Trim();
}
var eventParseResult = GetEventTypeFromLine(logLine);
var eventType = eventParseResult.type;
_logger.LogDebug(logLine);
if (eventType == GameEvent.EventType.Say)
{
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
if (matchResult.Success)
{
var message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.Replace("\x15", "")
.Trim();
if (message.Length > 0)
{
var originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]];
var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
var clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
{
return new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
}
if (eventType == GameEvent.EventType.Kill)
{
var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (match.Success)
{
var originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
var originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]];
var targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]];
var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
if (eventType == GameEvent.EventType.Damage)
{
var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (match.Success)
{
var originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
var originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]];
var targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]];
var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = logLine,
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
if (eventType == GameEvent.EventType.PreConnect)
{
var match = Configuration.Join.PatternMatcher.Match(logLine);
if (match.Success)
{
var originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]];
var networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new GameEvent()
{
Type = GameEvent.EventType.PreConnect,
Data = logLine,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine(),
},
NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]),
State = EFClient.ClientState.Connecting,
},
Extra = originIdString,
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
if (eventType == GameEvent.EventType.JoinTeam)
{
var match = Configuration.JoinTeam.PatternMatcher.Match(logLine);
if (match.Success)
{
var originIdString = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]];
var team = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginTeam]];
if (Configuration.TeamMapping.ContainsKey(team))
{
team = Configuration.TeamMapping[team].ToString();
}
var networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new GameEvent
{
Type = GameEvent.EventType.JoinTeam,
Data = logLine,
Origin = new EFClient
{
CurrentAlias = new EFAlias
{
Name = match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine(),
},
NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.JoinTeam.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]),
State = EFClient.ClientState.Connected,
},
Extra = team,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
if (eventType == GameEvent.EventType.PreDisconnect)
{
var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (match.Success)
{
var originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]];
var networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new GameEvent()
{
Type = GameEvent.EventType.PreDisconnect,
Data = logLine,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine()
},
NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]),
State = EFClient.ClientState.Disconnecting
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
if (eventType == GameEvent.EventType.MapEnd)
{
return new GameEvent()
{
Type = GameEvent.EventType.MapEnd,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
if (eventType == GameEvent.EventType.MapChange)
{
var dump = logLine.Replace("InitGame: ", "");
return new GameEvent()
{
Type = GameEvent.EventType.MapChange,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
Extra = dump.DictionaryFromKeyValue(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
if (eventParseResult.eventKey == null || !_customEventRegistrations.ContainsKey(eventParseResult.eventKey))
{
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
var eventModifier = _customEventRegistrations[eventParseResult.eventKey];
try
{
return eventModifier.Item2(logLine, Configuration, new GameEvent()
{
Type = GameEvent.EventType.Other,
Data = logLine,
Subtype = eventModifier.Item1,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
});
}
catch (Exception e)
{
_logger.LogError(e, "Could not handle custom event generation");
}
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
/// <inheritdoc/>
public void RegisterCustomEvent(string eventSubtype, string eventTriggerValue,
Func<string, IEventParserConfiguration, GameEvent, GameEvent> eventModifier)
public void RegisterCustomEvent(string eventSubtype, string eventTriggerValue, Func<string, IEventParserConfiguration, GameEvent, GameEvent> eventModifier)
{
if (string.IsNullOrWhiteSpace(eventSubtype))
{

View File

@ -14,7 +14,6 @@ namespace IW4MAdmin.Application.EventParsers
{
public string GameDirectory { get; set; }
public ParserRegex Say { get; set; }
public string LocalizeText { get; set; }
public ParserRegex Join { get; set; }
public ParserRegex JoinTeam { get; set; }
public ParserRegex Quit { get; set; }

View File

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces;
using System.Linq;
using IW4MAdmin.Application.Plugin.Script;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;

View File

@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using Data.Models.Client.Stats;
using Microsoft.EntityFrameworkCore;
namespace IW4MAdmin.Application.Extensions;
@ -19,10 +18,4 @@ public static class ScriptPluginExtensions
client.NetworkId
}).ToList();
}
public static IEnumerable<object> GetClientsStatData(this DbSet<EFClientStatistics> set, int[] clientIds,
double serverId)
{
return set.Where(stat => clientIds.Contains(stat.ClientId) && stat.ServerId == (long)serverId).ToList();
}
}

View File

@ -8,7 +8,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
@ -18,10 +17,7 @@ namespace IW4MAdmin.Application.Extensions
{
public static class StartupExtensions
{
private static ILogger _defaultLogger;
private static readonly LoggingLevelSwitch LevelSwitch = new();
private static readonly LoggingLevelSwitch MicrosoftLevelSwitch = new();
private static readonly LoggingLevelSwitch SystemLevelSwitch = new();
private static ILogger _defaultLogger = null;
public static IServiceCollection AddBaseLogger(this IServiceCollection services,
ApplicationConfiguration appConfig)
@ -33,21 +29,14 @@ namespace IW4MAdmin.Application.Extensions
.Build();
var loggerConfig = new LoggerConfiguration()
.ReadFrom.Configuration(configuration);
LevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Default"]);
MicrosoftLevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Override:Microsoft"]);
SystemLevelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(configuration["Serilog:MinimumLevel:Override:System"]);
loggerConfig = loggerConfig.MinimumLevel.ControlledBy(LevelSwitch);
loggerConfig = loggerConfig.MinimumLevel.Override("Microsoft", MicrosoftLevelSwitch)
.MinimumLevel.Override("System", SystemLevelSwitch);
.ReadFrom.Configuration(configuration)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning);
if (Utilities.IsDevelopment)
{
loggerConfig = loggerConfig.WriteTo.Console(
outputTemplate:
"[{Timestamp:HH:mm:ss} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Debug();
}
@ -55,15 +44,6 @@ namespace IW4MAdmin.Application.Extensions
_defaultLogger = loggerConfig.CreateLogger();
}
services.AddSingleton((string context) =>
{
return context.ToLower() switch
{
"microsoft" => MicrosoftLevelSwitch,
"system" => SystemLevelSwitch,
_ => LevelSwitch
};
});
services.AddLogging(builder => builder.AddSerilog(_defaultLogger, dispose: true));
services.AddSingleton(new LoggerFactory()
.AddSerilog(_defaultLogger, true));

View File

@ -1,13 +1,13 @@
using SharedLibraryCore;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -31,11 +31,16 @@ namespace IW4MAdmin.Application.Factories
/// <inheritdoc/>
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission,
bool isTargetRequired, IEnumerable<CommandArgument> args, Func<GameEvent, Task> executeAction, IEnumerable<Reference.Game> supportedGames)
bool isTargetRequired, IEnumerable<(string, bool)> args, Func<GameEvent, Task> executeAction, Server.Game[] supportedGames)
{
var permissionEnum = Enum.Parse<EFClient.Permission>(permission);
var argsArray = args.Select(_arg => new CommandArgument
{
Name = _arg.Item1,
Required = _arg.Item2
}).ToArray();
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, args, executeAction,
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction,
_config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>(), supportedGames);
}
}

View File

@ -0,0 +1,46 @@
using IW4MAdmin.Application.Misc;
using SharedLibraryCore;
using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application
{
public class GameEventHandler : IEventHandler
{
private readonly EventLog _eventLog;
private readonly ILogger _logger;
private readonly IEventPublisher _eventPublisher;
private static readonly GameEvent.EventType[] overrideEvents = new[]
{
GameEvent.EventType.Connect,
GameEvent.EventType.Disconnect,
GameEvent.EventType.Quit,
GameEvent.EventType.Stop
};
public GameEventHandler(ILogger<GameEventHandler> logger, IEventPublisher eventPublisher)
{
_eventLog = new EventLog();
_logger = logger;
_eventPublisher = eventPublisher;
}
public void HandleEvent(IManager manager, GameEvent gameEvent)
{
if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
{
EventApi.OnGameEvent(gameEvent);
_eventPublisher.Publish(gameEvent);
Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent));
}
else
{
_logger.LogDebug("Skipping event as we're shutting down {eventId}", gameEvent.Id);
}
}
}
}

View File

@ -1,213 +0,0 @@
using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.IO;
public class BaseConfigurationHandlerV2<TConfigurationType> : IConfigurationHandlerV2<TConfigurationType>
where TConfigurationType : class
{
private readonly ILogger<BaseConfigurationHandlerV2<TConfigurationType>> _logger;
private readonly ConfigurationWatcher _watcher;
private readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = true,
Converters =
{
new JsonStringEnumConverter()
},
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private readonly SemaphoreSlim _onIo = new(1, 1);
private TConfigurationType _configurationInstance;
private string _path = string.Empty;
private event Action<string> FileUpdated;
public BaseConfigurationHandlerV2(ILogger<BaseConfigurationHandlerV2<TConfigurationType>> logger,
ConfigurationWatcher watcher)
{
_logger = logger;
_watcher = watcher;
FileUpdated += OnFileUpdated;
}
~BaseConfigurationHandlerV2()
{
FileUpdated -= OnFileUpdated;
_watcher.Unregister(_path);
}
public async Task<TConfigurationType> Get(string configurationName,
TConfigurationType defaultConfiguration = default)
{
if (string.IsNullOrWhiteSpace(configurationName))
{
return defaultConfiguration;
}
var cleanName = configurationName.Replace("\\", "").Replace("/", "");
if (string.IsNullOrWhiteSpace(configurationName))
{
return defaultConfiguration;
}
_path = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{cleanName}.json");
TConfigurationType readConfiguration = null;
try
{
await _onIo.WaitAsync();
await using var fileStream = File.OpenRead(_path);
readConfiguration =
await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions);
await fileStream.DisposeAsync();
_watcher.Register(_path, FileUpdated);
if (readConfiguration is null)
{
_logger.LogError("Could not parse configuration {Type} at {FileName}", typeof(TConfigurationType).Name,
_path);
return defaultConfiguration;
}
}
catch (FileNotFoundException)
{
if (defaultConfiguration is not null)
{
await InternalSet(defaultConfiguration, false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not read configuration file at {Path}", _path);
return defaultConfiguration;
}
finally
{
if (_onIo.CurrentCount == 0)
{
_onIo.Release(1);
}
}
return _configurationInstance ??= readConfiguration;
}
public async Task Set(TConfigurationType configuration)
{
await InternalSet(configuration, true);
}
public async Task Set()
{
if (_configurationInstance is not null)
{
await InternalSet(_configurationInstance, true);
}
}
public event Action<TConfigurationType> Updated;
private async Task InternalSet(TConfigurationType configuration, bool awaitSemaphore)
{
try
{
if (awaitSemaphore)
{
await _onIo.WaitAsync();
}
await using var fileStream = File.Create(_path);
await JsonSerializer.SerializeAsync(fileStream, configuration, _serializerOptions);
await fileStream.DisposeAsync();
_configurationInstance = configuration;
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not save configuration {Type} {Path}", configuration.GetType().Name, _path);
}
finally
{
if (awaitSemaphore && _onIo.CurrentCount == 0)
{
_onIo.Release(1);
}
}
}
private async void OnFileUpdated(string filePath)
{
try
{
await _onIo.WaitAsync();
await using var fileStream = File.OpenRead(_path);
var readConfiguration =
await JsonSerializer.DeserializeAsync<TConfigurationType>(fileStream, _serializerOptions);
await fileStream.DisposeAsync();
if (readConfiguration is null)
{
_logger.LogWarning("Could not parse updated configuration {Type} at {Path}",
typeof(TConfigurationType).Name, filePath);
}
else
{
CopyUpdatedProperties(readConfiguration);
Updated?.Invoke(readConfiguration);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not parse updated configuration {Type} at {Path}",
typeof(TConfigurationType).Name, filePath);
}
finally
{
if (_onIo.CurrentCount == 0)
{
_onIo.Release(1);
}
}
}
private void CopyUpdatedProperties(TConfigurationType newConfiguration)
{
if (_configurationInstance is null)
{
_configurationInstance = newConfiguration;
return;
}
_logger.LogDebug("Updating existing config with new values {Type} at {Path}", typeof(TConfigurationType).Name,
_path);
if (_configurationInstance is IDictionary configDict && newConfiguration is IDictionary newConfigDict)
{
configDict.Clear();
foreach (var key in newConfigDict.Keys)
{
configDict.Add(key, newConfigDict[key]);
}
}
else
{
foreach (var property in _configurationInstance.GetType().GetProperties()
.Where(prop => prop.CanRead && prop.CanWrite))
{
property.SetValue(_configurationInstance, property.GetValue(newConfiguration));
}
}
}
}

View File

@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using SharedLibraryCore;
namespace IW4MAdmin.Application.IO;
public sealed class ConfigurationWatcher : IDisposable
{
private readonly FileSystemWatcher _watcher;
private readonly Dictionary<string, Action<string>> _registeredActions = new();
public ConfigurationWatcher()
{
_watcher = new FileSystemWatcher
{
Path = Path.Join(Utilities.OperatingDirectory, "Configuration"),
Filter = "*.json",
NotifyFilter = NotifyFilters.LastWrite
};
_watcher.Changed += WatcherOnChanged;
_watcher.EnableRaisingEvents = true;
}
public void Dispose()
{
_watcher.Changed -= WatcherOnChanged;
_watcher.Dispose();
}
public void Register(string fileName, Action<string> fileUpdated)
{
if (_registeredActions.ContainsKey(fileName))
{
return;
}
_registeredActions.Add(fileName, fileUpdated);
}
public void Unregister(string fileName)
{
if (_registeredActions.ContainsKey(fileName))
{
_registeredActions.Remove(fileName);
}
}
private void WatcherOnChanged(object sender, FileSystemEventArgs eventArgs)
{
if (!_registeredActions.ContainsKey(eventArgs.FullPath) || eventArgs.ChangeType != WatcherChangeTypes.Changed ||
new FileInfo(eventArgs.FullPath).Length == 0)
{
return;
}
_registeredActions[eventArgs.FullPath].Invoke(eventArgs.FullPath);
}
}

View File

@ -9,7 +9,7 @@ using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@ -27,13 +27,8 @@ using Data.Models;
using Data.Models.Server;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Commands;
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
@ -47,13 +42,11 @@ 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<EFServer> _serverCache;
private readonly CommandConfiguration _commandConfiguration;
private EFServer _cachedDatabaseServer;
private readonly StatManager _statManager;
private readonly ApplicationConfiguration _appConfig;
public IW4MServer(
ServerConfiguration serverConfiguration,
@ -77,19 +70,6 @@ namespace IW4MAdmin
_messageFormatter = messageFormatter;
_serverCache = serverCache;
_commandConfiguration = commandConfiguration;
_statManager = serviceProvider.GetRequiredService<StatManager>();
_appConfig = serviceProvider.GetService<ApplicationConfiguration>();
IGameServerEventSubscriptions.MonitoringStarted += async (gameEvent, token) =>
{
if (gameEvent.Server.Id != Id)
{
return;
}
await EnsureServerAdded();
await _statManager.EnsureServerAdded(gameEvent.Server, token);
};
}
public override async Task<EFClient> OnClientConnected(EFClient clientFromLog)
@ -126,7 +106,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,
@ -134,11 +114,6 @@ namespace IW4MAdmin
};
Manager.AddEvent(e);
Manager.QueueEvent(new ClientStateInitializeEvent
{
Client = client,
Source = this,
});
return client;
}
@ -233,17 +208,10 @@ 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.Where(plugin => !plugin.IsParser)
.Select(plugin => CreatePluginTask(plugin, E));
var pluginTasks = Manager.Plugins
.Select(async plugin => await CreatePluginTask(plugin, E));
await Task.WhenAll(pluginTasks);
}
@ -280,7 +248,7 @@ namespace IW4MAdmin
try
{
await plugin.OnEventAsync(gameEvent, this);
await plugin.OnEventAsync(gameEvent, this).WithWaitCancellation(tokenSource.Token);
}
catch (OperationCanceledException)
{
@ -307,7 +275,29 @@ namespace IW4MAdmin
{
ServerLogger.LogDebug("processing event of type {type}", E.Type);
if (E.Type == GameEvent.EventType.ConnectionLost)
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)
{
var exception = E.Extra as Exception;
ServerLogger.LogError(exception,
@ -315,12 +305,12 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{ListenAddress}:{ListenPort}"));
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Error)
.FromSource("System")
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{ListenAddress}:{ListenPort}"))
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
@ -332,16 +322,16 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.ConnectionRestored)
{
ServerLogger.LogInformation(
"Connection restored with {Server}", ToString());
"Connection restored with {server}", ToString());
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{ListenAddress}:{ListenPort}"));
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Information)
.FromSource("System")
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{ListenAddress}:{ListenPort}"))
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
@ -358,18 +348,9 @@ 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)
@ -377,6 +358,7 @@ namespace IW4MAdmin
if (E.Origin.State != ClientState.Connected)
{
E.Origin.State = ClientState.Connected;
E.Origin.LastConnection = DateTime.UtcNow;
E.Origin.Connections += 1;
ChatHistory.Add(new ChatInfo()
@ -516,12 +498,6 @@ 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)
@ -539,14 +515,8 @@ namespace IW4MAdmin
E.Target.SetLevel(Permission.User, E.Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
E.Target.GameName, E.Target.CurrentAlias?.IPAddress, new[] {EFPenalty.PenaltyType.Flag});
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)
@ -582,13 +552,6 @@ 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)
@ -710,7 +673,7 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.MapChange)
{
ServerLogger.LogInformation("New map loaded - {ClientCount} active players", ClientNum);
ServerLogger.LogInformation("New map loaded - {clientCount} active players", ClientNum);
// iw4 doesn't log the game info
if (E.Extra == null)
@ -726,12 +689,12 @@ namespace IW4MAdmin
{
if (dict.ContainsKey("gametype"))
{
UpdateGametype(dict["gametype"]);
Gametype = dict["gametype"];
}
if (dict.ContainsKey("hostname"))
{
UpdateHostname(dict["hostname"]);
Hostname = dict["hostname"];
}
var newMapName = dict.ContainsKey("mapname")
@ -744,30 +707,24 @@ namespace IW4MAdmin
else
{
var dict = (Dictionary<string, string>)E.Extra;
if (dict.ContainsKey("g_gametype"))
{
UpdateGametype(dict["g_gametype"]);
Gametype = dict["g_gametype"];
}
if (dict.ContainsKey("sv_hostname"))
{
UpdateHostname(dict["sv_hostname"]);
Hostname = dict["sv_hostname"];
}
if (dict.ContainsKey("sv_maxclients"))
{
UpdateMaxPlayers(int.Parse(dict["sv_maxclients"]));
MaxClients = int.Parse(dict["sv_maxclients"]);
}
else if (dict.ContainsKey("com_maxclients"))
{
UpdateMaxPlayers(int.Parse(dict["com_maxclients"]));
}
else if (dict.ContainsKey("com_maxplayers"))
{
UpdateMaxPlayers(int.Parse(dict["com_maxplayers"]));
MaxClients = int.Parse(dict["com_maxclients"]);
}
if (dict.ContainsKey("mapname"))
@ -780,8 +737,6 @@ namespace IW4MAdmin
{
lastGameTime = E.GameTime.Value;
}
MatchStartTime = DateTime.Now;
}
else if (E.Type == GameEvent.EventType.MapEnd)
@ -792,8 +747,6 @@ namespace IW4MAdmin
{
lastGameTime = E.GameTime.Value;
}
MatchEndTime = DateTime.Now;
}
else if (E.Type == GameEvent.EventType.Tell)
@ -814,6 +767,34 @@ 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)
{
while (ChatHistory.Count > Math.Ceiling(ClientNum / 2.0))
@ -833,53 +814,6 @@ 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<IDatabaseContextFactory>()
.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);
@ -929,12 +863,6 @@ namespace IW4MAdmin
/// <returns></returns>
async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token)
{
if (DateTime.Now - (MatchEndTime ?? MatchStartTime) < TimeSpan.FromSeconds(15))
{
ServerLogger.LogDebug("Skipping status poll attempt because the match ended recently");
return null;
}
var currentClients = GetClientsAsList();
var statusResponse = await this.GetStatusAsync(token);
@ -970,54 +898,40 @@ namespace IW4MAdmin
{
server ??= this;
return (await _serverCache.FirstAsync(cachedServer =>
cachedServer.EndPoint == server.Id || cachedServer.ServerId == server.EndPoint)).ServerId;
if ($"{server.IP}:{server.Port.ToString()}" == "66.150.121.184:28965")
{
return 886229536;
}
private long BuildLegacyDatabaseId()
{
long id = HashCode.Combine(ListenAddress, ListenPort);
return id < 0 ? Math.Abs(id) : id;
// todo: this is not stable and will need to be migrated again...
long id = HashCode.Combine(server.IP, server.Port);
id = id < 0 ? Math.Abs(id) : id;
var serverId = (await _serverCache
.FirstAsync(_server => _server.ServerId == server.EndPoint ||
_server.EndPoint == server.ToString() ||
_server.ServerId == id))?.ServerId;
return !serverId.HasValue ? id : serverId.Value;
}
private void UpdateMap(string mapName)
private void UpdateMap(string mapname)
{
if (string.IsNullOrEmpty(mapName))
if (!string.IsNullOrEmpty(mapname))
{
return;
}
var foundMap = Maps.Find(m => m.Name == mapName) ?? new Map
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map()
{
Alias = mapName,
Name = mapName
Alias = mapname,
Name = mapname
};
if (foundMap == CurrentMap)
{
return;
}
CurrentMap = foundMap;
using(LogContext.PushProperty("Server", Id))
{
ServerLogger.LogDebug("Updating map to {@CurrentMap}", CurrentMap);
}
}
private void UpdateGametype(string gameType)
{
if (string.IsNullOrEmpty(gameType))
if (!string.IsNullOrEmpty(gameType))
{
return;
}
Gametype = gameType;
using(LogContext.PushProperty("Server", Id))
{
ServerLogger.LogDebug("Updating gametype to {Gametype}", gameType);
}
}
@ -1028,7 +942,7 @@ namespace IW4MAdmin
return;
}
using(LogContext.PushProperty("Server", Id))
using(LogContext.PushProperty("Server", ToString()))
{
ServerLogger.LogDebug("Updating hostname to {HostName}", hostname);
}
@ -1043,7 +957,7 @@ namespace IW4MAdmin
return;
}
using(LogContext.PushProperty("Server", Id))
using(LogContext.PushProperty("Server", ToString()))
{
ServerLogger.LogDebug("Updating max clients to {MaxPlayers}", maxPlayers);
}
@ -1057,7 +971,7 @@ namespace IW4MAdmin
{
await client.OnDisconnect();
var e = new GameEvent
var e = new GameEvent()
{
Type = GameEvent.EventType.Disconnect,
Owner = this,
@ -1068,14 +982,6 @@ 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;
@ -1157,16 +1063,6 @@ namespace IW4MAdmin
Manager.AddEvent(gameEvent);
}
if (polledClients[2].Any())
{
Manager.QueueEvent(new ClientDataUpdateEvent
{
Clients = new ReadOnlyCollection<EFClient>(polledClients[2]),
Server = this,
Source = this,
});
}
if (Throttled)
{
var gameEvent = new GameEvent
@ -1178,12 +1074,6 @@ namespace IW4MAdmin
};
Manager.AddEvent(gameEvent);
Manager.QueueEvent(new ConnectionRestoreEvent
{
Server = this,
Source = this
});
}
LastPoll = DateTime.Now;
@ -1207,12 +1097,6 @@ namespace IW4MAdmin
};
Manager.AddEvent(gameEvent);
Manager.QueueEvent(new ConnectionInterruptEvent
{
Server = this,
Source = this
});
return true;
}
finally
@ -1263,20 +1147,22 @@ namespace IW4MAdmin
ServerLogger.LogError(e, "Unexpected exception occured during processing updates");
}
Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{ListenAddress}:{ListenPort}]"));
Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]"));
return false;
}
}
private void RunServerCollection()
{
if (DateTime.Now - _lastPlayerCount < _appConfig?.ServerDataCollectionInterval)
var appConfig = _serviceProvider.GetService<ApplicationConfiguration>();
if (DateTime.Now - _lastPlayerCount < appConfig?.ServerDataCollectionInterval)
{
return;
}
var maxItems = Math.Ceiling(_appConfig!.MaxClientHistoryTime.TotalMinutes /
_appConfig.ServerDataCollectionInterval.TotalMinutes);
var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes /
appConfig.ServerDataCollectionInterval.TotalMinutes);
while (ClientHistory.ClientCounts.Count > maxItems)
{
@ -1300,13 +1186,13 @@ namespace IW4MAdmin
{
ResolvedIpEndPoint =
new IPEndPoint(
(await Dns.GetHostAddressesAsync(ListenAddress)).First(address =>
address.AddressFamily == AddressFamily.InterNetwork), ListenPort);
(await Dns.GetHostAddressesAsync(IP)).First(address =>
address.AddressFamily == AddressFamily.InterNetwork), Port);
}
catch (Exception ex)
{
ServerLogger.LogWarning(ex, "Could not resolve hostname or IP for RCon connection {Address}:{Port}", ListenAddress, ListenPort);
ResolvedIpEndPoint = new IPEndPoint(IPAddress.Parse(ListenAddress), ListenPort);
ServerLogger.LogWarning(ex, "Could not resolve hostname or IP for RCon connection {IP}:{Port}", IP, Port);
ResolvedIpEndPoint = new IPEndPoint(IPAddress.Parse(IP), Port);
}
RconParser = Manager.AdditionalRConParsers
@ -1363,12 +1249,10 @@ namespace IW4MAdmin
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync", token: Manager.CancellationToken);
var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip", token: Manager.CancellationToken);
var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: "", token: Manager.CancellationToken);
var privateClients = await this.GetMappedDvarValueOrDefaultAsync("sv_privateClients", overrideDefault: 0,
token: Manager.CancellationToken);
if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName)
{
await this.SetDvarAsync("sv_sayname", CustomSayName,
await this.SetDvarAsync("sv_sayname", Manager.GetApplicationSettings().Configuration().CustomSayName,
Manager.CancellationToken);
}
@ -1406,14 +1290,12 @@ namespace IW4MAdmin
}
WorkingDirectory = basepath.Value;
Hostname = hostname;
MaxClients = maxplayers;
FSGame = game.Value;
Gametype = gametype;
IP = ip.Value is "localhost" or "0.0.0.0" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
GamePassword = gamePassword.Value;
PrivateClientSlots = privateClients.Value;
this.Hostname = hostname;
this.MaxClients = maxplayers;
this.FSGame = game.Value;
this.Gametype = gametype;
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
this.GamePassword = gamePassword.Value;
UpdateMap(mapname);
if (RconParser.CanGenerateLogPath && string.IsNullOrEmpty(ServerConfig.ManualLogPath))
@ -1575,12 +1457,6 @@ 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)
@ -1619,20 +1495,8 @@ 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 Task<string[]> ExecuteCommandAsync(string command, CancellationToken token = default) =>
Utilities.ExecuteCommandAsync(this, command, token);
public override Task SetDvarAsync(string name, object value, CancellationToken token = default) =>
Utilities.SetDvarAsync(this, name, value, token);
public override async Task TempBan(string reason, TimeSpan length, EFClient targetClient, EFClient originClient)
{
// ensure player gets kicked if command not performed on them in the same server
@ -1664,12 +1528,6 @@ 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)
@ -1705,12 +1563,6 @@ 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)
@ -1732,12 +1584,6 @@ 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()
@ -1747,7 +1593,5 @@ 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;
}
}

View File

@ -29,21 +29,15 @@ using Data.Helpers;
using Integrations.Source.Extensions;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.IO;
using IW4MAdmin.Application.Localization;
using IW4MAdmin.Application.Plugin;
using IW4MAdmin.Application.Plugin.Script;
using IW4MAdmin.Application.QueryHelpers;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
using IW4MAdmin.Plugins.Stats.Client;
using Microsoft.Extensions.Hosting;
using Stats.Client.Abstractions;
using Stats.Client;
using Stats.Config;
using Stats.Helpers;
using WebfrontCore.QueryHelpers.Models;
namespace IW4MAdmin.Application
{
@ -52,36 +46,15 @@ namespace IW4MAdmin.Application
public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString());
private static ApplicationManager _serverManager;
private static Task _applicationTask;
private static IServiceProvider _serviceProvider;
private static ServiceProvider _serviceProvider;
/// <summary>
/// entrypoint of the application
/// </summary>
/// <returns></returns>
public static async Task Main(bool noConfirm = false, int? maxConcurrentRequests = 25, int? requestQueueLimit = 25)
public static async Task Main(string[] args)
{
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) =>
{
var libraryName = eventArgs.Name.Split(",").First();
var overrides = new[] { nameof(SharedLibraryCore), nameof(Stats) };
if (!overrides.Contains(libraryName))
{
return AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(asm => asm.FullName == eventArgs.Name);
}
// added to be a bit more permissive with plugin references
return AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(asm => asm.FullName?.StartsWith(libraryName) ?? false);
};
if (noConfirm)
{
AppContext.SetSwitch("NoConfirmPrompt", true);
}
Environment.SetEnvironmentVariable("MaxConcurrentRequests", (maxConcurrentRequests * Environment.ProcessorCount).ToString());
Environment.SetEnvironmentVariable("RequestQueueLimit", requestQueueLimit.ToString());
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray;
@ -94,7 +67,7 @@ namespace IW4MAdmin.Application
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("=====================================================");
await LaunchAsync();
await LaunchAsync(args);
}
/// <summary>
@ -120,13 +93,13 @@ namespace IW4MAdmin.Application
/// task that initializes application and starts the application monitoring and runtime tasks
/// </summary>
/// <returns></returns>
private static async Task LaunchAsync()
private static async Task LaunchAsync(string[] args)
{
restart:
ITranslationLookup translationLookup = null;
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
Utilities.DefaultLogger = logger;
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version}", Version);
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version} {@Args}", Version, args);
try
{
@ -134,23 +107,23 @@ namespace IW4MAdmin.Application
ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories();
ConfigurationMigration.RemoveObsoletePlugins20210322();
logger.LogDebug("Configuring services...");
var configHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
await configHandler.BuildAsync();
_serviceProvider = WebfrontCore.Program.InitializeServices(ConfigureServices,
(configHandler.Configuration() ?? new ApplicationConfiguration()).WebfrontBindUrl);
var services = await ConfigureServices(args);
_serviceProvider = services.BuildServiceProvider();
var versionChecker = _serviceProvider.GetRequiredService<IMasterCommunication>();
_serverManager = (ApplicationManager) _serviceProvider.GetRequiredService<IManager>();
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
_applicationTask = RunApplicationTasksAsync(logger, services);
var tasks = new[]
{
versionChecker.CheckVersion(),
_applicationTask
};
await _serverManager.Init();
_applicationTask = RunApplicationTasksAsync(logger, _serverManager, _serviceProvider);
await _applicationTask;
logger.LogInformation("Shutdown completed successfully");
await Task.WhenAll(tasks);
}
catch (Exception e)
@ -200,55 +173,21 @@ namespace IW4MAdmin.Application
{
goto restart;
}
await _serviceProvider.DisposeAsync();
}
/// <summary>
/// runs the core application tasks
/// </summary>
/// <returns></returns>
private static Task RunApplicationTasksAsync(ILogger logger, ApplicationManager applicationManager,
IServiceProvider serviceProvider)
private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
{
var collectionService = serviceProvider.GetRequiredService<IServerDataCollector>();
var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>();
var masterCommunicator = serviceProvider.GetRequiredService<IMasterCommunication>();
var webfrontLifetime = serviceProvider.GetRequiredService<IHostApplicationLifetime>();
using var onWebfrontErrored = new ManualResetEventSlim();
var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront
? WebfrontCore.Program.GetWebHostTask(_serverManager.CancellationToken).ContinueWith(continuation =>
{
if (!continuation.IsFaulted)
{
return;
}
logger.LogCritical("Unable to start webfront task. {Message}",
continuation.Exception?.InnerException?.Message);
logger.LogDebug(continuation.Exception, "Unable to start webfront task");
onWebfrontErrored.Set();
})
? WebfrontCore.Program.Init(_serverManager, _serviceProvider, services, _serverManager.CancellationToken)
: Task.CompletedTask;
if (_serverManager.GetApplicationSettings().Configuration().EnableWebFront)
{
try
{
onWebfrontErrored.Wait(webfrontLifetime.ApplicationStarted);
}
catch
{
// ignored when webfront successfully starts
}
if (onWebfrontErrored.IsSet)
{
return Task.CompletedTask;
}
}
var collectionService = _serviceProvider.GetRequiredService<IServerDataCollector>();
// 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
@ -259,15 +198,18 @@ namespace IW4MAdmin.Application
var tasks = new[]
{
applicationManager.Start(),
versionChecker.CheckVersion(),
webfrontTask,
masterCommunicator.RunUploadStatus(_serverManager.CancellationToken),
_serverManager.Start(),
_serviceProvider.GetRequiredService<IMasterCommunication>()
.RunUploadStatus(_serverManager.CancellationToken),
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
};
logger.LogDebug("Starting webfront and input tasks");
return Task.WhenAll(tasks);
await Task.WhenAll(tasks);
logger.LogInformation("Shutdown completed successfully");
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
}
/// <summary>
@ -355,21 +297,8 @@ namespace IW4MAdmin.Application
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in plugins)
{
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);
}
defaultLogger.LogDebug("Registered plugin type {Name}", pluginType.FullName);
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
}
// register the plugin commands
@ -390,13 +319,10 @@ namespace IW4MAdmin.Application
serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
}
var scriptPlugins = pluginImporter.DiscoverScriptPlugins();
foreach (var scriptPlugin in scriptPlugins)
// register any script plugins
foreach (var plugin in pluginImporter.DiscoverScriptPlugins())
{
serviceCollection.AddSingleton(scriptPlugin.Item1, sp =>
sp.GetRequiredService<IScriptPluginFactory>()
.CreateScriptPlugin(scriptPlugin.Item1, scriptPlugin.Item2));
serviceCollection.AddSingleton(plugin);
}
// register any eventable types
@ -417,27 +343,22 @@ namespace IW4MAdmin.Application
/// <summary>
/// Configures the dependency injection services
/// </summary>
private static void ConfigureServices(IServiceCollection serviceCollection)
private static async Task<IServiceCollection> ConfigureServices(string[] args)
{
// todo: this is a quick fix
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
serviceCollection.AddConfiguration<ApplicationConfiguration>("IW4MAdminSettings")
.AddConfiguration<DefaultSettings>()
.AddConfiguration<CommandConfiguration>()
.AddConfiguration<StatsConfiguration>("StatsPluginSettings");
// for legacy purposes. update at some point
// setup the static resources (config/master api/translations)
var serviceCollection = new ServiceCollection();
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
appConfigHandler.BuildAsync().GetAwaiter().GetResult();
await appConfigHandler.BuildAsync();
var defaultConfigHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
await defaultConfigHandler.BuildAsync();
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration");
commandConfigHandler.BuildAsync().GetAwaiter().GetResult();
if (appConfigHandler.Configuration()?.MasterUrl == new Uri("http://api.raidmax.org:5000"))
{
appConfigHandler.Configuration().MasterUrl = new ApplicationConfiguration().MasterUrl;
}
await commandConfigHandler.BuildAsync();
var statsCommandHandler = new BaseConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
await statsCommandHandler.BuildAsync();
var defaultConfig = defaultConfigHandler.Configuration();
var appConfig = appConfigHandler.Configuration();
var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080")
@ -454,7 +375,7 @@ namespace IW4MAdmin.Application
{
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
appConfigHandler.Set(appConfig);
appConfigHandler.Save().GetAwaiter().GetResult();
await appConfigHandler.Save();
}
// register override level names
@ -467,10 +388,17 @@ namespace IW4MAdmin.Application
}
// build the dependency list
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
serviceCollection
.AddBaseLogger(appConfig)
.AddSingleton(defaultConfig)
.AddSingleton<IServiceCollection>(serviceCollection)
.AddSingleton<IConfigurationHandler<DefaultSettings>, BaseConfigurationHandler<DefaultSettings>>()
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
.AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler)
.AddSingleton(appConfig)
.AddSingleton(statsCommandHandler.Configuration() ?? new StatsConfiguration())
.AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
.Configuration() ?? new CommandConfiguration())
@ -503,7 +431,6 @@ namespace IW4MAdmin.Application
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>, ConnectionsResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, PermissionLevelChangedResponse>, PermissionLevelChangedResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>, ClientResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
.AddSingleton<IMasterCommunication, MasterCommunication>()
@ -520,22 +447,27 @@ namespace IW4MAdmin.Application
.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>))
.AddSingleton<IServerDataViewer, ServerDataViewer>()
.AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IEventPublisher, EventPublisher>()
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
#pragma warning disable CS0618
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
#pragma warning restore CS0618
.AddSingleton<IInteractionRegistration, InteractionRegistration>()
.AddSingleton<IRemoteCommandService, RemoteCommandService>()
.AddSingleton(new ConfigurationWatcher())
.AddSingleton(typeof(IConfigurationHandlerV2<>), typeof(BaseConfigurationHandlerV2<>))
.AddSingleton<IScriptPluginFactory, ScriptPluginFactory>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);
serviceCollection.AddSingleton<ICoreEventHandler, CoreEventHandler>();
if (args.Contains("serialevents"))
{
serviceCollection.AddSingleton<IEventHandler, SerialGameEventHandler>();
}
else
{
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
}
serviceCollection.AddSource();
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
return serviceCollection;
}
private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig)

View File

@ -27,8 +27,7 @@ public class
await using var context = _contextFactory.CreateContext();
var auditEntries = context.EFChangeHistory.Where(change => change.TargetEntityId == query.ClientId)
.Where(change => change.TypeOfChange == EFChangeHistory.ChangeType.Permission)
.OrderByDescending(change => change.TimeChanged);
.Where(change => change.TypeOfChange == EFChangeHistory.ChangeType.Permission);
var audits = from change in auditEntries
join client in context.Clients

View File

@ -49,10 +49,8 @@ namespace IW4MAdmin.Application.Misc
{
try
{
await _onSaving.WaitAsync();
await using var fileStream = File.OpenRead(FileName);
_configuration = await JsonSerializer.DeserializeAsync<T>(fileStream, _serializerOptions);
await fileStream.DisposeAsync();
}
catch (FileNotFoundException)
@ -68,13 +66,6 @@ namespace IW4MAdmin.Application.Misc
ConfigurationFileName = FileName
};
}
finally
{
if (_onSaving.CurrentCount == 0)
{
_onSaving.Release(1);
}
}
}
public async Task Save()
@ -85,7 +76,6 @@ namespace IW4MAdmin.Application.Misc
await using var fileStream = File.Create(FileName);
await JsonSerializer.SerializeAsync(fileStream, _configuration, _serializerOptions);
await fileStream.DisposeAsync();
}
finally

View File

@ -0,0 +1,27 @@
using Newtonsoft.Json;
using SharedLibraryCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application.Misc
{
public class EventLog : Dictionary<long, IList<GameEvent>>
{
private static JsonSerializerSettings serializationSettings;
public static JsonSerializerSettings BuildVcrSerializationSettings()
{
if (serializationSettings == null)
{
serializationSettings = new JsonSerializerSettings() { Formatting = Formatting.Indented, ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
serializationSettings.Converters.Add(new IPAddressConverter());
serializationSettings.Converters.Add(new IPEndPointConverter());
serializationSettings.Converters.Add(new GameEventConverter());
serializationSettings.Converters.Add(new ClientEntityConverter());
}
return serializationSettings;
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
public class EventPublisher : IEventPublisher
{
public event EventHandler<GameEvent> OnClientDisconnect;
public event EventHandler<GameEvent> OnClientConnect;
public event EventHandler<GameEvent> OnClientMetaUpdated;
private readonly ILogger _logger;
public EventPublisher(ILogger<EventPublisher> logger)
{
_logger = logger;
}
public void Publish(GameEvent gameEvent)
{
_logger.LogDebug("Handling publishing event of type {EventType}", gameEvent.Type);
try
{
if (gameEvent.Type == GameEvent.EventType.Connect)
{
OnClientConnect?.Invoke(this, gameEvent);
}
if (gameEvent.Type == GameEvent.EventType.Disconnect && gameEvent.Origin.ClientId != 0)
{
OnClientDisconnect?.Invoke(this, gameEvent);
}
if (gameEvent.Type == GameEvent.EventType.MetaUpdated)
{
OnClientMetaUpdated?.Invoke(this, gameEvent);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not publish event of type {EventType}", gameEvent.Type);
}
}
}
}

View File

@ -22,7 +22,7 @@ public class GeoLocationService : IGeoLocationService
try
{
using var reader = new DatabaseReader(_sourceAddress);
country = reader.Country(address);
reader.TryCountry(address, out country);
}
catch
{

View File

@ -5,7 +5,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using IW4MAdmin.Application.Plugin.Script;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
@ -29,13 +28,6 @@ public class InteractionRegistration : IInteractionRegistration
public void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration)
{
if (string.IsNullOrWhiteSpace(source))
{
throw new ArgumentException("Script interaction source cannot be null");
}
_logger.LogDebug("Registering script interaction {InteractionName} from {Source}", interactionName, source);
var plugin = _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>()
.FirstOrDefault(plugin => plugin.Name == source);
@ -44,17 +36,17 @@ public class InteractionRegistration : IInteractionRegistration
return;
}
Task<IInteractionData> WrappedDelegate(int? clientId, Reference.Game? game, CancellationToken token) =>
var wrappedDelegate = (int? clientId, Reference.Game? game, CancellationToken token) =>
Task.FromResult(
scriptPlugin.WrapDelegate<IInteractionData>(interactionRegistration, token, clientId, game, token));
scriptPlugin.WrapDelegate<IInteractionData>(interactionRegistration, clientId, game, token));
if (!_interactions.ContainsKey(interactionName))
{
_interactions.TryAdd(interactionName, WrappedDelegate);
_interactions.TryAdd(interactionName, wrappedDelegate);
}
else
{
_interactions[interactionName] = WrappedDelegate;
_interactions[interactionName] = wrappedDelegate;
}
}
@ -62,32 +54,27 @@ public class InteractionRegistration : IInteractionRegistration
{
if (!_interactions.ContainsKey(interactionName))
{
_logger.LogDebug("Registering interaction {InteractionName}", interactionName);
_interactions.TryAdd(interactionName, interactionRegistration);
}
else
{
_logger.LogDebug("Updating interaction {InteractionName}", interactionName);
_interactions[interactionName] = interactionRegistration;
}
}
public void UnregisterInteraction(string interactionName)
{
if (!_interactions.ContainsKey(interactionName))
if (_interactions.ContainsKey(interactionName))
{
return;
}
_logger.LogDebug("Unregistering interaction {InteractionName}", interactionName);
_interactions.TryRemove(interactionName, out _);
}
}
public async Task<IEnumerable<IInteractionData>> GetInteractions(string interactionPrefix = null,
int? clientId = null,
Reference.Game? game = null, CancellationToken token = default)
{
return await GetInteractionsInternal(interactionPrefix, clientId, game, token);
return await GetInteractionsPrivate(interactionPrefix, clientId, game, token);
}
public async Task<string> ProcessInteraction(string interactionId, int originId, int? targetId = null,
@ -116,21 +103,9 @@ public class InteractionRegistration : IInteractionRegistration
continue;
}
return scriptPlugin.ExecuteAction<string>(interaction.ScriptAction, token, originId, targetId, game, meta,
return scriptPlugin.ExecuteAction<string>(interaction.ScriptAction, originId, targetId, game, meta,
token);
}
foreach (var plugin in _serviceProvider.GetRequiredService<IEnumerable<IPluginV2>>())
{
if (plugin is not ScriptPluginV2 scriptPlugin || scriptPlugin.Name != interaction.Source)
{
continue;
}
return scriptPlugin
.QueryWithErrorHandling(interaction.ScriptAction, originId, targetId, game, meta, token)
?.ToString();
}
}
}
catch (Exception ex)
@ -143,10 +118,10 @@ public class InteractionRegistration : IInteractionRegistration
return null;
}
private async Task<IEnumerable<IInteractionData>> GetInteractionsInternal(string prefix = null,
int? clientId = null, Reference.Game? game = null, CancellationToken token = default)
private async Task<IEnumerable<IInteractionData>> GetInteractionsPrivate(string prefix = null, int? clientId = null,
Reference.Game? game = null, CancellationToken token = default)
{
var interactions = _interactions
return (await Task.WhenAll(_interactions
.Where(interaction => string.IsNullOrWhiteSpace(prefix) || interaction.Key.StartsWith(prefix)).Select(
async kvp =>
{
@ -162,10 +137,6 @@ public class InteractionRegistration : IInteractionRegistration
clientId);
return null;
}
});
return (await Task.WhenAll(interactions))
.Where(interaction => interaction is not null)
.ToList();
}))).Where(interaction => interaction is not null);
}
}

View File

@ -103,15 +103,17 @@ namespace IW4MAdmin.Application.Misc
await UploadStatus();
}
}
catch (Exception ex)
{
_logger.LogWarning("Could not send heartbeat - {Message}", ex.Message);
_logger.LogWarning(ex, "Could not send heartbeat");
}
try
{
await Task.Delay(Interval, token);
}
catch
{
break;
@ -147,13 +149,13 @@ namespace IW4MAdmin.Application.Misc
Map = s.CurrentMap.Name,
MaxClientNum = s.MaxClients,
Id = s.EndPoint,
Port = (short)s.ListenPort,
IPAddress = s.ListenAddress
Port = (short)s.Port,
IPAddress = s.IP
}).ToList(),
WebfrontUrl = _appConfig.WebfrontUrl
};
Response<ResultMessage> response;
Response<ResultMessage> response = null;
if (_firstHeartBeat)
{
@ -168,7 +170,7 @@ namespace IW4MAdmin.Application.Misc
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
{
_logger.LogWarning("Non success response code from master is {StatusCode}, message is {Message}", response.ResponseMessage.StatusCode, response.StringContent);
_logger.LogWarning("Non success response code from master is {statusCode}, message is {message}", response.ResponseMessage.StatusCode, response.StringContent);
}
}
}

View File

@ -7,7 +7,10 @@ using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
@ -19,6 +22,7 @@ public class MetaServiceV2 : IMetaServiceV2
{
private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory, IServiceProvider serviceProvider)
@ -26,6 +30,7 @@ public class MetaServiceV2 : IMetaServiceV2
_logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory;
_serviceProvider = serviceProvider;
}
public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId,
@ -64,6 +69,26 @@ public class MetaServiceV2 : IMetaServiceV2
}
await context.SaveChangesAsync(token);
var manager = _serviceProvider.GetRequiredService<IManager>();
var matchingClient = manager.GetActiveClients().FirstOrDefault(client => client.ClientId == clientId);
var server = matchingClient?.CurrentServer ?? manager.GetServers().FirstOrDefault();
if (server is not null)
{
manager.AddEvent(new GameEvent
{
Type = GameEvent.EventType.MetaUpdated,
Origin = matchingClient ?? new EFClient
{
ClientId = clientId
},
Data = metaValue,
Extra = metaKey,
Owner = server
});
}
}
public async Task SetPersistentMetaValue<T>(string metaKey, T metaValue, int clientId,

View File

@ -0,0 +1,177 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using SharedLibraryCore.Interfaces;
using System.Linq;
using SharedLibraryCore;
using IW4MAdmin.Application.API.Master;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IPluginImporter
/// discovers plugins and script plugins
/// </summary>
public class PluginImporter : IPluginImporter
{
private IEnumerable<PluginSubscriptionContent> _pluginSubscription;
private static readonly string PLUGIN_DIR = "Plugins";
private readonly ILogger _logger;
private readonly IRemoteAssemblyHandler _remoteAssemblyHandler;
private readonly IMasterApi _masterApi;
private readonly ApplicationConfiguration _appConfig;
public PluginImporter(ILogger<PluginImporter> logger, ApplicationConfiguration appConfig, IMasterApi masterApi, IRemoteAssemblyHandler remoteAssemblyHandler)
{
_logger = logger;
_masterApi = masterApi;
_remoteAssemblyHandler = remoteAssemblyHandler;
_appConfig = appConfig;
}
/// <summary>
/// discovers all the script plugins in the plugins dir
/// </summary>
/// <returns></returns>
public IEnumerable<IPlugin> DiscoverScriptPlugins()
{
var pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
if (!Directory.Exists(pluginDir))
{
return Enumerable.Empty<IPlugin>();
}
var scriptPluginFiles =
Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts()).ToList();
_logger.LogDebug("Discovered {count} potential script plugins", scriptPluginFiles.Count);
return scriptPluginFiles.Select(fileName =>
{
_logger.LogDebug("Discovered script plugin {fileName}", fileName);
return new ScriptPlugin(_logger, fileName);
}).ToList();
}
/// <summary>
/// discovers all the C# assembly plugins and commands
/// </summary>
/// <returns></returns>
public (IEnumerable<Type>, IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
{
var pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
var pluginTypes = Enumerable.Empty<Type>();
var commandTypes = Enumerable.Empty<Type>();
var configurationTypes = Enumerable.Empty<Type>();
if (Directory.Exists(pluginDir))
{
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
_logger.LogDebug("Discovered {count} potential plugin assemblies", dllFileNames.Length);
if (dllFileNames.Length > 0)
{
// we only want to load the most recent assembly in case of duplicates
var assemblies = dllFileNames.Select(_name => Assembly.LoadFrom(_name))
.Union(GetRemoteAssemblies())
.GroupBy(_assembly => _assembly.FullName).Select(_assembly => _assembly.OrderByDescending(_assembly => _assembly.GetName().Version).First());
pluginTypes = assemblies
.SelectMany(_asm =>
{
try
{
return _asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null);
_logger.LogDebug("Discovered {count} plugin implementations", pluginTypes.Count());
commandTypes = assemblies
.SelectMany(_asm =>{
try
{
return _asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command));
_logger.LogDebug("Discovered {count} plugin commands", commandTypes.Count());
configurationTypes = assemblies
.SelectMany(asm => {
try
{
return asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(asmType =>
asmType.IsClass && asmType.GetInterface(nameof(IBaseConfiguration), false) != null);
_logger.LogDebug("Discovered {count} configuration implementations", configurationTypes.Count());
}
}
return (pluginTypes, commandTypes, configurationTypes);
}
private IEnumerable<Assembly> GetRemoteAssemblies()
{
try
{
if (_pluginSubscription == null)
_pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription.Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray());
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not load remote assemblies");
return Enumerable.Empty<Assembly>();
}
}
private IEnumerable<string> GetRemoteScripts()
{
try
{
if (_pluginSubscription == null)
_pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());
}
catch (Exception ex)
{
_logger.LogWarning(ex,"Could not load remote scripts");
return Enumerable.Empty<string>();
}
}
}
public enum PluginType
{
Binary,
Script
}
}

View File

@ -13,10 +13,10 @@ namespace IW4MAdmin.Application.Misc
{
public class RemoteAssemblyHandler : IRemoteAssemblyHandler
{
private const int KeyLength = 32;
private const int TagLength = 16;
private const int NonceLength = 12;
private const int IterationCount = 10000;
private const int keyLength = 32;
private const int tagLength = 16;
private const int nonceLength = 12;
private const int iterationCount = 10000;
private readonly ApplicationConfiguration _appconfig;
private readonly ILogger _logger;
@ -30,7 +30,7 @@ namespace IW4MAdmin.Application.Misc
public IEnumerable<Assembly> DecryptAssemblies(string[] encryptedAssemblies)
{
return DecryptContent(encryptedAssemblies)
.Select(Assembly.Load);
.Select(decryptedAssembly => Assembly.Load(decryptedAssembly));
}
public IEnumerable<string> DecryptScripts(string[] encryptedScripts)
@ -38,24 +38,24 @@ namespace IW4MAdmin.Application.Misc
return DecryptContent(encryptedScripts).Select(decryptedScript => Encoding.UTF8.GetString(decryptedScript));
}
private IEnumerable<byte[]> DecryptContent(string[] content)
private byte[][] DecryptContent(string[] content)
{
if (string.IsNullOrEmpty(_appconfig.Id) || string.IsNullOrWhiteSpace(_appconfig.SubscriptionId))
{
_logger.LogWarning($"{nameof(_appconfig.Id)} and {nameof(_appconfig.SubscriptionId)} must be provided to attempt loading remote assemblies/scripts");
return Array.Empty<byte[]>();
return new byte[0][];
}
var assemblies = content.Select(piece =>
{
var byteContent = Convert.FromBase64String(piece);
var encryptedContent = byteContent.Take(byteContent.Length - (TagLength + NonceLength)).ToArray();
var tag = byteContent.Skip(byteContent.Length - (TagLength + NonceLength)).Take(TagLength).ToArray();
var nonce = byteContent.Skip(byteContent.Length - NonceLength).Take(NonceLength).ToArray();
var decryptedContent = new byte[encryptedContent.Length];
byte[] byteContent = Convert.FromBase64String(piece);
byte[] encryptedContent = byteContent.Take(byteContent.Length - (tagLength + nonceLength)).ToArray();
byte[] tag = byteContent.Skip(byteContent.Length - (tagLength + nonceLength)).Take(tagLength).ToArray();
byte[] nonce = byteContent.Skip(byteContent.Length - nonceLength).Take(nonceLength).ToArray();
byte[] decryptedContent = new byte[encryptedContent.Length];
var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id), IterationCount, HashAlgorithmName.SHA512);
var encryption = new AesGcm(keyGen.GetBytes(KeyLength));
var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id.ToString()), iterationCount, HashAlgorithmName.SHA512);
var encryption = new AesGcm(keyGen.GetBytes(keyLength));
try
{

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@ -27,20 +26,12 @@ public class RemoteCommandService : IRemoteCommandService
public async Task<IEnumerable<CommandResponseInfo>> Execute(int originId, int? targetId, string command,
IEnumerable<string> arguments, Server server)
{
var (_, result) = await ExecuteWithResult(originId, targetId, command, arguments, server);
return result;
}
public async Task<(bool, IEnumerable<CommandResponseInfo>)> ExecuteWithResult(int originId, int? targetId, string command,
IEnumerable<string> arguments, Server server)
{
if (originId < 1)
{
_logger.LogWarning("Not executing command {Command} for {Originid} because origin id is invalid", command,
originId);
return (false, Enumerable.Empty<CommandResponseInfo>());
return Enumerable.Empty<CommandResponseInfo>();
}
var client = await _clientService.Get(originId);
@ -57,8 +48,7 @@ public class RemoteCommandService : IRemoteCommandService
: $"{_appConfig.CommandPrefix}{command}",
Origin = client,
Owner = server,
IsRemote = true,
CorrelationId = Guid.NewGuid()
IsRemote = true
};
server.Manager.AddEvent(remoteEvent);
@ -74,7 +64,7 @@ public class RemoteCommandService : IRemoteCommandService
{
response = new[]
{
new CommandResponseInfo
new CommandResponseInfo()
{
ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"]
@ -92,7 +82,7 @@ public class RemoteCommandService : IRemoteCommandService
}
}
catch (OperationCanceledException)
catch (System.OperationCanceledException)
{
response = new[]
{
@ -104,6 +94,6 @@ public class RemoteCommandService : IRemoteCommandService
};
}
return (!remoteEvent.Failed, response);
return response;
}
}

View File

@ -1,17 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Threading.Tasks;
using Data.Models.Client;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Plugin.Script
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// generic script command implementation
@ -23,8 +20,8 @@ namespace IW4MAdmin.Application.Plugin.Script
public ScriptCommand(string name, string alias, string description, bool isTargetRequired,
EFClient.Permission permission,
IEnumerable<CommandArgument> args, Func<GameEvent, Task> executeAction, CommandConfiguration config,
ITranslationLookup layout, ILogger<ScriptCommand> logger, IEnumerable<Reference.Game> supportedGames)
CommandArgument[] args, Func<GameEvent, Task> executeAction, CommandConfiguration config,
ITranslationLookup layout, ILogger<ScriptCommand> logger, Server.Game[] supportedGames)
: base(config, layout)
{
_executeAction = executeAction;
@ -34,8 +31,8 @@ namespace IW4MAdmin.Application.Plugin.Script
Description = description;
RequiresTarget = isTargetRequired;
Permission = permission;
Arguments = args.ToArray();
SupportedGames = supportedGames?.Select(game => (Server.Game)game).ToArray();
Arguments = args;
SupportedGames = supportedGames;
}
public override async Task ExecuteAsync(GameEvent e)
@ -51,7 +48,7 @@ namespace IW4MAdmin.Application.Plugin.Script
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to execute ScriptCommand action for command {Command} {@Event}", Name, e);
_logger.LogError(ex, "Failed to execute ScriptCommand action for command {command} {@event}", Name, e);
}
}
}

View File

@ -1,30 +1,25 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using IW4MAdmin.Application.Configuration;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Misc;
using Jint;
using Jint.Native;
using Jint.Runtime;
using Jint.Runtime.Interop;
using Microsoft.CSharp.RuntimeBinder;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using IW4MAdmin.Application.Extensions;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Plugin.Script
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IPlugin
@ -48,6 +43,7 @@ namespace IW4MAdmin.Application.Plugin.Script
private Engine _scriptEngine;
private readonly string _fileName;
private readonly SemaphoreSlim _onProcessing = new(1, 1);
private readonly SemaphoreSlim _onDvarActionComplete = new(1, 1);
private bool _successfullyLoaded;
private readonly List<string> _registeredCommandNames;
private readonly ILogger _logger;
@ -59,7 +55,7 @@ namespace IW4MAdmin.Application.Plugin.Script
Watcher = new FileSystemWatcher
{
Path = workingDirectory ?? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}",
NotifyFilter = NotifyFilters.LastWrite,
NotifyFilter = NotifyFilters.Size,
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
};
@ -74,7 +70,7 @@ namespace IW4MAdmin.Application.Plugin.Script
}
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory,
IScriptPluginServiceResolver serviceResolver, IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler)
IScriptPluginServiceResolver serviceResolver)
{
try
{
@ -116,7 +112,6 @@ namespace IW4MAdmin.Application.Plugin.Script
}
}
_scriptEngine?.Dispose();
_scriptEngine = new Engine(cfg =>
cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable),
typeof(ScriptPluginExtensions))
@ -134,15 +129,9 @@ namespace IW4MAdmin.Application.Plugin.Script
.AddObjectConverter(new PermissionLevelToStringConverter()));
_scriptEngine.Execute(script);
if (!_scriptEngine.GetValue("init").IsUndefined())
{
// this is a v2 plugin and we don't want to try to load it
Watcher.EnableRaisingEvents = false;
Watcher.Dispose();
return;
}
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
_scriptEngine.SetValue("_lock", _onProcessing);
dynamic pluginObject = _scriptEngine.Evaluate("plugin").ToObject();
Author = pluginObject.author;
@ -178,19 +167,11 @@ namespace IW4MAdmin.Application.Plugin.Script
}
}
async Task<bool> OnLoadTask()
{
await OnLoadAsync(manager);
return true;
}
var loadComplete = false;
try
{
if (pluginObject.isParser)
{
loadComplete = await OnLoadTask();
await OnLoadAsync(manager);
IsParser = true;
var eventParser = (IEventParser)_scriptEngine.Evaluate("eventParser").ToObject();
var rconParser = (IRConParser)_scriptEngine.Evaluate("rconParser").ToObject();
@ -201,35 +182,32 @@ namespace IW4MAdmin.Application.Plugin.Script
catch (RuntimeBinderException)
{
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine, configHandler);
if (!loadComplete)
{
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine);
await configWrapper.InitializeAsync();
_scriptEngine.SetValue("_configHandler", configWrapper);
loadComplete = await OnLoadTask();
}
await OnLoadAsync(manager);
}
if (!firstRun && !loadComplete)
if (!firstRun)
{
loadComplete = await OnLoadTask();
await OnLoadAsync(manager);
}
_successfullyLoaded = loadComplete;
_successfullyLoaded = true;
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo} StackTrace={StackTrace}",
nameof(Initialize), Path.GetFileName(_fileName), ex.Location, ex.JavaScriptStackTrace);
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
nameof(Initialize), Path.GetFileName(_fileName), ex.Location);
throw new PluginException("An error occured while initializing script plugin");
}
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo} StackTrace={StackTrace}",
nameof(Initialize), _fileName, jsEx.Location, jsEx.JavaScriptStackTrace);
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo}",
nameof(Initialize), _fileName, jsEx.Location);
throw new PluginException("An error occured while initializing script plugin");
}
@ -257,23 +235,42 @@ namespace IW4MAdmin.Application.Plugin.Script
return;
}
var shouldRelease = false;
try
{
await _onProcessing.WaitAsync(Utilities.DefaultCommandTimeout / 2);
shouldRelease = true;
WrapJavaScriptErrorHandling(() =>
{
await _onProcessing.WaitAsync();
_scriptEngine.SetValue("_gameEvent", gameEvent);
_scriptEngine.SetValue("_server", server);
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(server));
return _scriptEngine.Evaluate("plugin.onEventAsync(_gameEvent, _server)");
}, new { EventType = gameEvent.Type }, server);
_scriptEngine.Evaluate("plugin.onEventAsync(_gameEvent, _server)");
}
catch (JavaScriptException ex)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} with event type {EventType} {@LocationInfo}",
nameof(OnEventAsync), Path.GetFileName(_fileName), gameEvent.Type, ex.Location);
}
throw new PluginException("An error occured while executing action for script plugin");
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex,
"Encountered error while running {MethodName} for script plugin {Plugin} with event type {EventType}",
nameof(OnEventAsync), _fileName, gameEvent.Type);
}
throw new PluginException("An error occured while executing action for script plugin");
}
finally
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
@ -281,37 +278,80 @@ namespace IW4MAdmin.Application.Plugin.Script
}
public Task OnLoadAsync(IManager manager)
{
try
{
_logger.LogDebug("OnLoad executing for {Name}", Name);
WrapJavaScriptErrorHandling(() =>
{
_scriptEngine.SetValue("_manager", manager);
return _scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
});
_scriptEngine.SetValue("getDvar", BeginGetDvar);
_scriptEngine.SetValue("setDvar", BeginSetDvar);
_scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
return Task.CompletedTask;
}
public Task OnTickAsync(Server server)
catch (JavaScriptException ex)
{
return Task.CompletedTask;
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
nameof(OnLoadAsync), Path.GetFileName(_fileName), ex.Location);
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
nameof(OnLoadAsync), Path.GetFileName(_fileName));
throw new PluginException("An error occured while executing action for script plugin");
}
}
public async Task OnUnloadAsync()
public async Task OnTickAsync(Server server)
{
_scriptEngine.SetValue("_server", server);
await Task.FromResult(_scriptEngine.Evaluate("plugin.onTickAsync(_server)"));
}
public Task OnUnloadAsync()
{
if (!_successfullyLoaded)
{
return;
return Task.CompletedTask;
}
try
{
await _onProcessing.WaitAsync();
_scriptEngine.Evaluate("plugin.onUnloadAsync()");
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
nameof(OnUnloadAsync), Path.GetFileName(_fileName), ex.Location);
_logger.LogDebug("OnUnload executing for {Name}", Name);
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
nameof(OnUnloadAsync), Path.GetFileName(_fileName));
WrapJavaScriptErrorHandling(() => _scriptEngine.Evaluate("plugin.onUnloadAsync()"));
throw new PluginException("An error occured while executing action for script plugin");
}
return Task.CompletedTask;
}
public T ExecuteAction<T>(Delegate action, params object[] param)
{
try
{
_onProcessing.Wait();
var args = param.Select(p => JsValue.FromObject(_scriptEngine, p)).ToArray();
var result = action.DynamicInvoke(JsValue.Undefined, args);
return (T)(result as JsValue)?.ToObject();
}
finally
{
@ -322,68 +362,17 @@ namespace IW4MAdmin.Application.Plugin.Script
}
}
public T ExecuteAction<T>(Delegate action, CancellationToken token, params object[] param)
public T WrapDelegate<T>(Delegate act, params object[] args)
{
var shouldRelease = false;
try
{
using var forceTimeout = new CancellationTokenSource(5000);
using var combined = CancellationTokenSource.CreateLinkedTokenSource(forceTimeout.Token, token);
_onProcessing.Wait(combined.Token);
shouldRelease = true;
_logger.LogDebug("Executing action for {Name}", Name);
return WrapJavaScriptErrorHandling(T() =>
{
var args = param.Select(p => JsValue.FromObject(_scriptEngine, p)).ToArray();
var result = action.DynamicInvoke(JsValue.Undefined, args);
return (T)(result as JsValue)?.ToObject();
},
new
{
Params = string.Join(", ",
param?.Select(eachParam => $"Type={eachParam?.GetType().Name} Value={eachParam}") ??
Enumerable.Empty<string>())
});
_onProcessing.Wait();
return (T)(act.DynamicInvoke(JsValue.Null,
args.Select(arg => JsValue.FromObject(_scriptEngine, arg)).ToArray()) as ObjectWrapper)?.ToObject();
}
finally
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release(1);
}
}
}
public T WrapDelegate<T>(Delegate act, CancellationToken token, params object[] args)
{
var shouldRelease = false;
try
{
using var forceTimeout = new CancellationTokenSource(5000);
using var combined = CancellationTokenSource.CreateLinkedTokenSource(forceTimeout.Token, token);
_onProcessing.Wait(combined.Token);
shouldRelease = true;
_logger.LogDebug("Wrapping delegate action for {Name}", Name);
return WrapJavaScriptErrorHandling(
T() => (T)(act.DynamicInvoke(JsValue.Null,
args.Select(arg => JsValue.FromObject(_scriptEngine, arg)).ToArray()) as ObjectWrapper)
?.ToObject(),
new
{
Params = string.Join(", ",
args?.Select(eachParam => $"Type={eachParam?.GetType().Name} Value={eachParam}") ??
Enumerable.Empty<string>())
});
}
finally
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
@ -415,10 +404,10 @@ namespace IW4MAdmin.Application.Plugin.Script
}
string permission = dynamicCommand.permission;
List<Reference.Game> supportedGames = null;
List<Server.Game> supportedGames = null;
var targetRequired = false;
var args = new List<CommandArgument>();
var args = new List<(string, bool)>();
dynamic arguments = null;
try
@ -445,7 +434,7 @@ namespace IW4MAdmin.Application.Plugin.Script
{
foreach (var arg in dynamicCommand.arguments)
{
args.Add(new CommandArgument { Name = arg.name, Required = (bool)arg.required });
args.Add((arg.name, (bool)arg.required));
}
}
@ -453,8 +442,8 @@ namespace IW4MAdmin.Application.Plugin.Script
{
foreach (var game in dynamicCommand.supportedGames)
{
supportedGames ??= new List<Reference.Game>();
supportedGames.Add(Enum.Parse(typeof(Reference.Game), game.ToString()));
supportedGames ??= new List<Server.Game>();
supportedGames.Add(Enum.Parse(typeof(Server.Game), game.ToString()));
}
}
catch (RuntimeBinderException)
@ -507,46 +496,145 @@ namespace IW4MAdmin.Application.Plugin.Script
}
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission,
targetRequired, args, Execute, supportedGames));
targetRequired, args, Execute, supportedGames?.ToArray()));
}
return commandList;
}
private T WrapJavaScriptErrorHandling<T>(Func<T> work, object additionalData = null, Server server = null,
[CallerMemberName] string methodName = "")
private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
{
using (LogContext.PushProperty("Server", server?.ToString()))
var operationTimeout = TimeSpan.FromSeconds(5);
void OnComplete(IAsyncResult result)
{
try
{
return work();
_onProcessing.Wait();
var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, value),
JsValue.FromObject(_scriptEngine, success)
});
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
methodName, Path.GetFileName(_fileName), ex.Location, ex.StackTrace, additionalData);
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
methodName, _fileName, jsEx.Location, jsEx.JavaScriptStackTrace, additionalData);
throw new PluginException("A runtime error occured while executing action for script plugin");
_logger.LogError(ex, "Could not invoke BeginGetDvar callback for {Filename} {@Location}",
Path.GetFileName(_fileName), ex.Location);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
methodName, Path.GetFileName(_fileName));
_logger.LogError(ex, "Could not complete {BeginGetDvar} for {Class}", nameof(BeginGetDvar), Name);
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
throw new PluginException("An error occured while executing action for script plugin");
new Thread(() =>
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(operationTimeout);
server.GetDvarAsync<string>(dvarName, token: tokenSource.Token).ContinueWith(action =>
{
if (action.IsCompletedSuccessfully)
{
OnComplete(new AsyncResult
{
IsCompleted = true,
AsyncState = (true, action.Result.Value)
});
}
else
{
OnComplete(new AsyncResult
{
IsCompleted = false,
AsyncState = (false, (string)null)
});
}
});
}).Start();
}
private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{
var operationTimeout = TimeSpan.FromSeconds(5);
void OnComplete(IAsyncResult result)
{
try
{
_onProcessing.Wait();
var success = (bool)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, dvarValue),
JsValue.FromObject(_scriptEngine, success)
});
}
catch (JavaScriptException ex)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex, "Could complete BeginSetDvar for {Filename} {@Location}",
Path.GetFileName(_fileName), ex.Location);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not complete {BeginSetDvar} for {Class}", nameof(BeginSetDvar), Name);
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
new Thread(() =>
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(operationTimeout);
server.SetDvarAsync(dvarName, dvarValue, token: tokenSource.Token).ContinueWith(action =>
{
if (action.IsCompletedSuccessfully)
{
OnComplete(new AsyncResult
{
IsCompleted = true,
AsyncState = true
});
}
else
{
OnComplete(new AsyncResult
{
IsCompleted = false,
AsyncState = false
});
}
});
}).Start();
}
}

View File

@ -0,0 +1,96 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using IW4MAdmin.Application.Configuration;
using Jint;
using Jint.Native;
using Newtonsoft.Json.Linq;
namespace IW4MAdmin.Application.Misc
{
public class ScriptPluginConfigurationWrapper
{
private readonly BaseConfigurationHandler<ScriptPluginConfiguration> _handler;
private ScriptPluginConfiguration _config;
private readonly string _pluginName;
private readonly Engine _scriptEngine;
public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine)
{
_handler = new BaseConfigurationHandler<ScriptPluginConfiguration>("ScriptPluginSettings");
_pluginName = pluginName;
_scriptEngine = scriptEngine;
}
public async Task InitializeAsync()
{
await _handler.BuildAsync();
_config = _handler.Configuration() ??
(ScriptPluginConfiguration) new ScriptPluginConfiguration().Generate();
}
private static int? AsInteger(double d)
{
return int.TryParse(d.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : (int?) null;
}
public async Task SetValue(string key, object value)
{
var castValue = value;
if (value is double d)
{
castValue = AsInteger(d) ?? value;
}
if (value is object[] array && array.All(item => item is double d && AsInteger(d) != null))
{
castValue = array.Select(item => AsInteger((double)item)).ToArray();
}
if (!_config.ContainsKey(_pluginName))
{
_config.Add(_pluginName, new Dictionary<string, object>());
}
var plugin = _config[_pluginName];
if (plugin.ContainsKey(key))
{
plugin[key] = castValue;
}
else
{
plugin.Add(key, castValue);
}
_handler.Set(_config);
await _handler.Save();
}
public JsValue GetValue(string key)
{
if (!_config.ContainsKey(_pluginName))
{
return JsValue.Undefined;
}
if (!_config[_pluginName].ContainsKey(key))
{
return JsValue.Undefined;
}
var item = _config[_pluginName][key];
if (item is JsonElement { ValueKind: JsonValueKind.Array } jElem)
{
item = jElem.Deserialize<List<dynamic>>();
}
return JsValue.FromObject(_scriptEngine, item);
}
}
}

View File

@ -1,8 +1,8 @@
using System;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Plugin.Script
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IScriptPluginServiceResolver
@ -25,7 +25,7 @@ namespace IW4MAdmin.Application.Plugin.Script
public object ResolveService(string serviceName, string[] genericParameters)
{
var serviceType = DetermineRootType(serviceName, genericParameters.Length);
var genericTypes = genericParameters.Select(genericTypeParam => DetermineRootType(genericTypeParam));
var genericTypes = genericParameters.Select(_genericTypeParam => DetermineRootType(_genericTypeParam));
var resolvedServiceType = serviceType.MakeGenericType(genericTypes.ToArray());
return _serviceProvider.GetService(resolvedServiceType);
}
@ -34,8 +34,8 @@ namespace IW4MAdmin.Application.Plugin.Script
{
var typeCollection = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(t => t.GetTypes());
var generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
var serviceType = typeCollection.FirstOrDefault(type => type.Name.ToLower() == generatedName);
string generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
var serviceType = typeCollection.FirstOrDefault(_type => _type.Name.ToLower() == generatedName);
if (serviceType == null)
{

View File

@ -6,20 +6,16 @@ using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Plugin.Script;
namespace IW4MAdmin.Application.Misc;
[Obsolete("This architecture is superseded by the request notify delay architecture")]
public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{
private Timer _timer;
private Action _actions;
private Delegate _jsAction;
private string _actionName;
private int _interval = DefaultInterval;
private long _waitingCount;
private const int DefaultDelay = 0;
private const int DefaultInterval = 1000;
private const int MaxWaiting = 10;
private readonly ILogger _logger;
private readonly SemaphoreSlim _onRunningTick = new(1, 1);
private SemaphoreSlim _onDependentAction;
@ -61,7 +57,6 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
_logger.LogDebug("Starting script timer...");
_timer ??= new Timer(callback => _actions(), null, delay, interval);
_interval = interval;
IsRunning = true;
}
@ -108,50 +103,39 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
_actions = OnTickInternal;
}
private void ReleaseThreads(bool releaseOnRunning, bool releaseOnDependent)
private void ReleaseThreads()
{
if (releaseOnRunning && _onRunningTick.CurrentCount == 0)
_logger.LogDebug("-Releasing OnTick for timer");
if (_onRunningTick.CurrentCount == 0)
{
_logger.LogDebug("-Releasing OnRunning for timer");
_onRunningTick.Release(1);
}
if (releaseOnDependent && _onDependentAction?.CurrentCount == 0)
if (_onDependentAction?.CurrentCount != 0)
{
_onDependentAction?.Release(1);
return;
}
_onDependentAction?.Release(1);
}
private async void OnTickInternal()
{
var releaseOnRunning = false;
var releaseOnDependent = false;
var previousTimerRunning = false;
try
{
try
{
if (Interlocked.Read(ref _waitingCount) > MaxWaiting)
{
_logger.LogWarning("Reached max number of waiting count ({WaitingCount}) for {OnTick}",
_waitingCount, nameof(OnTickInternal));
return;
}
Interlocked.Increment(ref _waitingCount);
using var tokenSource1 = new CancellationTokenSource();
tokenSource1.CancelAfter(TimeSpan.FromMilliseconds(_interval));
await _onRunningTick.WaitAsync(tokenSource1.Token);
releaseOnRunning = true;
}
catch (OperationCanceledException)
if (_onRunningTick.CurrentCount == 0)
{
_logger.LogDebug("Previous {OnTick} is still running, so we are skipping this one",
nameof(OnTickInternal));
previousTimerRunning = true;
return;
}
using var tokenSource = new CancellationTokenSource();
await _onRunningTick.WaitAsync();
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
try
@ -160,12 +144,11 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
if (_onDependentAction is not null)
{
await _onDependentAction.WaitAsync(tokenSource.Token);
releaseOnDependent = true;
}
}
catch (OperationCanceledException)
{
_logger.LogWarning("Dependent action did not release in allotted time so we are cancelling this tick");
_logger.LogDebug("Dependent action did not release in allotted time so we are cancelling this tick");
return;
}
@ -177,9 +160,8 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
catch (Exception ex) when (ex.InnerException is JavaScriptException jsx)
{
_logger.LogError(jsx,
"Could not execute timer tick for script action {ActionName} [{@LocationInfo}] [{@StackTrace}]",
_actionName,
jsx.Location, jsx.JavaScriptStackTrace);
"Could not execute timer tick for script action {ActionName} [@{LocationInfo}]", _actionName,
jsx.Location);
}
catch (Exception ex)
{
@ -187,8 +169,10 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
}
finally
{
ReleaseThreads(releaseOnRunning, releaseOnDependent);
Interlocked.Decrement(ref _waitingCount);
if (!previousTimerRunning)
{
ReleaseThreads();
}
}
}

View File

@ -12,10 +12,8 @@ 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
{
@ -26,20 +24,28 @@ 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<ServerDataCollector> logger, ApplicationConfiguration appConfig,
IManager manager, IDatabaseContextFactory contextFactory)
IManager manager, IDatabaseContextFactory contextFactory, IEventPublisher eventPublisher)
{
_logger = logger;
_appConfig = appConfig;
_manager = manager;
_contextFactory = contextFactory;
_eventPublisher = eventPublisher;
IManagementEventSubscriptions.ClientStateAuthorized += SaveConnectionInfo;
IManagementEventSubscriptions.ClientStateDisposed += SaveConnectionInfo;
_eventPublisher.OnClientConnect += SaveConnectionInfo;
_eventPublisher.OnClientDisconnect += SaveConnectionInfo;
}
~ServerDataCollector()
{
_eventPublisher.OnClientConnect -= SaveConnectionInfo;
_eventPublisher.OnClientDisconnect -= SaveConnectionInfo;
}
public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default)
@ -125,19 +131,18 @@ namespace IW4MAdmin.Application.Misc
await context.SaveChangesAsync(token);
}
private async Task SaveConnectionInfo(ClientStateEvent stateEvent, CancellationToken token)
private void SaveConnectionInfo(object sender, GameEvent gameEvent)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
using var context = _contextFactory.CreateContext(enableTracking: false);
context.ConnectionHistory.Add(new EFClientConnectionHistory
{
ClientId = stateEvent.Client.ClientId,
ServerId = await stateEvent.Client.CurrentServer.GetIdForServer(),
ConnectionType = stateEvent is ClientStateAuthorizeEvent
ClientId = gameEvent.Origin.ClientId,
ServerId = gameEvent.Owner.GetIdForServer().Result,
ConnectionType = gameEvent.Type == GameEvent.EventType.Connect
? Reference.ConnectionType.Connect
: Reference.ConnectionType.Disconnect
});
await context.SaveChangesAsync();
context.SaveChanges();
}
}
}

View File

@ -4,7 +4,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Server;
@ -41,20 +40,11 @@ namespace IW4MAdmin.Application.Misc
}
public async Task<(int?, DateTime?)>
MaxConcurrentClientsAsync(long? serverId = null, Reference.Game? gameCode = null, TimeSpan? overPeriod = null,
MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null,
CancellationToken token = default)
{
_snapshotCache.SetCacheItem(async (snapshots, ids, cancellationToken) =>
_snapshotCache.SetCacheItem(async (snapshots, cancellationToken) =>
{
Reference.Game? game = null;
long? id = null;
if (ids.Any())
{
game = (Reference.Game?)ids.First();
id = (long?)ids.Last();
}
var oldestEntry = overPeriod.HasValue
? DateTime.UtcNow - overPeriod.Value
: DateTime.UtcNow.AddDays(-1);
@ -62,10 +52,9 @@ namespace IW4MAdmin.Application.Misc
int? maxClients;
DateTime? maxClientsTime;
if (id != null)
if (serverId != null)
{
var clients = await snapshots.Where(snapshot => snapshot.ServerId == id)
.Where(snapshot => game == null || snapshot.Server.GameName == game)
var clients = await snapshots.Where(snapshot => snapshot.ServerId == serverId)
.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.OrderByDescending(snapshot => snapshot.ClientCount)
.Select(snapshot => new
@ -82,7 +71,6 @@ namespace IW4MAdmin.Application.Misc
else
{
var clients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.Where(snapshot => game == null || snapshot.Server.GameName == game)
.GroupBy(snapshot => snapshot.PeriodBlock)
.Select(grp => new
{
@ -99,12 +87,11 @@ namespace IW4MAdmin.Application.Misc
_logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients);
return (maxClients, maxClientsTime);
}, nameof(MaxConcurrentClientsAsync), new object[] { gameCode, serverId }, _cacheTimeSpan, true);
}, nameof(MaxConcurrentClientsAsync), _cacheTimeSpan, true);
try
{
return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync),
new object[] { gameCode, serverId }, token);
return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync), token);
}
catch (Exception ex)
{
@ -113,30 +100,22 @@ namespace IW4MAdmin.Application.Misc
}
}
public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, Reference.Game? gameCode = null, CancellationToken token = default)
public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default)
{
_serverStatsCache.SetCacheItem(async (set, ids, cancellationToken) =>
_serverStatsCache.SetCacheItem(async (set, cancellationToken) =>
{
Reference.Game? game = null;
if (ids.Any())
{
game = (Reference.Game?)ids.First();
}
var count = await set.CountAsync(item => game == null || item.GameName == game,
cancellationToken);
var count = await set.CountAsync(cancellationToken);
var startOfPeriod =
DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24);
var recentCount = await set.CountAsync(client => (game == null || client.GameName == game) && client.LastConnection >= startOfPeriod,
var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod,
cancellationToken);
return (count, recentCount);
}, nameof(_serverStatsCache), new object[] { gameCode }, _cacheTimeSpan, true);
}, nameof(_serverStatsCache), _cacheTimeSpan, true);
try
{
return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), new object[] { gameCode }, token);
return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), token);
}
catch (Exception ex)
{
@ -187,28 +166,21 @@ namespace IW4MAdmin.Application.Misc
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
{
_rankedClientsCache.SetCacheItem((set, ids, cancellationToken) =>
_rankedClientsCache.SetCacheItem(async (set, cancellationToken) =>
{
long? id = null;
if (ids.Any())
{
id = (long?)ids.First();
}
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
return set
return await set
.Where(rating => rating.Newest)
.Where(rating => rating.ServerId == id)
.Where(rating => rating.ServerId == serverId)
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken);
}, nameof(_rankedClientsCache), new object[] { serverId }, _cacheTimeSpan);
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
try
{
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), new object[] { serverId }, token);
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token);
}
catch (Exception ex)
{

View File

@ -1,207 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using IW4MAdmin.Application.API.Master;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Plugin
{
/// <summary>
/// implementation of IPluginImporter
/// discovers plugins and script plugins
/// </summary>
public class PluginImporter : IPluginImporter
{
private IEnumerable<PluginSubscriptionContent> _pluginSubscription;
private const string PluginDir = "Plugins";
private const string PluginV2Match = "^ *((?:var|const|let) +init)|function init";
private readonly ILogger _logger;
private readonly IRemoteAssemblyHandler _remoteAssemblyHandler;
private readonly IMasterApi _masterApi;
private readonly ApplicationConfiguration _appConfig;
private static readonly Type[] FilterTypes =
{
typeof(IPlugin),
typeof(IPluginV2),
typeof(Command),
typeof(IBaseConfiguration)
};
public PluginImporter(ILogger<PluginImporter> logger, ApplicationConfiguration appConfig, IMasterApi masterApi,
IRemoteAssemblyHandler remoteAssemblyHandler)
{
_logger = logger;
_masterApi = masterApi;
_remoteAssemblyHandler = remoteAssemblyHandler;
_appConfig = appConfig;
}
/// <summary>
/// discovers all the script plugins in the plugins dir
/// </summary>
/// <returns></returns>
public IEnumerable<(Type, string)> DiscoverScriptPlugins()
{
var pluginDir = $"{Utilities.OperatingDirectory}{PluginDir}{Path.DirectorySeparatorChar}";
if (!Directory.Exists(pluginDir))
{
return Enumerable.Empty<(Type, string)>();
}
var scriptPluginFiles =
Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts()).ToList();
var bothVersionPlugins = scriptPluginFiles.Select(fileName =>
{
_logger.LogDebug("Discovered script plugin {FileName}", fileName);
try
{
var fileContents = File.ReadAllLines(fileName);
var isValidV2 = fileContents.Any(line => Regex.IsMatch(line, PluginV2Match));
return isValidV2 ? (typeof(IPluginV2), fileName) : (typeof(IPlugin), fileName);
}
catch
{
return (typeof(IPlugin), fileName);
}
}).ToList();
return bothVersionPlugins;
}
/// <summary>
/// discovers all the C# assembly plugins and commands
/// </summary>
/// <returns></returns>
public (IEnumerable<Type>, IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
{
var pluginDir = $"{Utilities.OperatingDirectory}{PluginDir}{Path.DirectorySeparatorChar}";
var pluginTypes = new List<Type>();
var commandTypes = new List<Type>();
var configurationTypes = new List<Type>();
if (!Directory.Exists(pluginDir))
{
return (pluginTypes, commandTypes, configurationTypes);
}
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
_logger.LogDebug("Discovered {Count} potential plugin assemblies", dllFileNames.Length);
if (!dllFileNames.Any())
{
return (pluginTypes, commandTypes, configurationTypes);
}
// we only want to load the most recent assembly in case of duplicates
var assemblies = dllFileNames.Select(Assembly.LoadFrom)
.Union(GetRemoteAssemblies())
.GroupBy(assembly => assembly.FullName).Select(assembly =>
assembly.OrderByDescending(asm => asm.GetName().Version).First());
var eligibleAssemblyTypes = assemblies
.SelectMany(asm =>
{
try
{
return asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
}).Where(type =>
FilterTypes.Any(filterType => type.GetInterface(filterType.Name, false) != null) ||
(type.IsClass && FilterTypes.Contains(type.BaseType)));
foreach (var assemblyType in eligibleAssemblyTypes)
{
var isPlugin =
(assemblyType.GetInterface(nameof(IPlugin), false) ??
assemblyType.GetInterface(nameof(IPluginV2), false)) != null &&
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
if (isPlugin)
{
pluginTypes.Add(assemblyType);
continue;
}
var isCommand = assemblyType.IsClass && assemblyType.BaseType == typeof(Command) &&
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
if (isCommand)
{
commandTypes.Add(assemblyType);
continue;
}
var isConfiguration = assemblyType.IsClass &&
assemblyType.GetInterface(nameof(IBaseConfiguration), false) != null &&
(!assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
if (isConfiguration)
{
configurationTypes.Add(assemblyType);
}
}
_logger.LogDebug("Discovered {Count} plugin implementations", pluginTypes.Count);
_logger.LogDebug("Discovered {Count} plugin command implementations", commandTypes.Count);
_logger.LogDebug("Discovered {Count} plugin configuration implementations", configurationTypes.Count);
return (pluginTypes, commandTypes, configurationTypes);
}
private IEnumerable<Assembly> GetRemoteAssemblies()
{
try
{
_pluginSubscription ??= _masterApi
.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription
.Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray());
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not load remote assemblies");
return Enumerable.Empty<Assembly>();
}
}
private IEnumerable<string> GetRemoteScripts()
{
try
{
_pluginSubscription ??= _masterApi
.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription
.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());
}
catch (Exception ex)
{
_logger.LogWarning(ex,"Could not load remote scripts");
return Enumerable.Empty<string>();
}
}
}
public enum PluginType
{
Binary,
Script
}
}

View File

@ -1,130 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using IW4MAdmin.Application.Configuration;
using Jint;
using Jint.Native;
using Jint.Native.Json;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Plugin.Script;
public class ScriptPluginConfigurationWrapper
{
public event Action<JsValue, Delegate> ConfigurationUpdated;
private readonly ScriptPluginConfiguration _config;
private readonly IConfigurationHandlerV2<ScriptPluginConfiguration> _configHandler;
private readonly Engine _scriptEngine;
private readonly JsonParser _engineParser;
private readonly List<(string, Delegate)> _updateCallbackActions = new();
private string _pluginName;
public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine, IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler)
{
_pluginName = pluginName;
_scriptEngine = scriptEngine;
_configHandler = configHandler;
_configHandler.Updated += OnConfigurationUpdated;
_config = configHandler.Get("ScriptPluginSettings", new ScriptPluginConfiguration()).GetAwaiter().GetResult();
_engineParser = new JsonParser(_scriptEngine);
}
~ScriptPluginConfigurationWrapper()
{
_configHandler.Updated -= OnConfigurationUpdated;
}
public void SetName(string name)
{
_pluginName = name;
}
public async Task SetValue(string key, object value)
{
var castValue = value;
if (value is double doubleValue)
{
castValue = AsInteger(doubleValue) ?? value;
}
if (value is object[] array && array.All(item => item is double d && AsInteger(d) != null))
{
castValue = array.Select(item => AsInteger((double)item)).ToArray();
}
if (!_config.ContainsKey(_pluginName))
{
_config.Add(_pluginName, new Dictionary<string, object>());
}
var plugin = _config[_pluginName];
if (plugin.ContainsKey(key))
{
plugin[key] = castValue;
}
else
{
plugin.Add(key, castValue);
}
await _configHandler.Set(_config);
}
public JsValue GetValue(string key) => GetValue(key, null);
public JsValue GetValue(string key, Delegate updateCallback)
{
if (!_config.ContainsKey(_pluginName))
{
return JsValue.Undefined;
}
if (!_config[_pluginName].ContainsKey(key))
{
return JsValue.Undefined;
}
var item = _config[_pluginName][key];
if (item is JsonElement { ValueKind: JsonValueKind.Array } jElem)
{
item = jElem.Deserialize<List<dynamic>>();
}
if (updateCallback is not null)
{
_updateCallbackActions.Add((key, updateCallback));
}
try
{
return _engineParser.Parse(item!.ToString()!);
}
catch
{
// ignored
}
return JsValue.FromObject(_scriptEngine, item);
}
private static int? AsInteger(double value)
{
return int.TryParse(value.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : null;
}
private void OnConfigurationUpdated(ScriptPluginConfiguration config)
{
foreach (var callback in _updateCallbackActions)
{
ConfigurationUpdated?.Invoke(GetValue(callback.Item1), callback.Item2);
}
}
}

View File

@ -1,32 +0,0 @@
using System;
using IW4MAdmin.Application.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Plugin.Script;
public class ScriptPluginFactory : IScriptPluginFactory
{
private readonly IServiceProvider _serviceProvider;
public ScriptPluginFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public object CreateScriptPlugin(Type type, string fileName)
{
if (type == typeof(IPlugin))
{
return new ScriptPlugin(_serviceProvider.GetRequiredService<ILogger<ScriptPlugin>>(),
fileName);
}
return new ScriptPluginV2(fileName, _serviceProvider.GetRequiredService<ILogger<ScriptPluginV2>>(),
_serviceProvider.GetRequiredService<IScriptPluginServiceResolver>(),
_serviceProvider.GetRequiredService<IScriptCommandFactory>(),
_serviceProvider.GetRequiredService<IConfigurationHandlerV2<ScriptPluginConfiguration>>(),
_serviceProvider.GetRequiredService<IInteractionRegistration>());
}
}

View File

@ -1,143 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jint.Native;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Plugin.Script;
public class ScriptPluginHelper
{
private readonly IManager _manager;
private readonly ScriptPluginV2 _scriptPlugin;
private readonly SemaphoreSlim _onRequestRunning = new(1, 1);
private const int RequestTimeout = 5000;
public ScriptPluginHelper(IManager manager, ScriptPluginV2 scriptPlugin)
{
_manager = manager;
_scriptPlugin = scriptPlugin;
}
public void GetUrl(string url, Delegate callback)
{
RequestUrl(new ScriptPluginWebRequest(url), callback);
}
public void GetUrl(string url, string bearerToken, Delegate callback)
{
var headers = new Dictionary<string, string> { { "Authorization", $"Bearer {bearerToken}" } };
RequestUrl(new ScriptPluginWebRequest(url, Headers: headers), callback);
}
public void PostUrl(string url, string body, string bearerToken, Delegate callback)
{
var headers = new Dictionary<string, string> { { "Authorization", $"Bearer {bearerToken}" } };
RequestUrl(
new ScriptPluginWebRequest(url, body, "POST", Headers: headers), callback);
}
public void RequestUrl(ScriptPluginWebRequest request, Delegate callback)
{
Task.Run(() =>
{
try
{
var response = RequestInternal(request);
_scriptPlugin.ExecuteWithErrorHandling(scriptEngine =>
{
callback.DynamicInvoke(JsValue.Undefined, new[] { JsValue.FromObject(scriptEngine, response) });
});
}
catch
{
// ignored
}
});
}
public void RequestNotifyAfterDelay(int delayMs, Delegate callback)
{
Task.Run(async () =>
{
try
{
await Task.Delay(delayMs, _manager.CancellationToken);
_scriptPlugin.ExecuteWithErrorHandling(_ => callback.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined }));
}
catch
{
// ignored
}
});
}
public void RegisterDynamicCommand(JsValue command)
{
_scriptPlugin.RegisterDynamicCommand(command.ToObject());
}
private object RequestInternal(ScriptPluginWebRequest request)
{
var entered = false;
using var tokenSource = new CancellationTokenSource(RequestTimeout);
using var client = new HttpClient();
try
{
_onRequestRunning.Wait(tokenSource.Token);
entered = true;
var requestMessage = new HttpRequestMessage(new HttpMethod(request.Method), request.Url);
if (request.Body is not null)
{
requestMessage.Content = new StringContent(request.Body.ToString() ?? string.Empty, Encoding.Default,
request.ContentType ?? "text/plain");
}
if (request.Headers is not null)
{
foreach (var (key, value) in request.Headers)
{
if (!string.IsNullOrWhiteSpace(key))
{
requestMessage.Headers.Add(key, value);
}
}
}
var response = client.Send(requestMessage, tokenSource.Token);
using var reader = new StreamReader(response.Content.ReadAsStream());
return reader.ReadToEnd();
}
catch (HttpRequestException ex)
{
return new
{
ex.StatusCode,
ex.Message,
IsError = true
};
}
catch (Exception ex)
{
return new
{
ex.Message,
IsError = true
};
}
finally
{
if (entered)
{
_onRequestRunning.Release(1);
}
}
}
}

View File

@ -1,597 +0,0 @@
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using IW4MAdmin.Application.Configuration;
using IW4MAdmin.Application.Extensions;
using Jint;
using Jint.Native;
using Jint.Runtime;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events.Server;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces.Events;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using JavascriptEngine = Jint.Engine;
namespace IW4MAdmin.Application.Plugin.Script;
public class ScriptPluginV2 : IPluginV2
{
public string Name { get; private set; } = string.Empty;
public string Author { get; private set; } = string.Empty;
public string Version { get; private set; }
private readonly string _fileName;
private readonly ILogger<ScriptPluginV2> _logger;
private readonly IScriptPluginServiceResolver _pluginServiceResolver;
private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IConfigurationHandlerV2<ScriptPluginConfiguration> _configHandler;
private readonly IInteractionRegistration _interactionRegistration;
private readonly SemaphoreSlim _onProcessingScript = new(1, 1);
private readonly SemaphoreSlim _onLoadingFile = new(1, 1);
private readonly FileSystemWatcher _scriptWatcher;
private readonly List<string> _registeredCommandNames = new();
private readonly List<string> _registeredInteractions = new();
private readonly Dictionary<MethodInfo, List<object>> _registeredEvents = new();
private IManager _manager;
private bool _firstInitialization = true;
private record ScriptPluginDetails(string Name, string Author, string Version,
ScriptPluginCommandDetails[] Commands, ScriptPluginInteractionDetails[] Interactions);
private record ScriptPluginCommandDetails(string Name, string Description, string Alias, string Permission,
bool TargetRequired, CommandArgument[] Arguments, IEnumerable<Reference.Game> SupportedGames, Delegate Execute);
private JavascriptEngine ScriptEngine
{
get
{
lock (ActiveEngines)
{
return ActiveEngines[$"{GetHashCode()}-{_nextEngineId}"];
}
}
}
private record ScriptPluginInteractionDetails(string Name, Delegate Action);
private ScriptPluginConfigurationWrapper _scriptPluginConfigurationWrapper;
private int _nextEngineId;
private static readonly Dictionary<string, JavascriptEngine> ActiveEngines = new();
public ScriptPluginV2(string fileName, ILogger<ScriptPluginV2> logger,
IScriptPluginServiceResolver pluginServiceResolver, IScriptCommandFactory scriptCommandFactory,
IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler,
IInteractionRegistration interactionRegistration)
{
_fileName = fileName;
_logger = logger;
_pluginServiceResolver = pluginServiceResolver;
_scriptCommandFactory = scriptCommandFactory;
_configHandler = configHandler;
_interactionRegistration = interactionRegistration;
_scriptWatcher = new FileSystemWatcher
{
Path = Path.Join(Utilities.OperatingDirectory, "Plugins"),
NotifyFilter = NotifyFilters.LastWrite,
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
};
IManagementEventSubscriptions.Load += OnLoad;
}
public void ExecuteWithErrorHandling(Action<Engine> work)
{
WrapJavaScriptErrorHandling(() =>
{
work(ScriptEngine);
return true;
}, _logger, _fileName, _onProcessingScript);
}
public object QueryWithErrorHandling(Delegate action, params object[] args)
{
return WrapJavaScriptErrorHandling(() =>
{
var jsArgs = args?.Select(param => JsValue.FromObject(ScriptEngine, param)).ToArray();
var result = action.DynamicInvoke(JsValue.Undefined, jsArgs);
return result;
}, _logger, _fileName, _onProcessingScript);
}
public void RegisterDynamicCommand(object command)
{
var parsedCommand = ParseScriptCommandDetails(command);
RegisterCommand(_manager, parsedCommand.First());
}
private async Task OnLoad(IManager manager, CancellationToken token)
{
_manager = manager;
var entered = false;
try
{
await _onLoadingFile.WaitAsync(token);
entered = true;
_logger.LogDebug("{Method} executing for {Plugin}", nameof(OnLoad), _fileName);
if (new FileInfo(_fileName).Length == 0L)
{
return;
}
_scriptWatcher.EnableRaisingEvents = false;
UnregisterScriptEntities(manager);
ResetEngineState();
if (_firstInitialization)
{
_scriptWatcher.Changed += async (_, _) => await OnLoad(manager, token);
_firstInitialization = false;
}
await using var stream =
new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream, Encoding.Default);
var pluginScript = await reader.ReadToEndAsync();
var pluginDetails = WrapJavaScriptErrorHandling(() =>
{
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
{
return null;
}
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())
{
return null;
}
return AsScriptPluginInstance(initResult.ToObject());
}, _logger, _fileName, _onProcessingScript);
if (pluginDetails is null)
{
_logger.LogInformation("No valid script plugin signature found for {FilePath}", _fileName);
return;
}
foreach (var command in pluginDetails.Commands)
{
RegisterCommand(manager, command);
_logger.LogDebug("Registered script plugin command {Command} for {Plugin}", command.Name,
pluginDetails.Name);
}
foreach (var interaction in pluginDetails.Interactions)
{
RegisterInteraction(interaction);
_logger.LogDebug("Registered script plugin interaction {Interaction} for {Plugin}", interaction.Name,
pluginDetails.Name);
}
Name = pluginDetails.Name;
Author = pluginDetails.Author;
Version = pluginDetails.Version;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error encountered loading script plugin {Name}", _fileName);
}
finally
{
if (entered)
{
_onLoadingFile.Release(1);
_scriptWatcher.EnableRaisingEvents = true;
}
_logger.LogDebug("{Method} completed for {Plugin}", nameof(OnLoad), _fileName);
}
}
private void RegisterInteraction(ScriptPluginInteractionDetails interaction)
{
Task<IInteractionData> Action(int? targetId, Reference.Game? game, CancellationToken token) =>
WrapJavaScriptErrorHandling(() =>
{
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
{
return null;
}
var args = new object[] { targetId, game, token }.Select(arg => JsValue.FromObject(ScriptEngine, arg))
.ToArray();
if (interaction.Action.DynamicInvoke(JsValue.Undefined, args) is not ObjectWrapper result)
{
throw new PluginException("Invalid interaction object returned");
}
return Task.FromResult((IInteractionData)result.ToObject());
}, _logger, _fileName, _onProcessingScript);
_interactionRegistration.RegisterInteraction(interaction.Name, Action);
_registeredInteractions.Add(interaction.Name);
}
private void RegisterCommand(IManager manager, ScriptPluginCommandDetails command)
{
Task Execute(GameEvent gameEvent) =>
WrapJavaScriptErrorHandling(() =>
{
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
{
return null;
}
command.Execute.DynamicInvoke(JsValue.Undefined,
new[] { JsValue.FromObject(ScriptEngine, gameEvent) });
return Task.CompletedTask;
}, _logger, _fileName, _onProcessingScript);
var scriptCommand = _scriptCommandFactory.CreateScriptCommand(command.Name, command.Alias,
command.Description,
command.Permission, command.TargetRequired,
command.Arguments, Execute, command.SupportedGames);
manager.RemoveCommandByName(scriptCommand.Name);
manager.AddAdditionalCommand(scriptCommand);
if (!_registeredCommandNames.Contains(scriptCommand.Name))
{
_registeredCommandNames.Add(scriptCommand.Name);
}
}
private void ResetEngineState()
{
JavascriptEngine oldEngine = null;
lock (ActiveEngines)
{
if (ActiveEngines.ContainsKey($"{GetHashCode()}-{_nextEngineId}"))
{
oldEngine = ActiveEngines[$"{GetHashCode()}-{_nextEngineId}"];
_logger.LogDebug("Removing script engine from active list {HashCode}", _nextEngineId);
ActiveEngines.Remove($"{GetHashCode()}-{_nextEngineId}");
}
}
Interlocked.Increment(ref _nextEngineId);
oldEngine?.Dispose();
var newEngine = new JavascriptEngine(cfg =>
cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable),
typeof(ScriptPluginExtensions), typeof(LoggerExtensions))
.AllowClr(typeof(System.Net.Http.HttpClient).Assembly, typeof(EFClient).Assembly,
typeof(Utilities).Assembly, typeof(Encoding).Assembly, typeof(CancellationTokenSource).Assembly,
typeof(Data.Models.Client.EFClient).Assembly, typeof(IW4MAdmin.Plugins.Stats.Plugin).Assembly, typeof(ScriptPluginWebRequest).Assembly)
.CatchClrExceptions()
.AddObjectConverter(new EnumsToStringConverter()));
lock (ActiveEngines)
{
_logger.LogDebug("Adding script engine to active list {HashCode}", _nextEngineId);
ActiveEngines.Add($"{GetHashCode()}-{_nextEngineId}", newEngine);
}
_scriptPluginConfigurationWrapper =
new ScriptPluginConfigurationWrapper(_fileName.Split(Path.DirectorySeparatorChar).Last(), ScriptEngine,
_configHandler);
_scriptPluginConfigurationWrapper.ConfigurationUpdated += (configValue, callbackAction) =>
{
WrapJavaScriptErrorHandling(() =>
{
callbackAction.DynamicInvoke(JsValue.Undefined, new[] { configValue });
return Task.CompletedTask;
}, _logger, _fileName, _onProcessingScript);
};
}
private void UnregisterScriptEntities(IManager manager)
{
foreach (var commandName in _registeredCommandNames)
{
manager.RemoveCommandByName(commandName);
_logger.LogDebug("Unregistered script plugin command {Command} for {Plugin}", commandName, Name);
}
_registeredCommandNames.Clear();
foreach (var interactionName in _registeredInteractions)
{
_interactionRegistration.UnregisterInteraction(interactionName);
}
_registeredInteractions.Clear();
foreach (var (removeMethod, subscriptions) in _registeredEvents)
{
foreach (var subscription in subscriptions)
{
removeMethod.Invoke(null, new[] { subscription });
}
subscriptions.Clear();
}
_registeredEvents.Clear();
}
private void EventCallbackWrapper(string eventCallbackName, Delegate javascriptAction)
{
var eventCategory = eventCallbackName.Split(".")[0];
var eventCategoryType = eventCategory switch
{
nameof(IManagementEventSubscriptions) => typeof(IManagementEventSubscriptions),
nameof(IGameEventSubscriptions) => typeof(IGameEventSubscriptions),
nameof(IGameServerEventSubscriptions) => typeof(IGameServerEventSubscriptions),
_ => null
};
if (eventCategoryType is null)
{
_logger.LogWarning("{EventCategory} is not a valid subscription category", eventCategory);
return;
}
var eventName = eventCallbackName.Split(".")[1];
var eventAddMethod = eventCategoryType.GetMethods()
.FirstOrDefault(method => method.Name.StartsWith($"add_{eventName}"));
var eventRemoveMethod = eventCategoryType.GetMethods()
.FirstOrDefault(method => method.Name.StartsWith($"remove_{eventName}"));
if (eventAddMethod is null || eventRemoveMethod is null)
{
_logger.LogWarning("{EventName} is not a valid subscription event", eventName);
return;
}
var genericType = eventAddMethod.GetParameters()[0].ParameterType.GetGenericArguments()[0];
var eventWrapper =
typeof(ScriptPluginV2).GetMethod(nameof(BuildEventWrapper), BindingFlags.Static | BindingFlags.NonPublic)!
.MakeGenericMethod(genericType)
.Invoke(null,
new object[]
{ _logger, _fileName, javascriptAction, GetHashCode(), _nextEngineId, _onProcessingScript });
eventAddMethod.Invoke(null, new[] { eventWrapper });
if (!_registeredEvents.ContainsKey(eventRemoveMethod))
{
_registeredEvents.Add(eventRemoveMethod, new List<object> { eventWrapper });
}
else
{
_registeredEvents[eventRemoveMethod].Add(eventWrapper);
}
}
private static Func<TEventType, CancellationToken, Task> BuildEventWrapper<TEventType>(ILogger logger,
string fileName, Delegate javascriptAction, int hashCode, int engineId, SemaphoreSlim onProcessingScript)
{
return (coreEvent, token) =>
{
return WrapJavaScriptErrorHandling(() =>
{
if (IsEngineDisposed(hashCode, engineId))
{
return Task.CompletedTask;
}
JavascriptEngine engine;
lock (ActiveEngines)
{
engine = ActiveEngines[$"{hashCode}-{engineId}"];
}
var args = new object[] { coreEvent, token }
.Select(param => JsValue.FromObject(engine, param))
.ToArray();
javascriptAction.DynamicInvoke(JsValue.Undefined, args);
return Task.CompletedTask;
}, logger, fileName, onProcessingScript, (coreEvent as GameServerEvent)?.Server,
additionalData: coreEvent.GetType().Name);
};
}
private static bool IsEngineDisposed(int hashCode, int engineId)
{
lock (ActiveEngines)
{
return !ActiveEngines.ContainsKey($"{hashCode}-{engineId}");
}
}
private static TResultType WrapJavaScriptErrorHandling<TResultType>(Func<TResultType> work, ILogger logger,
string fileName, SemaphoreSlim onProcessingScript, IGameServer server = null, object additionalData = null,
bool throwException = false,
[CallerMemberName] string methodName = "")
{
using (LogContext.PushProperty("Server", server?.Id))
{
var waitCompleted = false;
try
{
onProcessingScript.Wait();
waitCompleted = true;
return work();
}
catch (JavaScriptException ex)
{
logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
methodName, Path.GetFileName(fileName), ex.Location, ex.StackTrace, additionalData);
if (throwException)
{
throw new PluginException("A runtime error occured while executing action for script plugin");
}
}
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
{
logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
methodName, fileName, jsEx.Location, jsEx.JavaScriptStackTrace, additionalData);
if (throwException)
{
throw new PluginException("A runtime error occured while executing action for script plugin");
}
}
catch (Exception ex)
{
logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
methodName, Path.GetFileName(fileName));
if (throwException)
{
throw new PluginException("An error occured while executing action for script plugin");
}
}
finally
{
if (waitCompleted)
{
onProcessingScript.Release(1);
}
}
}
return default;
}
private static ScriptPluginDetails AsScriptPluginInstance(dynamic source)
{
var commandDetails = ParseScriptCommandDetails(source);
var interactionDetails = Array.Empty<ScriptPluginInteractionDetails>();
if (HasProperty(source, "interactions") && source.interactions is dynamic[])
{
interactionDetails = ((dynamic[])source.interactions).Select(interaction =>
{
var name = HasProperty(interaction, "name") && interaction.name is string
? (string)interaction.name
: string.Empty;
var action = HasProperty(interaction, "action") && interaction.action is Delegate
? (Delegate)interaction.action
: null;
return new ScriptPluginInteractionDetails(name, action);
}).ToArray();
}
var name = HasProperty(source, "name") && source.name is string ? (string)source.name : string.Empty;
var author = HasProperty(source, "author") && source.author is string ? (string)source.author : string.Empty;
var version = HasProperty(source, "version") && source.version is string ? (string)source.author : string.Empty;
return new ScriptPluginDetails(name, author, version, commandDetails, interactionDetails);
}
private static ScriptPluginCommandDetails[] ParseScriptCommandDetails(dynamic source)
{
var commandDetails = Array.Empty<ScriptPluginCommandDetails>();
if (HasProperty(source, "commands") && source.commands is dynamic[])
{
commandDetails = ((dynamic[])source.commands).Select(command =>
{
var commandArgs = Array.Empty<CommandArgument>();
if (HasProperty(command, "arguments") && command.arguments is dynamic[])
{
commandArgs = ((dynamic[])command.arguments).Select(argument => new CommandArgument
{
Name = HasProperty(argument, "name") ? argument.name : string.Empty,
Required = HasProperty(argument, "required") && argument.required is bool &&
(bool)argument.required
}).ToArray();
}
var name = HasProperty(command, "name") && command.name is string
? (string)command.name
: string.Empty;
var description = HasProperty(command, "description") && command.description is string
? (string)command.description
: string.Empty;
var alias = HasProperty(command, "alias") && command.alias is string
? (string)command.alias
: string.Empty;
var permission = HasProperty(command, "permission") && command.permission is string
? (string)command.permission
: string.Empty;
var isTargetRequired = HasProperty(command, "targetRequired") && command.targetRequired is bool &&
(bool)command.targetRequired;
var supportedGames =
HasProperty(command, "supportedGames") && command.supportedGames is IEnumerable<object>
? ((IEnumerable<object>)command.supportedGames).Where(game => !string.IsNullOrEmpty(game?.ToString()))
.Select(game =>
Enum.Parse<Reference.Game>(game.ToString()!))
: Array.Empty<Reference.Game>();
var execute = HasProperty(command, "execute") && command.execute is Delegate
? (Delegate)command.execute
: (GameEvent _) => Task.CompletedTask;
return new ScriptPluginCommandDetails(name, description, alias, permission, isTargetRequired,
commandArgs, supportedGames, execute);
}).ToArray();
}
return commandDetails;
}
private static bool HasProperty(dynamic source, string name)
{
Type objType = source.GetType();
if (objType == typeof(ExpandoObject))
{
return ((IDictionary<string, object>)source).ContainsKey(name);
}
return objType.GetProperty(name) != null;
}
public class EnumsToStringConverter : IObjectConverter
{
public bool TryConvert(Engine engine, object value, out JsValue result)
{
if (value is Enum)
{
result = value.ToString();
return true;
}
result = JsValue.Null;
return false;
}
}
}

View File

@ -1,6 +0,0 @@
using System.Collections.Generic;
namespace IW4MAdmin.Application.Plugin.Script;
public record ScriptPluginWebRequest(string Url, object Body = null, string Method = "GET", string ContentType = "text/plain",
Dictionary<string, string> Headers = null);

View File

@ -1,297 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using WebfrontCore.Permissions;
using WebfrontCore.QueryHelpers.Models;
using EFClient = Data.Models.Client.EFClient;
namespace IW4MAdmin.Application.QueryHelpers;
public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>
{
public ApplicationConfiguration _appConfig { get; }
private readonly IDatabaseContextFactory _contextFactory;
private readonly IGeoLocationService _geoLocationService;
private class ClientAlias
{
public EFClient Client { get; set; }
public EFAlias Alias { get; set; }
}
public ClientResourceQueryHelper(IDatabaseContextFactory contextFactory, IGeoLocationService geoLocationService,
ApplicationConfiguration appConfig)
{
_appConfig = appConfig;
_contextFactory = contextFactory;
_geoLocationService = geoLocationService;
}
public async Task<ResourceQueryHelperResult<ClientResourceResponse>> QueryResource(ClientResourceRequest query)
{
await using var context = _contextFactory.CreateContext(false);
var iqAliases = context.Aliases.AsQueryable();
var iqClients = context.Clients.AsQueryable();
var iqClientAliases = iqClients.Join(iqAliases, client => client.AliasLinkId, alias => alias.LinkId,
(client, alias) => new ClientAlias { Client = client, Alias = alias });
return await StartFromClient(query, iqClientAliases, iqClients);
}
private async Task<ResourceQueryHelperResult<ClientResourceResponse>> StartFromClient(ClientResourceRequest query,
IQueryable<ClientAlias> clientAliases, IQueryable<EFClient> iqClients)
{
if (!string.IsNullOrWhiteSpace(query.ClientGuid))
{
clientAliases = SearchByGuid(query, clientAliases);
}
if (query.ClientLevel is not null)
{
clientAliases = SearchByLevel(query, clientAliases);
}
if (query.ClientConnected is not null)
{
clientAliases = SearchByLastConnection(query, clientAliases);
}
if (query.GameName is not null)
{
clientAliases = SearchByGame(query, clientAliases);
}
if (!string.IsNullOrWhiteSpace(query.ClientName))
{
clientAliases = SearchByName(query, clientAliases);
}
if (!string.IsNullOrWhiteSpace(query.ClientIp))
{
clientAliases = SearchByIp(query, clientAliases,
_appConfig.HasPermission(query.RequesterPermission, WebfrontEntity.ClientIPAddress,
WebfrontPermission.Read));
}
var iqGroupedClientAliases = clientAliases.GroupBy(a => new { a.Client.ClientId, a.Client.LastConnection });
iqGroupedClientAliases = query.Direction == SortDirection.Descending
? iqGroupedClientAliases.OrderByDescending(clientAlias => clientAlias.Key.LastConnection)
: iqGroupedClientAliases.OrderBy(clientAlias => clientAlias.Key.LastConnection);
var clientIds = iqGroupedClientAliases.Select(g => g.Key.ClientId)
.Skip(query.Offset)
.Take(query.Count);
// this pulls in more records than we need, but it's more efficient than ordering grouped entities
var clientLookups = await clientAliases
.Where(clientAlias => clientIds.Contains(clientAlias.Client.ClientId))
.Select(clientAlias => new ClientResourceResponse
{
ClientId = clientAlias.Client.ClientId,
AliasId = clientAlias.Alias.AliasId,
LinkId = clientAlias.Client.AliasLinkId,
CurrentClientName = clientAlias.Client.CurrentAlias.Name,
MatchedClientName = clientAlias.Alias.Name,
CurrentClientIp = clientAlias.Client.CurrentAlias.IPAddress,
MatchedClientIp = clientAlias.Alias.IPAddress,
ClientLevel = clientAlias.Client.Level.ToLocalizedLevelName(),
ClientLevelValue = clientAlias.Client.Level,
LastConnection = clientAlias.Client.LastConnection,
Game = clientAlias.Client.GameName
})
.ToListAsync();
var groupClients = clientLookups.GroupBy(x => x.ClientId);
var orderedClients = query.Direction == SortDirection.Descending
? groupClients.OrderByDescending(SearchByAliasLocal(query.ClientName, query.ClientIp))
: groupClients.OrderBy(SearchByAliasLocal(query.ClientName, query.ClientIp));
var clients = orderedClients.Select(client => client.First()).ToList();
await ProcessAliases(query, clients);
return new ResourceQueryHelperResult<ClientResourceResponse>
{
Results = clients
};
}
private async Task ProcessAliases(ClientResourceRequest query, IEnumerable<ClientResourceResponse> clients)
{
await Parallel.ForEachAsync(clients, new ParallelOptions { MaxDegreeOfParallelism = 15 },
async (client, token) =>
{
if (!query.IncludeGeolocationData || client.CurrentClientIp is null)
{
return;
}
var geolocationData = await _geoLocationService.Locate(client.CurrentClientIp.ConvertIPtoString());
client.ClientCountryCode = geolocationData.CountryCode;
if (!string.IsNullOrWhiteSpace(client.ClientCountryCode))
{
client.ClientCountryDisplayName = geolocationData.Country;
}
});
}
private static Func<IGrouping<int, ClientResourceResponse>, DateTime> SearchByAliasLocal(string clientName,
string ipAddress)
{
return group =>
{
ClientResourceResponse match = null;
var lowercaseClientName = clientName?.ToLower();
if (!string.IsNullOrWhiteSpace(lowercaseClientName))
{
match = group.ToList().FirstOrDefault(SearchByNameLocal(lowercaseClientName));
}
if (match is null && !string.IsNullOrWhiteSpace(ipAddress))
{
match = group.ToList().FirstOrDefault(SearchByIpLocal(ipAddress));
}
return (match ?? group.First()).LastConnection;
};
}
private static Func<ClientResourceResponse, bool> SearchByNameLocal(string clientName)
{
return clientResourceResponse =>
clientResourceResponse.MatchedClientName.Contains(clientName);
}
private static Func<ClientResourceResponse, bool> SearchByIpLocal(string clientIp)
{
return clientResourceResponse => clientResourceResponse.MatchedClientIp.ConvertIPtoString().Contains(clientIp);
}
private static IQueryable<ClientAlias> SearchByName(ClientResourceRequest query,
IQueryable<ClientAlias> clientAliases)
{
var lowerCaseQueryName = query.ClientName.ToLower();
clientAliases = clientAliases.Where(query.IsExactClientName
? ExactNameMatch(lowerCaseQueryName)
: LikeNameMatch(lowerCaseQueryName));
return clientAliases;
}
private static Expression<Func<ClientAlias, bool>> LikeNameMatch(string lowerCaseQueryName)
{
return clientAlias => EF.Functions.Like(
clientAlias.Alias.SearchableName,
$"%{lowerCaseQueryName}%") || EF.Functions.Like(
clientAlias.Alias.Name.ToLower(),
$"%{lowerCaseQueryName}%");
}
private static Expression<Func<ClientAlias, bool>> ExactNameMatch(string lowerCaseQueryName)
{
return clientAlias =>
lowerCaseQueryName == clientAlias.Alias.Name || lowerCaseQueryName == clientAlias.Alias.SearchableName;
}
private static IQueryable<ClientAlias> SearchByIp(ClientResourceRequest query,
IQueryable<ClientAlias> clientAliases, bool canSearchIP)
{
var ipString = query.ClientIp.Trim();
var ipAddress = ipString.ConvertToIP();
if (ipAddress != null && ipString.Split('.').Length == 4 && query.IsExactClientIp)
{
clientAliases = clientAliases.Where(clientAlias =>
clientAlias.Alias.IPAddress != null && clientAlias.Alias.IPAddress == ipAddress);
}
else if(canSearchIP)
{
clientAliases = clientAliases.Where(clientAlias =>
EF.Functions.Like(clientAlias.Alias.SearchableIPAddress, $"{ipString}%"));
}
return clientAliases;
}
private static IQueryable<ClientAlias> SearchByGuid(ClientResourceRequest query,
IQueryable<ClientAlias> clients)
{
var guidString = query.ClientGuid.Trim();
var parsedGuids = new List<long>();
long guid = 0;
try
{
guid = guidString.ConvertGuidToLong(NumberStyles.HexNumber, false, 0);
}
catch
{
// ignored
}
if (guid != 0)
{
parsedGuids.Add(guid);
}
try
{
guid = guidString.ConvertGuidToLong(NumberStyles.Integer, false, 0);
}
catch
{
// ignored
}
if (guid != 0)
{
parsedGuids.Add(guid);
}
if (!parsedGuids.Any())
{
return clients;
}
clients = clients.Where(client => parsedGuids.Contains(client.Client.NetworkId));
return clients;
}
private static IQueryable<ClientAlias> SearchByLevel(ClientResourceRequest query, IQueryable<ClientAlias> clients)
{
clients = clients.Where(clientAlias => clientAlias.Client.Level == query.ClientLevel);
return clients;
}
private static IQueryable<ClientAlias> SearchByLastConnection(ClientResourceRequest query,
IQueryable<ClientAlias> clients)
{
clients = clients.Where(clientAlias => clientAlias.Client.LastConnection >= query.ClientConnected);
return clients;
}
private static IQueryable<ClientAlias> SearchByGame(ClientResourceRequest query, IQueryable<ClientAlias> clients)
{
clients = clients.Where(clientAlias => clientAlias.Client.GameName == query.GameName);
return clients;
}
}

View File

@ -194,14 +194,10 @@ namespace IW4MAdmin.Application.RConParsers
foreach (var line in response)
{
var regex = Regex.Match(line, parserRegex.Pattern);
if (!regex.Success || !parserRegex.GroupMapping.ContainsKey(groupType))
if (regex.Success && parserRegex.GroupMapping.ContainsKey(groupType))
{
continue;
}
value = regex.Groups[parserRegex.GroupMapping[groupType]].ToString();
break;
}
}
if (value == null)
@ -308,7 +304,7 @@ namespace IW4MAdmin.Application.RConParsers
{
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkId = networkIdString.IsBotGuid() || (ip == null && ping is 999 or 0) ?
networkId = networkIdString.IsBotGuid() || (ip == null && ping == 999) ?
name.GenerateGuidFromString() :
networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
}

View File

@ -0,0 +1,41 @@
using SharedLibraryCore;
using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
namespace IW4MAdmin.Application
{
class SerialGameEventHandler : IEventHandler
{
private delegate void GameEventAddedEventHandler(object sender, GameEventArgs args);
private event GameEventAddedEventHandler GameEventAdded;
private static readonly GameEvent.EventType[] overrideEvents = new[]
{
GameEvent.EventType.Connect,
GameEvent.EventType.Disconnect,
GameEvent.EventType.Quit,
GameEvent.EventType.Stop
};
public SerialGameEventHandler()
{
GameEventAdded += GameEventHandler_GameEventAdded;
}
private async void GameEventHandler_GameEventAdded(object sender, GameEventArgs args)
{
await (sender as IManager).ExecuteEvent(args.Event);
EventApi.OnGameEvent(args.Event);
}
public void HandleEvent(IManager manager, GameEvent gameEvent)
{
if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
{
GameEventAdded?.Invoke(manager, new GameEventArgs(null, false, gameEvent));
}
}
}
}

View File

@ -11,11 +11,10 @@ namespace Data.Abstractions
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
TimeSpan? expirationTime = null, bool autoRefresh = false);
void SetCacheItem(Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false);
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
Task<TReturnType> GetCacheItem(string keyName, IEnumerable<object> ids = null, CancellationToken token = default);
Task<TReturnType> GetCacheItem(string keyName, object id = null, CancellationToken token = default);
}
}

View File

@ -32,7 +32,6 @@ namespace Data.Context
public DbSet<EFClientMessage> ClientMessages { get; set; }
public DbSet<EFServerStatistics> ServerStatistics { get; set; }
public DbSet<EFClientStatistics> ClientStatistics { get; set; }
public DbSet<EFHitLocation> HitLocations { get; set; }
public DbSet<EFClientHitStatistic> HitStatistics { get; set; }
public DbSet<EFWeapon> Weapons { get; set; }
@ -88,8 +87,7 @@ namespace Data.Context
// make network id unique
modelBuilder.Entity<EFClient>(entity =>
{
entity.HasIndex(client => client.NetworkId);
entity.HasIndex(client => client.LastConnection);
entity.HasIndex(e => e.NetworkId);
entity.HasAlternateKey(client => new
{
client.NetworkId,
@ -131,7 +129,6 @@ namespace Data.Context
ent.HasIndex(_alias => _alias.SearchableName);
ent.HasIndex(_alias => new {_alias.Name, _alias.IPAddress});
ent.Property(alias => alias.SearchableIPAddress)
.HasMaxLength(255)
.HasComputedColumnSql(@"((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", stored: true);
ent.HasIndex(alias => alias.SearchableIPAddress);
});
@ -153,8 +150,6 @@ namespace Data.Context
modelBuilder.Entity<EFClientConnectionHistory>(ent => ent.HasIndex(history => history.CreatedDateTime));
modelBuilder.Entity<EFServerSnapshot>(ent => ent.HasIndex(snapshot => snapshot.CapturedAt));
// force full name for database conversion
modelBuilder.Entity<EFClient>().ToTable("EFClients");
modelBuilder.Entity<EFAlias>().ToTable("EFAlias");

View File

@ -18,7 +18,7 @@ namespace Data.Helpers
private readonly IDatabaseContextFactory _contextFactory;
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
private readonly string _defaultKey = null;
private readonly object _defaultKey = new();
private bool _autoRefresh;
private const int DefaultExpireMinutes = 15;
@ -29,7 +29,7 @@ namespace Data.Helpers
public string Key { get; set; }
public DateTime LastRetrieval { get; set; }
public TimeSpan ExpirationTime { get; set; }
public Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TCacheType>> Getter { get; set; }
public Func<DbSet<TEntityType>, CancellationToken, Task<TCacheType>> Getter { get; set; }
public TCacheType Value { get; set; }
public bool IsSet { get; set; }
@ -53,10 +53,10 @@ namespace Data.Helpers
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
TimeSpan? expirationTime = null, bool autoRefresh = false)
{
SetCacheItem((set, _, token) => getter(set, token), key, null, expirationTime, autoRefresh);
SetCacheItem(getter, key, null, expirationTime, autoRefresh);
}
public void SetCacheItem(Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TReturnType>> getter, string key,
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
{
ids ??= new[] { _defaultKey };
@ -66,15 +66,11 @@ namespace Data.Helpers
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
}
var cacheInstance = _cacheStates[key];
var id = GenerateKeyFromIds(ids);
lock (_cacheStates)
foreach (var id in ids)
{
if (cacheInstance.ContainsKey(id))
if (_cacheStates[key].ContainsKey(id))
{
return;
}
continue;
}
var state = new CacheState<TReturnType>
@ -84,27 +80,26 @@ namespace Data.Helpers
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
lock (_cacheStates)
{
cacheInstance.Add(id, state);
}
_cacheStates[key].Add(id, state);
_autoRefresh = autoRefresh;
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, ids, CancellationToken.None);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
}
}
public Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
GetCacheItem(keyName, null, cancellationToken);
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
await GetCacheItem(keyName, null, cancellationToken);
public async Task<TReturnType> GetCacheItem(string keyName, IEnumerable<object> ids = null,
public async Task<TReturnType> GetCacheItem(string keyName, object id = null,
CancellationToken cancellationToken = default)
{
if (!_cacheStates.ContainsKey(keyName))
@ -112,33 +107,26 @@ namespace Data.Helpers
throw new ArgumentException("No cache found for key {key}", keyName);
}
var cacheInstance = _cacheStates[keyName];
CacheState<TReturnType> state;
lock (_cacheStates)
{
state = ids is null ? cacheInstance.Values.First() : _cacheStates[keyName][GenerateKeyFromIds(ids)];
}
var state = id is null ? _cacheStates[keyName].Values.First() : _cacheStates[keyName][id];
// when auto refresh is off we want to check the expiration and value
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
if ((state.IsExpired || !state.IsSet) && !_autoRefresh || _autoRefresh && !state.IsSet)
{
await RunCacheUpdate(state, ids, cancellationToken);
await RunCacheUpdate(state, cancellationToken);
}
return state.Value;
}
private async Task RunCacheUpdate(CacheState<TReturnType> state, IEnumerable<object> ids, CancellationToken token)
private async Task RunCacheUpdate(CacheState<TReturnType> state, CancellationToken token)
{
try
{
_logger.LogDebug("Running update for {ClassName} {@State}", GetType().Name, state);
await using var context = _contextFactory.CreateContext(false);
var set = context.Set<TEntityType>();
var value = await state.Getter(set, ids, token);
var value = await state.Getter(set, token);
state.Value = value;
state.IsSet = true;
state.LastRetrieval = DateTime.Now;
@ -148,8 +136,5 @@ namespace Data.Helpers
_logger.LogError(ex, "Could not get cached value for {Key}", state.Key);
}
}
private static string GenerateKeyFromIds(IEnumerable<object> ids) =>
string.Join("_", ids.Select(id => id?.ToString() ?? "null"));
}
}

View File

@ -8,14 +8,14 @@ using System.Threading;
using System.Threading.Tasks;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Data.Helpers;
namespace Data.Helpers
{
public class LookupCache<T> : ILookupCache<T> where T : class, IUniqueId
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
private Dictionary<long, T> _cachedItems;
private readonly SemaphoreSlim _onOperation = new(1, 1);
private readonly SemaphoreSlim _onOperation = new SemaphoreSlim(1, 1);
public LookupCache(ILogger<LookupCache<T>> logger, IDatabaseContextFactory contextFactory)
{
@ -35,7 +35,7 @@ public class LookupCache<T> : ILookupCache<T> where T : class, IUniqueId
if (existingItem != null)
{
_logger.LogDebug("Cached item already added for {Type} {Id} {Value}", typeof(T).Name, item.Id,
_logger.LogDebug("Cached item already added for {type} {id} {value}", typeof(T).Name, item.Id,
item.Value);
_onOperation.Release();
return existingItem;
@ -43,7 +43,7 @@ public class LookupCache<T> : ILookupCache<T> where T : class, IUniqueId
try
{
_logger.LogDebug("Adding new {Type} with {Id} {Value}", typeof(T).Name, item.Id, item.Value);
_logger.LogDebug("Adding new {type} with {id} {value}", typeof(T).Name, item.Id, item.Value);
await using var context = _contextFactory.CreateContext();
context.Set<T>().Add(item);
await context.SaveChangesAsync();
@ -52,7 +52,7 @@ public class LookupCache<T> : ILookupCache<T> where T : class, IUniqueId
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not add item to cache for {Type}", typeof(T).Name);
_logger.LogError(ex, "Could not add item to cache for {type}", typeof(T).Name);
throw new Exception("Could not add item to cache");
}
finally
@ -66,12 +66,22 @@ public class LookupCache<T> : ILookupCache<T> where T : class, IUniqueId
public async Task<T> FirstAsync(Func<T, bool> query)
{
await _onOperation.WaitAsync();
try
{
await _onOperation.WaitAsync();
var cachedResult = _cachedItems.Values.Where(query);
if (cachedResult.Any())
{
return cachedResult.FirstOrDefault();
}
}
catch
{
}
finally
{
if (_onOperation.CurrentCount == 0)
@ -79,6 +89,8 @@ public class LookupCache<T> : ILookupCache<T> where T : class, IUniqueId
_onOperation.Release(1);
}
}
return null;
}
public IEnumerable<T> GetAll()
@ -95,7 +107,8 @@ public class LookupCache<T> : ILookupCache<T> where T : class, IUniqueId
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not initialize caching for {CacheType}", typeof(T).Name);
_logger.LogError(ex, "Could not initialize caching for {cacheType}", typeof(T).Name);
}
}
}
}

View File

@ -808,8 +808,7 @@ namespace Data.Migrations.MySql
b.Property<string>("SearchableIPAddress")
.ValueGeneratedOnAddOrUpdate()
.HasMaxLength(255)
.HasColumnType("varchar(255)")
.HasColumnType("longtext")
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
b.Property<string>("SearchableName")

View File

@ -11,8 +11,7 @@ namespace Data.Migrations.MySql
migrationBuilder.AddColumn<string>(
name: "SearchableIPAddress",
table: "EFAlias",
type: "varchar(255)",
maxLength: 255,
type: "longtext",
nullable: true,
computedColumnSql: "CONCAT((IPAddress & 255), \".\", ((IPAddress >> 8) & 255), \".\", ((IPAddress >> 16) & 255), \".\", ((IPAddress >> 24) & 255))",
stored: true)

View File

@ -808,7 +808,6 @@ namespace Data.Migrations.MySql
b.Property<string>("SearchableIPAddress")
.ValueGeneratedOnAddOrUpdate()
.HasMaxLength(255)
.HasColumnType("varchar(255)")
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);

View File

@ -1,24 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddLastConnectionIndexEFClient : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClients_LastConnection",
table: "EFClients",
column: "LastConnection");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_LastConnection",
table: "EFClients");
}
}
}

View File

@ -1,24 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddIndexToEFServerSnapshotCapturedAt : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot",
column: "CapturedAt");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot");
}
}
}

View File

@ -96,8 +96,6 @@ namespace Data.Migrations.MySql
b.HasIndex("CurrentAliasId");
b.HasIndex("LastConnection");
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);
@ -814,7 +812,6 @@ namespace Data.Migrations.MySql
b.Property<string>("SearchableIPAddress")
.ValueGeneratedOnAddOrUpdate()
.HasMaxLength(255)
.HasColumnType("varchar(255)")
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
@ -1111,8 +1108,6 @@ namespace Data.Migrations.MySql
b.HasKey("ServerSnapshotId");
b.HasIndex("CapturedAt");
b.HasIndex("MapId");
b.HasIndex("ServerId");

View File

@ -1,24 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddLastConnectionIndexEFClient : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClients_LastConnection",
table: "EFClients",
column: "LastConnection");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_LastConnection",
table: "EFClients");
}
}
}

View File

@ -1,52 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddIndexToEFServerSnapshotCapturedAt : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "SearchableIPAddress",
table: "EFAlias",
type: "character varying(255)",
maxLength: 255,
nullable: true,
computedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
stored: true,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true,
oldComputedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
oldStored: true);
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot",
column: "CapturedAt");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot");
migrationBuilder.AlterColumn<string>(
name: "SearchableIPAddress",
table: "EFAlias",
type: "text",
nullable: true,
computedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
stored: true,
oldClrType: typeof(string),
oldType: "character varying(255)",
oldMaxLength: 255,
oldNullable: true,
oldComputedColumnSql: "((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)",
oldStored: true);
}
}
}

View File

@ -103,8 +103,6 @@ namespace Data.Migrations.Postgresql
b.HasIndex("CurrentAliasId");
b.HasIndex("LastConnection");
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);
@ -853,8 +851,7 @@ namespace Data.Migrations.Postgresql
b.Property<string>("SearchableIPAddress")
.ValueGeneratedOnAddOrUpdate()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnType("text")
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
b.Property<string>("SearchableName")
@ -1164,8 +1161,6 @@ namespace Data.Migrations.Postgresql
b.HasKey("ServerSnapshotId");
b.HasIndex("CapturedAt");
b.HasIndex("MapId");
b.HasIndex("ServerId");

View File

@ -1,24 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddLastConnectionIndexEFClient : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClients_LastConnection",
table: "EFClients",
column: "LastConnection");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_LastConnection",
table: "EFClients");
}
}
}

View File

@ -1,24 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddIndexToEFServerSnapshotCapturedAt : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot",
column: "CapturedAt");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFServerSnapshot_CapturedAt",
table: "EFServerSnapshot");
}
}
}

View File

@ -94,8 +94,6 @@ namespace Data.Migrations.Sqlite
b.HasIndex("CurrentAliasId");
b.HasIndex("LastConnection");
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);
@ -812,7 +810,6 @@ namespace Data.Migrations.Sqlite
b.Property<string>("SearchableIPAddress")
.ValueGeneratedOnAddOrUpdate()
.HasMaxLength(255)
.HasColumnType("TEXT")
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
@ -1109,8 +1106,6 @@ namespace Data.Migrations.Sqlite
b.HasKey("ServerSnapshotId");
b.HasIndex("CapturedAt");
b.HasIndex("MapId");
b.HasIndex("ServerId");

View File

@ -16,8 +16,7 @@
T7 = 8,
SHG1 = 9,
CSGO = 10,
H1 = 11,
L4D2 = 12,
H1 = 11
}
public enum ConnectionType

View File

@ -6,7 +6,6 @@ trigger:
include:
- release/pre
- master
- develop
pr: none
@ -21,8 +20,6 @@ variables:
buildConfiguration: Stable
isPreRelease: false
jobs:
- job: Build_Deploy
steps:
- task: UseDotNet@2
displayName: 'Install .NET Core 6 SDK'
@ -35,7 +32,7 @@ jobs:
- task: PowerShell@2
displayName: 'Setup Pre-Release configuration'
condition: or(eq(variables['Build.SourceBranch'], 'refs/heads/release/pre'), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
condition: eq(variables['Build.SourceBranch'], 'refs/heads/release/pre')
inputs:
targetType: 'inline'
script: |
@ -174,15 +171,7 @@ jobs:
targetPath: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip'
artifact: 'IW4MAdmin-$(Build.BuildNumber).zip'
- task: PublishPipelineArtifact@1
displayName: 'Publish artifact for analysis'
inputs:
targetPath: '$(outputFolder)'
artifact: 'IW4MAdmin.$(buildConfiguration)'
publishLocation: 'pipeline'
- task: FtpUpload@2
condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop')
displayName: 'Upload zip file to website'
inputs:
credentialsOption: 'inputs'
@ -198,7 +187,6 @@ jobs:
trustSSL: false
- task: FtpUpload@2
condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop')
displayName: 'Upload version info to website'
inputs:
credentialsOption: 'inputs'
@ -214,7 +202,6 @@ jobs:
trustSSL: false
- task: GitHubRelease@1
condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop')
displayName: 'Make GitHub release'
inputs:
gitHubConnection: 'github.com_RaidMax'
@ -227,12 +214,11 @@ jobs:
assets: '$(Build.ArtifactStagingDirectory)/*.zip'
isPreRelease: $(isPreRelease)
releaseNotesSource: 'inline'
releaseNotesInline: 'Automated rolling release - changelog below. [Updating Instructions](https://github.com/RaidMax/IW4M-Admin/wiki/Getting-Started#updating)'
releaseNotesInline: 'todo'
changeLogCompareToRelease: 'lastNonDraftRelease'
changeLogType: 'commitBased'
- task: PowerShell@2
condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop')
displayName: 'Update master version'
inputs:
targetType: 'inline'
@ -251,3 +237,10 @@ jobs:
}
Invoke-RestMethod @params
- task: PublishPipelineArtifact@1
displayName: 'Publish artifact for analysis'
inputs:
targetPath: '$(outputFolder)'
artifact: 'IW4MAdmin.$(buildConfiguration)'
publishLocation: 'pipeline'

View File

@ -41,17 +41,24 @@ waitForAttack()
{
self endon( "disconnect" );
self notifyOnPlayerCommand( "player_shot", "+attack" );
self.lastAttackTime = 0;
for( ;; )
{
self notifyOnPlayerCommand( "player_shot", "+attack" );
self waittill( "player_shot" );
self.lastAttackTime = getTime();
}
}
getHttpString( url )
{
request = httpGet( url );
request waittill( "done", success, data );
request destroy();
}
runRadarUpdates()
{
interval = getDvarInt( "sv_printradar_updateinterval", 500 );

View File

@ -40,11 +40,11 @@ waitForAttack()
{
self endon( "disconnect" );
self notifyOnPlayerCommand( "player_shot", "+attack" );
self.lastAttackTime = 0;
for( ;; )
{
self notifyOnPlayerCommand( "player_shot", "+attack" );
self waittill( "player_shot" );
self.lastAttackTime = getTime();

View File

@ -0,0 +1,264 @@
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
#include common_scripts\utility;
init()
{
SetDvarIfUninitialized( "sv_customcallbacks", true );
SetDvarIfUninitialized( "sv_framewaittime", 0.05 );
SetDvarIfUninitialized( "sv_additionalwaittime", 0.1 );
SetDvarIfUninitialized( "sv_maxstoredframes", 12 );
SetDvarIfUninitialized( "sv_printradarupdates", 0 );
SetDvarIfUninitialized( "sv_printradar_updateinterval", 500 );
SetDvarIfUninitialized( "sv_iw4madmin_url", "http://127.0.0.1:1624" );
level thread onPlayerConnect();
if (getDvarInt("sv_printradarupdates") == 1)
{
level thread runRadarUpdates();
}
level waittill( "prematch_over" );
level.callbackPlayerKilled = ::Callback_PlayerKilled;
level.callbackPlayerDamage = ::Callback_PlayerDamage;
level.callbackPlayerDisconnect = ::Callback_PlayerDisconnect;
}
//It's called slightly different in T6
//set_dvar_if_unset(dvar, val, reset)
SetDvarIfUninitialized(dvar, val)
{
set_dvar_if_unset(dvar,val);
}
onPlayerConnect( player )
{
for( ;; )
{
level waittill( "connected", player );
player thread waitForFrameThread();
player thread waitForAttack();
}
}
//Got added to T6 on April 2020
waitForAttack()
{
self endon( "disconnect" );
self.lastAttackTime = 0;
for( ;; )
{
self notifyOnPlayerCommand( "player_shot", "+attack" );
self waittill( "player_shot" );
self.lastAttackTime = getTime();
}
}
runRadarUpdates()
{
interval = getDvarInt( "sv_printradar_updateinterval" );
for ( ;; )
{
for ( i = 0; i <= 17; i++ )
{
player = level.players[i];
if ( isDefined( player ) )
{
payload = player.guid + ";" + player.origin + ";" + player getPlayerAngles() + ";" + player.team + ";" + player.kills + ";" + player.deaths + ";" + player.score + ";" + player GetCurrentWeapon() + ";" + player.health + ";" + isAlive(player) + ";" + player.timePlayed["total"];
logPrint( "LiveRadar;" + payload + "\n" );
}
}
wait( interval / 1000 );
}
}
hitLocationToBone( hitloc )
{
switch( hitloc )
{
case "helmet":
return "j_helmet";
case "head":
return "j_head";
case "neck":
return "j_neck";
case "torso_upper":
return "j_spineupper";
case "torso_lower":
return "j_spinelower";
case "right_arm_upper":
return "j_shoulder_ri";
case "left_arm_upper":
return "j_shoulder_le";
case "right_arm_lower":
return "j_elbow_ri";
case "left_arm_lower":
return "j_elbow_le";
case "right_hand":
return "j_wrist_ri";
case "left_hand":
return "j_wrist_le";
case "right_leg_upper":
return "j_hip_ri";
case "left_leg_upper":
return "j_hip_le";
case "right_leg_lower":
return "j_knee_ri";
case "left_leg_lower":
return "j_knee_le";
case "right_foot":
return "j_ankle_ri";
case "left_foot":
return "j_ankle_le";
default:
return "tag_origin";
}
}
waitForFrameThread()
{
self endon( "disconnect" );
self.currentAnglePosition = 0;
self.anglePositions = [];
for (i = 0; i < getDvarInt( "sv_maxstoredframes" ); i++)
{
self.anglePositions[i] = self getPlayerAngles();
}
for( ;; )
{
self.anglePositions[self.currentAnglePosition] = self getPlayerAngles();
wait( getDvarFloat( "sv_framewaittime" ) );
self.currentAnglePosition = (self.currentAnglePosition + 1) % getDvarInt( "sv_maxstoredframes" );
}
}
waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
{
currentIndex = self.currentAnglePosition;
wait( 0.05 * afterFrameCount );
self.angleSnapshot = [];
for( j = 0; j < self.anglePositions.size; j++ )
{
self.angleSnapshot[j] = self.anglePositions[j];
}
anglesStr = "";
collectedFrames = 0;
i = currentIndex - beforeFrameCount;
while (collectedFrames < beforeFrameCount)
{
fixedIndex = i;
if (i < 0)
{
fixedIndex = self.angleSnapshot.size - abs(i);
}
anglesStr += self.angleSnapshot[int(fixedIndex)] + ":";
collectedFrames++;
i++;
}
if (i == currentIndex)
{
anglesStr += self.angleSnapshot[i] + ":";
i++;
}
collectedFrames = 0;
while (collectedFrames < afterFrameCount)
{
fixedIndex = i;
if (i > self.angleSnapshot.size - 1)
{
fixedIndex = i % self.angleSnapshot.size;
}
anglesStr += self.angleSnapshot[int(fixedIndex)] + ":";
collectedFrames++;
i++;
}
lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
}
vectorScale( vector, scale )
{
return ( vector[0] * scale, vector[1] * scale, vector[2] * scale );
}
Process_Hit( type, attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon )
{
if (sMeansOfDeath == "MOD_FALLING" || !isPlayer(attacker))
{
return;
}
victim = self;
_attacker = attacker;
if ( !isPlayer( attacker ) && isDefined( attacker.owner ) )
{
_attacker = attacker.owner;
}
else if( !isPlayer( attacker ) && sMeansOfDeath == "MOD_FALLING" )
{
_attacker = victim;
}
location = victim GetTagOrigin( hitLocationToBone( sHitLoc ) );
isKillstreakKill = false;
if(!isPlayer(attacker))
{
isKillstreakKill = true;
}
if(maps/mp/killstreaks/_killstreaks::iskillstreakweapon(sWeapon))
{
isKillstreakKill = true;
}
logLine = "Script" + type + ";" + _attacker.guid + ";" + victim.guid + ";" + _attacker GetTagOrigin("tag_eye") + ";" + location + ";" + iDamage + ";" + sWeapon + ";" + sHitLoc + ";" + sMeansOfDeath + ";" + _attacker getPlayerAngles() + ";" + int(gettime()) + ";" + isKillstreakKill + ";" + _attacker playerADS() + ";0;0";
attacker thread waitForAdditionalAngles( logLine, 2, 2 );
}
Callback_PlayerDamage( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex )
{
if ( level.teamBased && isDefined( attacker ) && ( self != attacker ) && isDefined( attacker.team ) && ( self.pers[ "team" ] == attacker.team ) )
{
return;
}
if ( self.health - iDamage > 0 )
{
self Process_Hit( "Damage", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
}
self [[maps/mp/gametypes/_globallogic_player::callback_playerdamage]]( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex );
}
Callback_PlayerKilled(eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration)
{
Process_Hit( "Kill", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
self [[maps/mp/gametypes/_globallogic_player::callback_playerkilled]]( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration );
}
Callback_PlayerDisconnect()
{
level notify( "disconnected", self );
self [[maps/mp/gametypes/_globallogic_player::callback_playerdisconnect]]();
}

View File

@ -1,8 +1,10 @@
#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
Init()
{
thread Setup();
level thread Setup();
}
Setup()
@ -18,44 +20,16 @@ Setup()
level.eventBus.timeout = 30;
level.commonFunctions = spawnstruct();
level.commonFunctions.setDvar = "SetDvarIfUninitialized";
level.commonFunctions.getPlayerFromClientNum = "GetPlayerFromClientNum";
level.commonFunctions.waittillNotifyOrTimeout = "WaittillNotifyOrTimeout";
level.commonFunctions.getInboundData = "GetInboundData";
level.commonFunctions.getOutboundData = "GetOutboundData";
level.commonFunctions.setInboundData = "SetInboundData";
level.commonFunctions.setOutboundData = "SetOutboundData";
level.overrideMethods = [];
level.overrideMethods[level.commonFunctions.setDvar] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getPlayerFromClientNum] = ::_GetPlayerFromClientNum;
level.overrideMethods[level.commonFunctions.getInboundData] = ::_GetInboundData;
level.overrideMethods[level.commonFunctions.getOutboundData] = ::_GetOutboundData;
level.overrideMethods[level.commonFunctions.setInboundData] = ::_SetInboundData;
level.overrideMethods[level.commonFunctions.setOutboundData] = ::_SetOutboundData;
level.busMethods = [];
level.busMethods[level.commonFunctions.getInboundData] = ::_GetInboundData;
level.busMethods[level.commonFunctions.getOutboundData] = ::_GetOutboundData;
level.busMethods[level.commonFunctions.setInboundData] = ::_SetInboundData;
level.busMethods[level.commonFunctions.setOutboundData] = ::_SetOutboundData;
level.commonKeys = spawnstruct();
level.commonKeys.enabled = "sv_iw4madmin_integration_enabled";
level.commonKeys.busMode = "sv_iw4madmin_integration_busmode";
level.commonKeys.busDir = "sv_iw4madmin_integration_busdir";
level.eventBus.inLocation = "";
level.eventBus.outLocation = "";
level.commonFunctions.SetDvar = "SetDvarIfUninitialized";
level.notifyTypes = spawnstruct();
level.notifyTypes.gameFunctionsInitialized = "GameFunctionsInitialized";
level.notifyTypes.sharedFunctionsInitialized = "SharedFunctionsInitialized";
level.notifyTypes.integrationBootstrapInitialized = "IntegrationBootstrapInitialized";
level.clientDataKey = "clientData";
level.eventTypes = spawnstruct();
level.eventTypes.eventAvailable = "EventAvailable";
level.eventTypes.localClientEvent = "client_event";
level.eventTypes.clientDataReceived = "ClientDataReceived";
level.eventTypes.clientDataRequested = "ClientDataRequested";
level.eventTypes.setClientDataRequested = "SetClientDataRequested";
@ -73,11 +47,12 @@ Setup()
level.clientCommandCallbacks = [];
level.clientCommandRusAsTarget = [];
level.logger = spawnstruct();
level.overrideMethods = [];
level.iw4madminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" );
InitializeLogger();
wait ( 0.05 * 2 ); // needed to give script engine time to propagate notifies
wait ( 0.05 ); // needed to give script engine time to propagate notifies
level notify( level.notifyTypes.integrationBootstrapInitialized );
level waittill( level.notifyTypes.gameFunctionsInitialized );
@ -86,58 +61,174 @@ Setup()
_SetDvarIfUninitialized( level.eventBus.inVar, "" );
_SetDvarIfUninitialized( level.eventBus.outVar, "" );
_SetDvarIfUninitialized( level.commonKeys.enabled, 1 );
_SetDvarIfUninitialized( level.commonKeys.busMode, "rcon" );
_SetDvarIfUninitialized( level.commonKeys.busdir, "" );
_SetDvarIfUninitialized( "sv_iw4madmin_integration_enabled", 1 );
_SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 );
_SetDvarIfUninitialized( "GroupSeparatorChar", "" );
_SetDvarIfUninitialized( "RecordSeparatorChar", "" );
_SetDvarIfUninitialized( "UnitSeparatorChar", "" );
if ( GetDvarInt( level.commonKeys.enabled ) != 1 )
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
// start long running tasks
thread MonitorEvents();
thread MonitorBus();
level thread MonitorClientEvents();
level thread MonitorBus();
level thread OnPlayerConnect();
}
MonitorEvents()
//////////////////////////////////
// Client Methods
//////////////////////////////////
OnPlayerConnect()
{
level endon( level.eventTypes.gameEnd );
level endon ( "game_ended" );
for ( ;; )
{
level waittill( level.eventTypes.eventAvailable, event );
level waittill( "connected", player );
LogDebug( "Processing Event " + event.type + "-" + event.subtype );
if ( _IsBot( player ) )
{
// we don't want to track bots
continue;
}
eventHandler = level.eventCallbacks[event.type];
if ( !IsDefined( player.pers[level.clientDataKey] ) )
{
player.pers[level.clientDataKey] = spawnstruct();
}
player thread OnPlayerSpawned();
player thread OnPlayerJoinedTeam();
player thread OnPlayerJoinedSpectators();
player thread PlayerTrackingOnInterval();
}
}
OnPlayerSpawned()
{
self endon( "disconnect" );
for ( ;; )
{
self waittill( "spawned_player" );
self PlayerSpawnEvents();
}
}
OnPlayerDisconnect()
{
self endon ( "disconnect" );
for ( ;; )
{
self waittill( "disconnect" );
self SaveTrackingMetrics();
}
}
OnPlayerJoinedTeam()
{
self endon( "disconnect" );
for( ;; )
{
self waittill( "joined_team" );
// join spec and join team occur at the same moment - out of order logging would be problematic
wait( 0.25 );
LogPrint( GenerateJoinTeamString( false ) );
}
}
OnPlayerJoinedSpectators()
{
self endon( "disconnect" );
for( ;; )
{
self waittill( "joined_spectators" );
LogPrint( GenerateJoinTeamString( true ) );
}
}
OnGameEnded()
{
for ( ;; )
{
level waittill( "game_ended" );
// note: you can run data code here but it's possible for
// data to get truncated, so we will try a timer based approach for now
}
}
DisplayWelcomeData()
{
self endon( "disconnect" );
clientData = self.pers[level.clientDataKey];
if ( clientData.permissionLevel == "User" || clientData.permissionLevel == "Flagged" )
{
return;
}
self IPrintLnBold( "Welcome, your level is ^5" + clientData.permissionLevel );
wait( 2.0 );
self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection );
}
PlayerSpawnEvents()
{
self endon( "disconnect" );
clientData = self.pers[level.clientDataKey];
// this gives IW4MAdmin some time to register the player before making the request;
// although probably not necessary some users might have a slow database or poll rate
wait ( 2 );
if ( IsDefined( clientData.state ) && clientData.state == "complete" )
{
return;
}
self RequestClientBasicData();
}
PlayerTrackingOnInterval()
{
self endon( "disconnect" );
for ( ;; )
{
wait ( 120 );
if ( IsAlive( self ) )
{
self SaveTrackingMetrics();
}
}
}
MonitorClientEvents()
{
level endon( "game_ended" );
for ( ;; )
{
level waittill( level.eventTypes.localClientEvent, client );
LogDebug( "Processing Event " + client.event.type + "-" + client.event.subtype );
eventHandler = level.eventCallbacks[client.event.type];
if ( IsDefined( eventHandler ) )
{
if ( IsDefined( event.entity ) )
{
event.entity [[eventHandler]]( event );
}
else
{
[[eventHandler]]( event );
}
client [[eventHandler]]( client.event );
LogDebug( "notify client for " + client.event.type );
client notify( level.eventTypes.localClientEvent, client.event );
}
if ( IsDefined( event.entity ) )
{
LogDebug( "Notify client for " + event.type );
event.entity notify( event.type, event );
}
else
{
LogDebug( "Notify level for " + event.type );
level notify( event.type, event );
}
client.eventData = [];
}
}
@ -145,58 +236,16 @@ MonitorEvents()
// Helper Methods
//////////////////////////////////
NotImplementedFunction( a, b, c, d, e, f )
_IsBot( entity )
{
LogWarning( "Function not implemented" );
if ( IsDefined ( a ) )
{
LogWarning( a );
}
// there already is a cgame function exists as "IsBot", for IW4, but unsure what all titles have it defined,
// so we are defining it here
return IsDefined( entity.pers["isBot"] ) && entity.pers["isBot"];
}
_SetDvarIfUninitialized( dvarName, dvarValue )
{
[[level.overrideMethods[level.commonFunctions.setDvar]]]( dvarName, dvarValue );
}
_GetPlayerFromClientNum( clientNum )
{
assertEx( clientNum >= 0, "clientNum cannot be negative" );
if ( clientNum < 0 )
{
return undefined;
}
for ( i = 0; i < level.players.size; i++ )
{
if ( level.players[i] getEntityNumber() == clientNum )
{
return level.players[i];
}
}
return undefined;
}
_GetInboundData( location )
{
return GetDvar( level.eventBus.inVar );
}
_GetOutboundData( location )
{
return GetDvar( level.eventBus.outVar );
}
_SetInboundData( location, data )
{
return SetDvar( level.eventBus.inVar, data );
}
_SetOutboundData( location, data )
{
return SetDvar( level.eventBus.outVar, data );
[[level.overrideMethods[level.commonFunctions.SetDvar]]]( dvarName, dvarValue );
}
// Not every game can output to console or even game log.
@ -217,7 +266,7 @@ _Log( LogLevel, message )
{
for( i = 0; i < level.logger._logger.size; i++ )
{
[[level.logger._logger[i]]]( LogLevel, GetSubStr( message, 0, 1000 ) );
[[level.logger._logger[i]]]( LogLevel, message );
}
}
@ -279,13 +328,13 @@ RegisterLogger( logger )
RequestClientMeta( metaKey )
{
getClientMetaEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "Meta", self, metaKey );
thread QueueEvent( getClientMetaEvent, level.eventTypes.clientDataRequested, self );
level thread QueueEvent( getClientMetaEvent, level.eventTypes.clientDataRequested, self );
}
RequestClientBasicData()
{
getClientDataEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "None", self, "" );
thread QueueEvent( getClientDataEvent, level.eventTypes.clientDataRequested, self );
level thread QueueEvent( getClientDataEvent, level.eventTypes.clientDataRequested, self );
}
IncrementClientMeta( metaKey, incrementValue, clientId )
@ -298,22 +347,51 @@ DecrementClientMeta( metaKey, decrementValue, clientId )
SetClientMeta( metaKey, decrementValue, clientId, "decrement" );
}
GenerateJoinTeamString( isSpectator )
{
team = self.team;
if ( IsDefined( self.joining_team ) )
{
team = self.joining_team;
}
else
{
if ( isSpectator || !IsDefined( team ) )
{
team = "spectator";
}
}
guid = self GetXuid();
if ( guid == "0" )
{
guid = self.guid;
}
if ( !IsDefined( guid ) || guid == "0" )
{
guid = "undefined";
}
return "JT;" + guid + ";" + self getEntityNumber() + ";" + team + ";" + self.name + "\n";
}
SetClientMeta( metaKey, metaValue, clientId, direction )
{
data = [];
data["key"] = metaKey;
data["value"] = metaValue;
data = "key=" + metaKey + "|value=" + metaValue;
clientNumber = -1;
if ( IsDefined ( clientId ) )
{
data["clientId"] = clientId;
data = data + "|clientId=" + clientId;
clientNumber = -1;
}
if ( IsDefined( direction ) )
{
data["direction"] = direction;
data = data + "|direction=" + direction;
}
if ( IsPlayer( self ) )
@ -322,7 +400,40 @@ SetClientMeta( metaKey, metaValue, clientId, direction )
}
setClientMetaEvent = BuildEventRequest( true, level.eventTypes.setClientDataRequested, "Meta", clientNumber, data );
thread QueueEvent( setClientMetaEvent, level.eventTypes.setClientDataRequested, self );
level thread QueueEvent( setClientMetaEvent, level.eventTypes.setClientDataRequested, self );
}
SaveTrackingMetrics()
{
if ( !IsDefined( self.persistentClientId ) )
{
return;
}
LogDebug( "Saving tracking metrics for " + self.persistentClientId );
if ( !IsDefined( self.lastShotCount ) )
{
self.lastShotCount = 0;
}
currentShotCount = self [[level.overrideMethods["GetTotalShotsFired"]]]();
change = currentShotCount - self.lastShotCount;
self.lastShotCount = currentShotCount;
LogDebug( "Total Shots Fired increased by " + change );
if ( !IsDefined( change ) )
{
change = 0;
}
if ( change == 0 )
{
return;
}
IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId );
}
BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
@ -337,11 +448,6 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
eventSubtype = "None";
}
if ( !IsDefined( entOrId ) )
{
entOrId = "-1";
}
if ( IsPlayer( entOrId ) )
{
entOrId = entOrId getEntityNumber();
@ -354,64 +460,51 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
request = "1";
}
data = BuildDataString( data );
groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 );
request = request + groupSeparator + eventType + groupSeparator + eventSubtype + groupSeparator + entOrId + groupSeparator + data;
request = request + ";" + eventType + ";" + eventSubtype + ";" + entOrId + ";" + data;
return request;
}
MonitorBus()
{
level endon( level.eventTypes.gameEnd );
level.eventBus.inLocation = level.eventBus.inVar + "_" + GetDvar( "net_port" );
level.eventBus.outLocation = level.eventBus.outVar + "_" + GetDvar( "net_port" );
[[level.overrideMethods[level.commonFunctions.SetInboundData]]]( level.eventBus.inLocation, "" );
[[level.overrideMethods[level.commonFunctions.SetOutboundData]]]( level.eventBus.outLocation, "" );
level endon( "game_ended" );
for( ;; )
{
wait ( 0.1 );
// check to see if IW4MAdmin is ready to receive more data
inVal = [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation );
if ( !IsDefined( inVal ) || inVal == "" )
if ( getDvar( level.eventBus.inVar ) == "" )
{
level notify( "bus_ready" );
}
eventString = [[level.busMethods[level.commonFunctions.getOutboundData]]]( level.eventBus.outLocation );
eventString = getDvar( level.eventBus.outVar );
if ( !IsDefined( eventString ) || eventString == "" )
if ( eventString == "" )
{
continue;
}
LogDebug( "-> " + eventString );
groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 );
NotifyEvent( strtok( eventString, groupSeparator ) );
NotifyClientEvent( strtok( eventString, ";" ) );
[[level.busMethods[level.commonFunctions.SetOutboundData]]]( level.eventBus.outLocation, "" );
SetDvar( level.eventBus.outVar, "" );
}
}
QueueEvent( request, eventType, notifyEntity )
{
level endon( level.eventTypes.gameEnd );
level endon( "game_ended" );
start = GetTime();
maxWait = level.eventBus.timeout * 1000; // 30 seconds
timedOut = "";
while ( [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ) != "" && ( GetTime() - start ) < maxWait )
while ( GetDvar( level.eventBus.inVar ) != "" && ( GetTime() - start ) < maxWait )
{
level [[level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout]]]( "bus_ready", 1 );
level [[level.overrideMethods["waittill_notify_or_timeout"]]]( "bus_ready", 1 );
if ( [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ) != "" )
if ( GetDvar( level.eventBus.inVar ) != "" )
{
LogDebug( "A request is already in progress..." );
timedOut = "set";
@ -430,14 +523,14 @@ QueueEvent( request, eventType, notifyEntity )
notifyEntity NotifyClientEventTimeout( eventType );
}
[[level.busMethods[level.commonFunctions.SetInboundData]]]( level.eventBus.inLocation, "" );
SetDvar( level.eventBus.inVar, "" );
return;
}
LogDebug("<- " + request );
[[level.busMethods[level.commonFunctions.setInboundData]]]( level.eventBus.inLocation, request );
SetDvar( level.eventBus.inVar, request );
}
ParseDataString( data )
@ -448,13 +541,13 @@ ParseDataString( data )
return [];
}
dataParts = strtok( data, GetSubStr( GetDvar( "RecordSeparatorChar" ), 0, 1 ) );
dataParts = strtok( data, "|" );
dict = [];
for ( i = 0; i < dataParts.size; i++ )
{
part = dataParts[i];
splitPart = strtok( part, GetSubStr( GetDvar( "UnitSeparatorChar" ), 0, 1 ) );
splitPart = strtok( part, "=" );
key = splitPart[0];
value = splitPart[1];
dict[key] = value;
@ -464,26 +557,6 @@ ParseDataString( data )
return dict;
}
BuildDataString( data )
{
if ( IsString( data ) )
{
return data;
}
dataString = "";
keys = GetArrayKeys( data );
unitSeparator = GetSubStr( GetDvar( "UnitSeparatorChar" ), 0, 1 );
recordSeparator = GetSubStr( GetDvar( "RecordSeparatorChar" ), 0, 1 );
for ( i = 0; i < keys.size; i++ )
{
dataString = dataString + keys[i] + unitSeparator + data[keys[i]] + recordSeparator;
}
return dataString;
}
NotifyClientEventTimeout( eventType )
{
// todo: make this actual eventing
@ -493,18 +566,23 @@ NotifyClientEventTimeout( eventType )
}
}
NotifyEvent( eventInfo )
NotifyClientEvent( eventInfo )
{
origin = [[level.overrideMethods[level.commonFunctions.getPlayerFromClientNum]]]( int( eventInfo[3] ) );
target = [[level.overrideMethods[level.commonFunctions.getPlayerFromClientNum]]]( int( eventInfo[4] ) );
origin = getPlayerFromClientNum( int( eventInfo[3] ) );
target = getPlayerFromClientNum( int( eventInfo[4] ) );
event = spawnstruct();
event.type = eventInfo[1];
event.subtype = eventInfo[2];
event.data = ParseDataString( eventInfo[5] );
event.data = eventInfo[5];
event.origin = origin;
event.target = target;
if ( IsDefined( event.data ) )
{
LogDebug( "NotifyClientEvent->" + event.data );
}
if ( int( eventInfo[3] ) != -1 && !IsDefined( origin ) )
{
LogDebug( "origin is null but the slot id is " + int( eventInfo[3] ) );
@ -514,15 +592,41 @@ NotifyEvent( eventInfo )
LogDebug( "target is null but the slot id is " + int( eventInfo[4] ) );
}
client = event.origin;
if ( !IsDefined( client ) )
if ( IsDefined( target ) )
{
client = event.target;
}
else if ( IsDefined( origin ) )
{
client = event.origin;
}
else
{
LogDebug( "Neither origin or target are set but we are a Client Event, aborting" );
event.entity = client;
level notify( level.eventTypes.eventAvailable, event );
return;
}
client.event = event;
level notify( level.eventTypes.localClientEvent, client );
}
GetPlayerFromClientNum( clientNum )
{
if ( clientNum < 0 )
{
return undefined;
}
for ( i = 0; i < level.players.size; i++ )
{
if ( level.players[i] getEntityNumber() == clientNum )
{
return level.players[i];
}
}
return undefined;
}
AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
@ -542,7 +646,7 @@ AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
OnClientDataReceived( event )
{
assertEx( isDefined( self ), "player entity is not defined");
event.data = ParseDataString( event.data );
clientData = self.pers[level.clientDataKey];
if ( event.subtype == "Fail" )
@ -562,7 +666,7 @@ OnClientDataReceived( event )
metaKey = event.data[0];
clientData.meta[metaKey] = event.data[metaKey];
LogDebug( "Meta Key=" + CoerceUndefined( metaKey ) + ", Meta Value=" + CoerceUndefined( event.data[metaKey] ) );
LogDebug( "Meta Key=" + metaKey + ", Meta Value=" + event.data[metaKey] );
return;
}
@ -571,14 +675,15 @@ OnClientDataReceived( event )
clientData.clientId = event.data["clientId"];
clientData.lastConnection = event.data["lastConnection"];
clientData.tag = event.data["tag"];
clientData.performance = event.data["performance"];
clientData.state = "complete";
self.persistentClientId = event.data["clientId"];
self thread DisplayWelcomeData();
}
OnExecuteCommand( event )
{
data = event.data;
data = ParseDataString( event.data );
response = "";
command = level.clientCommandCallbacks[event.subtype];
@ -592,14 +697,7 @@ OnExecuteCommand( event )
if ( IsDefined( command ) )
{
if ( IsDefined( executionContextEntity ) )
{
response = executionContextEntity thread [[command]]( event, data );
}
else
{
thread [[command]]( event );
}
response = executionContextEntity [[command]]( event, data );
}
else
{
@ -615,15 +713,6 @@ OnExecuteCommand( event )
OnSetClientDataCompleted( event )
{
LogDebug( "Set Client Data -> subtype = " + CoerceUndefined( event.subType ) + ", status = " + CoerceUndefined( event.data["status"] ) );
}
CoerceUndefined( object )
{
if ( !IsDefined( object ) )
{
return "undefined";
}
return object;
// IW4MAdmin let us know it persisted (success or fail)
LogDebug( "Set Client Data -> subtype = " + event.subType + " status = " + event.data["status"] );
}

View File

@ -2,50 +2,34 @@
Init()
{
thread Setup();
level.eventBus.gamename = "IW4";
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
waittillframeend;
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.eventBus.gamename = "IW4";
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\_integration_base::RegisterLogger( ::Log2Console );
level.overrideMethods[level.commonFunctions.getTotalShotsFired] = ::GetTotalShotsFired;
level.overrideMethods[level.commonFunctions.setDvar] = ::SetDvarIfUninitializedWrapper;
level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper;
level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper;
level.overrideMethods[level.commonFunctions.changeTeam] = ::ChangeTeam;
level.overrideMethods[level.commonFunctions.getTeamCounts] = ::CountPlayers;
level.overrideMethods[level.commonFunctions.getMaxClients] = ::GetMaxClients;
level.overrideMethods[level.commonFunctions.getTeamBased] = ::GetTeamBased;
level.overrideMethods[level.commonFunctions.getClientTeam] = ::GetClientTeam;
level.overrideMethods[level.commonFunctions.getClientKillStreak] = ::GetClientKillStreak;
level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData] = ::BackupRestoreClientKillStreakData;
level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout;
level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper;
level.overrideMethods[level.commonFunctions.getInboundData] = ::GetInboundData;
level.overrideMethods[level.commonFunctions.getOutboundData] = ::GetOutboundData;
level.overrideMethods[level.commonFunctions.setInboundData] = ::SetInboundData;
level.overrideMethods[level.commonFunctions.setOutboundData] = ::SetOutboundData;
level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired;
level.overrideMethods["SetDvarIfUninitialized"] = ::_SetDvarIfUninitialized;
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
RegisterClientCommands();
level notify( level.notifyTypes.gameFunctionsInitialized );
scripts\_integration_base::_SetDvarIfUninitialized( level.commonKeys.busdir, GetDvar( "fs_homepath" ) + "userraw/" + "scriptdata" );
if ( GetDvarInt( level.commonKeys.enabled ) != 1 )
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
thread OnPlayerConnect();
level thread OnPlayerConnect();
}
OnPlayerConnect()
@ -56,7 +40,7 @@ OnPlayerConnect()
{
level waittill( "connected", player );
if ( player IsTestClient() )
if ( scripts\_integration_base::_IsBot( player ) )
{
// we don't want to track bots
continue;
@ -92,7 +76,7 @@ WaitForClientEvents()
for ( ;; )
{
self waittill( level.eventTypes.eventAvailable, event );
self waittill( level.eventTypes.localClientEvent, event );
scripts\_integration_base::LogDebug( "Received client event " + event.type );
@ -104,114 +88,17 @@ WaitForClientEvents()
}
}
GetInboundData( location )
{
return FileRead( location );
}
GetOutboundData( location )
{
return FileRead( location );
}
SetInboundData( location, data )
{
FileWrite( location, data, "write" );
}
SetOutboundData( location, data )
{
FileWrite( location, data, "write" );
}
GetMaxClients()
{
return level.maxClients;
}
GetTeamBased()
{
return level.teamBased;
}
CountPlayers()
{
return maps\mp\gametypes\_teams::CountPlayers();
}
GetClientTeam()
{
if ( IsDefined( self.pers["team"] ) && self.pers["team"] == "allies" )
{
return "allies";
}
else if ( IsDefined( self.pers["team"] ) && self.pers["team"] == "axis" )
{
return "axis";
}
else
{
return "none";
}
}
GetClientKillStreak()
{
return int( self.pers["cur_kill_streak"] );
}
BackupRestoreClientKillStreakData( restore )
{
if ( restore )
{
foreach ( index, streakStruct in self.pers["killstreaks_backup"] )
{
self.pers["killstreaks"][index] = self.pers["killstreaks_backup"][index];
}
}
else
{
self.pers["killstreaks_backup"] = [];
foreach ( index, streakStruct in self.pers["killstreaks"] )
{
self.pers["killstreaks_backup"][index] = self.pers["killstreaks"][index];
}
}
}
WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 )
{
return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 );
}
ChangeTeam( team )
{
switch ( team )
{
case "allies":
self [[level.allies]]();
break;
case "axis":
self [[level.axis]]();
break;
case "spectator":
self [[level.spectator]]();
break;
}
}
GetTotalShotsFired()
{
return maps\mp\_utility::getPlayerStat( "mostshotsfired" );
}
WaitillNotifyOrTimeoutWrapper( _notify, timeout )
_SetDvarIfUninitialized( dvar, value )
{
SetDvarIfUninitialized( dvar, value );
}
_waittill_notify_or_timeout( _notify, timeout )
{
common_scripts\utility::waittill_notify_or_timeout( _notify, timeout );
}
@ -221,21 +108,6 @@ Log2Console( logLevel, message )
PrintConsole( "[" + logLevel + "] " + message + "\n" );
}
SetDvarIfUninitializedWrapper( dvar, value )
{
SetDvarIfUninitialized( dvar, value );
}
GetXuidWrapper()
{
return self GetXUID();
}
IsBotWrapper( client )
{
return client IsTestClient();
}
//////////////////////////////////
// GUID helpers
/////////////////////////////////
@ -479,9 +351,7 @@ NoClipImpl()
self SetClientDvar( "sv_cheats", 0 );
self God();
self.clientflags |= 1; // IW4x specific
self Noclip();
self Hide();
self.isNoClipped = true;
@ -495,9 +365,7 @@ NoClipImpl()
self SetClientDvar( "sv_cheats", 0 );
self God();
self.clientflags &= ~1; // IW4x specific
self Noclip();
self Show();
self.isNoClipped = false;
@ -548,8 +416,12 @@ HideImpl()
}
AlertImpl( event, data )
{
if ( level.eventBus.gamename == "IW4" )
{
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], "compass_waypoint_target", ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
}
return "Sent alert to " + self.name;
}

View File

@ -1,46 +1,91 @@
#include common_scripts\utility;
#inline scripts\_integration_utility;
Init()
{
thread Setup();
level.eventBus.gamename = "IW5";
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
waittillframeend;
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.eventBus.gamename = "IW5";
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\_integration_base::RegisterLogger( ::Log2Console );
scripts\mp\_integration_base::RegisterLogger( ::Log2Console );
level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired;
level.overrideMethods["SetDvarIfUninitialized"] = ::_SetDvarIfUninitialized;
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
level.overrideMethods[level.commonFunctions.getTotalShotsFired] = ::GetTotalShotsFired;
level.overrideMethods[level.commonFunctions.setDvar] = ::SetDvarIfUninitializedWrapper;
level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper;
level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper;
level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper;
level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout;
RegisterClientCommands();
level notify( level.notifyTypes.gameFunctionsInitialized );
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
level thread OnPlayerConnect();
}
OnPlayerConnect()
{
level endon ( "game_ended" );
for ( ;; )
{
level waittill( "connected", player );
if ( scripts\mp\_integration_base::_IsBot( player ) )
{
// we don't want to track bots
continue;
}
player thread SetPersistentData();
player thread WaitForClientEvents();
}
}
RegisterClientCommands()
{
scripts\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
scripts\mp\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\mp\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\mp\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\mp\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\mp\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\mp\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\mp\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\mp\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\mp\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\mp\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\mp\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
WaitForClientEvents()
{
self endon( "disconnect" );
// example of requesting a meta value
lastServerMetaKey = "LastServerPlayed";
// self scripts\mp\_integration_base::RequestClientMeta( lastServerMetaKey );
for ( ;; )
{
self waittill( level.eventTypes.localClientEvent, event );
scripts\mp\_integration_base::LogDebug( "Received client event " + event.type );
if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey )
{
clientData = self.pers[level.clientDataKey];
lastServerPlayed = clientData.meta[lastServerMetaKey];
}
}
}
GetTotalShotsFired()
@ -48,12 +93,12 @@ GetTotalShotsFired()
return maps\mp\_utility::getPlayerStat( "mostshotsfired" );
}
SetDvarIfUninitializedWrapper( dvar, value )
_SetDvarIfUninitialized( dvar, value )
{
SetDvarIfUninitialized( dvar, value );
}
WaitillNotifyOrTimeoutWrapper( _notify, timeout )
_waittill_notify_or_timeout( _notify, timeout )
{
common_scripts\utility::waittill_notify_or_timeout( _notify, timeout );
}
@ -63,19 +108,135 @@ Log2Console( logLevel, message )
Print( "[" + logLevel + "] " + message + "\n" );
}
IsBotWrapper( client )
//////////////////////////////////
// GUID helpers
/////////////////////////////////
SetPersistentData()
{
return client IsTestClient();
self endon( "disconnect" );
guidHigh = self GetPlayerData( "bests", "none" );
guidLow = self GetPlayerData( "awards", "none" );
persistentGuid = guidHigh + "," + guidLow;
guidIsStored = guidHigh != 0 && guidLow != 0;
if ( guidIsStored )
{
// give IW4MAdmin time to collect IP
wait( 15 );
scripts\mp\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid );
scripts\mp\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid );
return;
}
GetXuidWrapper()
{
return self GetXUID();
guid = self SplitGuid();
scripts\mp\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow );
self SetPlayerData( "bests", "none", guid["high"] );
self SetPlayerData( "awards", "none", guid["low"] );
}
WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 )
SplitGuid()
{
return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 );
guid = self GetGuid();
if ( isDefined( self.guid ) )
{
guid = self.guid;
}
firstPart = 0;
secondPart = 0;
stringLength = 17;
firstPartExp = 0;
secondPartExp = 0;
for ( i = stringLength - 1; i > 0; i-- )
{
char = GetSubStr( guid, i - 1, i );
if ( char == "" )
{
char = "0";
}
if ( i > stringLength / 2 )
{
value = GetIntForHexChar( char );
power = Pow( 16, secondPartExp );
secondPart = secondPart + ( value * power );
secondPartExp++;
}
else
{
value = GetIntForHexChar( char );
power = Pow( 16, firstPartExp );
firstPart = firstPart + ( value * power );
firstPartExp++;
}
}
split = [];
split["low"] = int( secondPart );
split["high"] = int( firstPart );
return split;
}
Pow( num, exponent )
{
result = 1;
while( exponent != 0 )
{
result = result * num;
exponent--;
}
return result;
}
GetIntForHexChar( char )
{
char = ToLower( char );
// generated by co-pilot because I can't be bothered to make it more "elegant"
switch( char )
{
case "0":
return 0;
case "1":
return 1;
case "2":
return 2;
case "3":
return 3;
case "4":
return 4;
case "5":
return 5;
case "6":
return 6;
case "7":
return 7;
case "8":
return 8;
case "9":
return 9;
case "a":
return 10;
case "b":
return 11;
case "c":
return 12;
case "d":
return 13;
case "e":
return 14;
case "f":
return 15;
default:
return 0;
}
}
//////////////////////////////////
@ -84,7 +245,10 @@ WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 )
GiveWeaponImpl( event, data )
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
@ -95,7 +259,10 @@ GiveWeaponImpl( event, data )
TakeWeaponsImpl()
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
@ -105,7 +272,10 @@ TakeWeaponsImpl()
TeamSwitchImpl()
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
@ -123,7 +293,10 @@ TeamSwitchImpl()
LockControlsImpl()
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
@ -160,8 +333,6 @@ LockControlsImpl()
NoClipImpl()
{
_VERIFY_PLAYER_ENT( self );
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
@ -207,11 +378,10 @@ NoClipImpl()
HideImpl()
{
_VERIFY_PLAYER_ENT( self );
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
@ -249,7 +419,10 @@ HideImpl()
AlertImpl( event, data )
{
if ( level.eventBus.gamename == "IW5" ) {
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
}
return "Sent alert to " + self.name;
}
@ -267,8 +440,6 @@ GotoImpl( event, data )
GotoCoordImpl( data )
{
_VERIFY_PLAYER_ENT( self );
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
@ -282,8 +453,6 @@ GotoCoordImpl( data )
GotoPlayerImpl( target )
{
_VERIFY_PLAYER_ENT( self );
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
@ -296,7 +465,10 @@ GotoPlayerImpl( target )
PlayerToMeImpl( event )
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
@ -304,7 +476,10 @@ PlayerToMeImpl( event )
KillImpl()
{
_IS_ALIVE( self );
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
@ -314,8 +489,6 @@ KillImpl()
SetSpectatorImpl()
{
_VERIFY_PLAYER_ENT( self );
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";

View File

@ -1,889 +0,0 @@
Init()
{
thread Setup();
}
Setup()
{
wait ( 0.05 );
level endon( "game_ended" );
level waittill( level.notifyTypes.integrationBootstrapInitialized );
level.commonFunctions.changeTeam = "ChangeTeam";
level.commonFunctions.getTeamCounts = "GetTeamCounts";
level.commonFunctions.getMaxClients = "GetMaxClients";
level.commonFunctions.getTeamBased = "GetTeamBased";
level.commonFunctions.getClientTeam = "GetClientTeam";
level.commonFunctions.getClientKillStreak = "GetClientKillStreak";
level.commonFunctions.backupRestoreClientKillStreakData = "BackupRestoreClientKillStreakData";
level.commonFunctions.getTotalShotsFired = "GetTotalShotsFired";
level.commonFunctions.waitTillAnyTimeout = "WaitTillAnyTimeout";
level.commonFunctions.isBot = "IsBot";
level.commonFunctions.getXuid = "GetXuid";
level.overrideMethods[level.commonFunctions.changeTeam] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getTeamCounts] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getTeamBased] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getMaxClients] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getClientTeam] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getClientKillStreak] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.getXuid] = scripts\_integration_base::NotImplementedFunction;
level.overrideMethods[level.commonFunctions.isBot] = scripts\_integration_base::NotImplementedFunction;
// these can be overridden per game if needed
level.commonKeys.team1 = "allies";
level.commonKeys.team2 = "axis";
level.commonKeys.teamSpectator = "spectator";
level.commonKeys.autoBalance = "sv_iw4madmin_autobalance";
level.eventTypes.connect = "connected";
level.eventTypes.disconnect = "disconnect";
level.eventTypes.joinTeam = "joined_team";
level.eventTypes.joinSpec = "joined_spectators";
level.eventTypes.spawned = "spawned_player";
level.eventTypes.gameEnd = "game_ended";
level.eventTypes.urlRequested = "UrlRequested";
level.eventTypes.urlRequestCompleted = "UrlRequestCompleted";
level.eventTypes.registerCommandRequested = "RegisterCommandRequested";
level.eventTypes.getCommandsRequested = "GetCommandsRequested";
level.eventTypes.getBusModeRequested = "GetBusModeRequested";
level.eventCallbacks[level.eventTypes.urlRequestCompleted] = ::OnUrlRequestCompletedCallback;
level.eventCallbacks[level.eventTypes.getCommandsRequested] = ::OnCommandsRequestedCallback;
level.eventCallbacks[level.eventTypes.getBusModeRequested] = ::OnBusModeRequestedCallback;
level.iw4madminIntegrationDefaultPerformance = 200;
level.notifyEntities = [];
level.customCommands = [];
level notify( level.notifyTypes.sharedFunctionsInitialized );
level waittill( level.notifyTypes.gameFunctionsInitialized );
scripts\_integration_base::_SetDvarIfUninitialized( level.commonKeys.autoBalance, 0 );
if ( GetDvarInt( level.commonKeys.enabled ) != 1 )
{
return;
}
thread OnPlayerConnect();
}
_IsBot( player )
{
return [[level.overrideMethods[level.commonFunctions.isBot]]]( player );
}
OnPlayerConnect()
{
level endon( level.eventTypes.gameEnd );
for ( ;; )
{
level waittill( level.eventTypes.connect, player );
if ( _IsBot( player ) )
{
// we don't want to track bots
continue;
}
if ( !IsDefined( player.pers[level.clientDataKey] ) )
{
player.pers[level.clientDataKey] = spawnstruct();
}
player thread OnPlayerSpawned();
player thread OnPlayerJoinedTeam();
player thread OnPlayerJoinedSpectators();
player thread PlayerTrackingOnInterval();
if ( GetDvarInt( level.commonKeys.autoBalance ) != 1 || !IsDefined( [[level.overrideMethods[level.commonFunctions.getTeamBased]]]() ) )
{
continue;
}
if ( ![[level.overrideMethods[level.commonFunctions.getTeamBased]]]() )
{
continue;
}
teamToJoin = player GetTeamToJoin();
player [[level.overrideMethods[level.commonFunctions.changeTeam]]]( teamToJoin );
player thread OnPlayerFirstSpawn();
player thread OnPlayerDisconnect();
}
}
PlayerSpawnEvents()
{
self endon( level.eventTypes.disconnect );
clientData = self.pers[level.clientDataKey];
// this gives IW4MAdmin some time to register the player before making the request;
// although probably not necessary some users might have a slow database or poll rate
wait ( 2 );
if ( IsDefined( clientData.state ) && clientData.state == "complete" )
{
return;
}
self scripts\_integration_base::RequestClientBasicData();
self waittill( level.eventTypes.clientDataReceived, clientEvent );
if ( clientData.permissionLevel == "User" || clientData.permissionLevel == "Flagged" )
{
return;
}
self IPrintLnBold( "Welcome, your level is ^5" + clientData.permissionLevel );
wait( 2.0 );
self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection + " ago" );
}
PlayerTrackingOnInterval()
{
self endon( level.eventTypes.disconnect );
for ( ;; )
{
wait ( 120 );
if ( IsAlive( self ) )
{
self SaveTrackingMetrics();
}
}
}
SaveTrackingMetrics()
{
if ( !IsDefined( self.persistentClientId ) )
{
return;
}
scripts\_integration_base::LogDebug( "Saving tracking metrics for " + self.persistentClientId );
if ( !IsDefined( self.lastShotCount ) )
{
self.lastShotCount = 0;
}
currentShotCount = self [[level.overrideMethods["GetTotalShotsFired"]]]();
change = currentShotCount - self.lastShotCount;
self.lastShotCount = currentShotCount;
scripts\_integration_base::LogDebug( "Total Shots Fired increased by " + change );
if ( !IsDefined( change ) )
{
change = 0;
}
if ( change == 0 )
{
return;
}
scripts\_integration_base::IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId );
}
OnBusModeRequestedCallback( event )
{
data = [];
data["mode"] = GetDvar( level.commonKeys.busMode );
data["directory"] = GetDvar( level.commonKeys.busDir );
data["inLocation"] = level.eventBus.inLocation;
data["outLocation"] = level.eventBus.outLocation;
scripts\_integration_base::LogDebug( "Bus mode requested" );
busModeRequest = scripts\_integration_base::BuildEventRequest( false, level.eventTypes.getBusModeRequested, "", undefined, data );
scripts\_integration_base::QueueEvent( busModeRequest, level.eventTypes.getBusModeRequested, undefined );
scripts\_integration_base::LogDebug( "Bus mode updated" );
if ( GetDvar( level.commonKeys.busMode ) == "file" && GetDvar( level.commonKeys.busDir ) != "" )
{
level.busMethods[level.commonFunctions.getInboundData] = level.overrideMethods[level.commonFunctions.getInboundData];
level.busMethods[level.commonFunctions.getOutboundData] = level.overrideMethods[level.commonFunctions.getOutboundData];
level.busMethods[level.commonFunctions.setInboundData] = level.overrideMethods[level.commonFunctions.setInboundData];
level.busMethods[level.commonFunctions.setOutboundData] = level.overrideMethods[level.commonFunctions.setOutboundData];
}
}
// #region register script command
OnCommandsRequestedCallback( event )
{
scripts\_integration_base::LogDebug( "Get commands requested" );
thread SendCommands( event.data["name"] );
}
SendCommands( commandName )
{
level endon( level.eventTypes.gameEnd );
for ( i = 0; i < level.customCommands.size; i++ )
{
data = level.customCommands[i];
if ( IsDefined( commandName ) && commandName != data["name"] )
{
continue;
}
scripts\_integration_base::LogDebug( "Sending custom command " + ( i + 1 ) + "/" + level.customCommands.size + ": " + data["name"] );
commandRegisterRequest = scripts\_integration_base::BuildEventRequest( false, level.eventTypes.registerCommandRequested, "", undefined, data );
// not threading here as there might be a lot of commands to register
scripts\_integration_base::QueueEvent( commandRegisterRequest, level.eventTypes.registerCommandRequested, undefined );
}
}
RegisterScriptCommandObject( command )
{
RegisterScriptCommand( command.eventKey, command.name, command.alias, command.description, command.minPermission, command.supportedGames, command.requiresTarget, command.handler );
}
RegisterScriptCommand( eventKey, name, alias, description, minPermission, supportedGames, requiresTarget, handler )
{
if ( !IsDefined( eventKey ) )
{
scripts\_integration_base::LogError( "eventKey must be provided for script command" );
return;
}
data = [];
data["eventKey"] = eventKey;
if ( IsDefined( name ) )
{
data["name"] = name;
}
else
{
scripts\_integration_base::LogError( "name must be provided for script command" );
return;
}
if ( IsDefined( alias ) )
{
data["alias"] = alias;
}
if ( IsDefined( description ) )
{
data["description"] = description;
}
if ( IsDefined( minPermission ) )
{
data["minPermission"] = minPermission;
}
if ( IsDefined( supportedGames ) )
{
data["supportedGames"] = supportedGames;
}
data["requiresTarget"] = false;
if ( IsDefined( requiresTarget ) )
{
data["requiresTarget"] = requiresTarget;
}
if ( IsDefined( handler ) )
{
level.clientCommandCallbacks[eventKey + "Execute"] = handler;
level.clientCommandRusAsTarget[eventKey + "Execute"] = data["requiresTarget"];
}
else
{
scripts\_integration_base::LogWarning( "handler not defined for script command " + name );
}
level.customCommands[level.customCommands.size] = data;
}
// #end region
// #region web requests
RequestUrlObject( request )
{
return RequestUrl( request.url, request.method, request.body, request.headers, request );
}
RequestUrl( url, method, body, headers, webNotify )
{
if ( !IsDefined( webNotify ) )
{
webNotify = SpawnStruct();
webNotify.url = url;
webNotify.method = method;
webNotify.body = body;
webNotify.headers = headers;
}
webNotify.index = GetNextNotifyEntity();
scripts\_integration_base::LogDebug( "next notify index is " + webNotify.index );
level.notifyEntities[webNotify.index] = webNotify;
data = [];
data["url"] = webNotify.url;
data["entity"] = webNotify.index;
if ( IsDefined( method ) )
{
data["method"] = method;
}
if ( IsDefined( body ) )
{
data["body"] = body;
}
if ( IsDefined( headers ) )
{
headerString = "";
keys = GetArrayKeys( headers );
for ( i = 0; i < keys.size; i++ )
{
headerString = headerString + keys[i] + ":" + headers[keys[i]] + ",";
}
data["headers"] = headerString;
}
webNotifyEvent = scripts\_integration_base::BuildEventRequest( true, level.eventTypes.urlRequested, "", webNotify.index, data );
thread scripts\_integration_base::QueueEvent( webNotifyEvent, level.eventTypes.urlRequested, webNotify );
webNotify thread WaitForUrlRequestComplete();
return webNotify;
}
WaitForUrlRequestComplete()
{
level endon( level.eventTypes.gameEnd );
timeoutResult = self [[level.overrideMethods[level.commonFunctions.waitTillAnyTimeout]]]( 30, level.eventTypes.urlRequestCompleted );
if ( timeoutResult == level.eventBus.timeoutKey )
{
scripts\_integration_base::LogWarning( "Request to " + self.url + " timed out" );
self notify ( level.eventTypes.urlRequestCompleted, "error" );
}
scripts\_integration_base::LogDebug( "Request to " + self.url + " completed" );
level.notifyEntities[self.index] = undefined;
}
OnUrlRequestCompletedCallback( event )
{
if ( !IsDefined( event ) || !IsDefined( event.data ) )
{
scripts\_integration_base::LogWarning( "Incomplete data for url request callback. [1]" );
return;
}
notifyEnt = event.data["entity"];
response = event.data["response"];
if ( !IsDefined( notifyEnt ) || !IsDefined( response ) )
{
scripts\_integration_base::LogWarning( "Incomplete data for url request callback. [2] " + scripts\_integration_base::CoerceUndefined( notifyEnt ) + " , " + scripts\_integration_base::CoerceUndefined( response ) );
return;
}
webNotify = level.notifyEntities[int( notifyEnt )];
if ( !IsDefined( webNotify.response ) )
{
webNotify.response = response;
}
else
{
webNotify.response = webNotify.response + response;
}
if ( int( event.data["remaining"] ) != 0 )
{
scripts\_integration_base::LogDebug( "Additional data available for url request " + notifyEnt + " (" + event.data["remaining"] + " chunks remaining)" );
return;
}
scripts\_integration_base::LogDebug( "Notifying " + notifyEnt + " that url request completed" );
webNotify notify( level.eventTypes.urlRequestCompleted, webNotify.response );
}
GetNextNotifyEntity()
{
max = level.notifyEntities.size + 1;
for ( i = 0; i < max; i++ )
{
if ( !IsDefined( level.notifyEntities[i] ) )
{
return i;
}
}
return max;
}
// #end region
// #region team balance
OnPlayerDisconnect()
{
level endon( level.eventTypes.gameEnd );
self endon( "disconnect_logic_end" );
for ( ;; )
{
self waittill( level.eventTypes.disconnect );
scripts\_integration_base::LogDebug( "client is disconnecting" );
OnTeamSizeChanged();
self notify( "disconnect_logic_end" );
}
}
OnPlayerJoinedTeam()
{
self endon( level.eventTypes.disconnect );
for( ;; )
{
self waittill( level.eventTypes.joinTeam );
wait( 0.25 );
LogPrint( GenerateJoinTeamString( false ) );
if ( GetDvarInt( level.commonKeys.autoBalance ) != 1 )
{
continue;
}
if ( IsDefined( self.wasAutoBalanced ) && self.wasAutoBalanced )
{
self.wasAutoBalanced = false;
continue;
}
newTeam = self [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
scripts\_integration_base::LogDebug( self.name + " switched to " + newTeam );
if ( newTeam != level.commonKeys.team1 && newTeam != level.commonKeys.team2 )
{
OnTeamSizeChanged();
scripts\_integration_base::LogDebug( "not force balancing " + self.name + " because they switched to spec" );
continue;
}
properTeam = self GetTeamToJoin();
if ( newTeam != properTeam )
{
self [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( false );
self [[level.overrideMethods[level.commonFunctions.changeTeam]]]( properTeam );
wait ( 0.1 );
self [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( true );
}
}
}
OnPlayerSpawned()
{
self endon( level.eventTypes.disconnect );
for ( ;; )
{
self waittill( level.eventTypes.spawned );
self thread PlayerSpawnEvents();
}
}
OnPlayerJoinedSpectators()
{
self endon( level.eventTypes.disconnect );
for( ;; )
{
self waittill( level.eventTypes.joinSpec );
LogPrint( GenerateJoinTeamString( true ) );
}
}
OnPlayerFirstSpawn()
{
self endon( level.eventTypes.disconnect );
timeoutResult = self [[level.overrideMethods[level.commonFunctions.waitTillAnyTimeout]]]( 30, level.eventTypes.spawned );
if ( timeoutResult != level.eventBus.timeoutKey )
{
return;
}
scripts\_integration_base::LogDebug( "moving " + self.name + " to spectator because they did not spawn within expected duration" );
self [[level.overrideMethods[level.commonFunctions.changeTeam]]]( level.commonKeys.teamSpectator );
}
OnTeamSizeChanged()
{
if ( level.players.size < 3 )
{
scripts\_integration_base::LogDebug( "not enough clients to autobalance" );
return;
}
if ( !IsDefined( GetSmallerTeam( 1 ) ) )
{
scripts\_integration_base::LogDebug( "teams are not unbalanced enough to auto balance" );
return;
}
toSwap = FindClientToSwap();
curentTeam = toSwap [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
otherTeam = level.commonKeys.team1;
if ( curentTeam == otherTeam )
{
otherTeam = level.commonKeys.team2;
}
toSwap.wasAutoBalanced = true;
if ( !IsDefined( toSwap.autoBalanceCount ) )
{
toSwap.autoBalanceCount = 1;
}
else
{
toSwap.autoBalanceCount++;
}
toSwap [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( false );
scripts\_integration_base::LogDebug( "swapping " + toSwap.name + " from " + curentTeam + " to " + otherTeam );
toSwap [[level.overrideMethods[level.commonFunctions.changeTeam]]]( otherTeam );
wait ( 0.1 ); // give the killstreak on team switch clear event time to execute
toSwap [[level.overrideMethods[level.commonFunctions.backupRestoreClientKillStreakData]]]( true );
}
FindClientToSwap()
{
smallerTeam = GetSmallerTeam( 1 );
teamPool = level.commonKeys.team1;
if ( IsDefined( smallerTeam ) )
{
if ( smallerTeam == teamPool )
{
teamPool = level.commonKeys.team2;
}
}
else
{
teamPerformances = GetTeamPerformances();
team1Perf = teamPerformances[level.commonKeys.team1];
team2Perf = teamPerformances[level.commonKeys.team2];
teamPool = level.commonKeys.team1;
if ( team2Perf > team1Perf )
{
teamPool = level.commonKeys.team2;
}
}
client = GetBestSwapCandidate( teamPool );
if ( !IsDefined( client ) )
{
scripts\_integration_base::LogDebug( "could not find candidate to swap teams" );
}
else
{
scripts\_integration_base::LogDebug( "best candidate to swap teams is " + client.name );
}
return client;
}
GetBestSwapCandidate( team )
{
candidates = [];
maxClients = [[level.overrideMethods[level.commonFunctions.getMaxClients]]]();
for ( i = 0; i < maxClients; i++ )
{
candidates[i] = GetClosestPerformanceClientForTeam( team, candidates );
}
candidate = undefined;
foundCandidate = false;
for ( i = 0; i < maxClients; i++ )
{
if ( !IsDefined( candidates[i] ) )
{
continue;
}
candidate = candidates[i];
candidateKillStreak = candidate [[level.overrideMethods[level.commonFunctions.getClientKillStreak]]]();
scripts\_integration_base::LogDebug( "candidate killstreak is " + candidateKillStreak );
if ( candidateKillStreak > 3 )
{
scripts\_integration_base::LogDebug( "skipping candidate selection for " + candidate.name + " because their kill streak is too high" );
continue;
}
if ( IsDefined( candidate.autoBalanceCount ) && candidate.autoBalanceCount > 2 )
{
scripts\_integration_base::LogDebug( "skipping candidate selection for " + candidate.name + " they have been swapped too many times" );
continue;
}
foundCandidate = true;
break;
}
if ( foundCandidate )
{
return candidate;
}
return undefined;
}
GetClosestPerformanceClientForTeam( sourceTeam, excluded )
{
if ( !IsDefined( excluded ) )
{
excluded = [];
}
otherTeam = level.commonKeys.team1;
if ( sourceTeam == otherTeam )
{
otherTeam = level.commonKeys.team2;
}
teamPerformances = GetTeamPerformances();
players = level.players;
choice = undefined;
closest = 9999999;
for ( i = 0; i < players.size; i++ )
{
isExcluded = false;
for ( j = 0; j < excluded.size; j++ )
{
if ( excluded[j] == players[i] )
{
isExcluded = true;
break;
}
}
if ( isExcluded )
{
continue;
}
if ( players[i] [[level.overrideMethods[level.commonFunctions.getClientTeam]]]() != sourceTeam )
{
continue;
}
clientPerformance = players[i] GetClientPerformanceOrDefault();
sourceTeamNewPerformance = teamPerformances[sourceTeam] - clientPerformance;
otherTeamNewPerformance = teamPerformances[otherTeam] + clientPerformance;
candidateValue = Abs( sourceTeamNewPerformance - otherTeamNewPerformance );
scripts\_integration_base::LogDebug( "perf=" + clientPerformance + " candidateValue=" + candidateValue + " src=" + sourceTeamNewPerformance + " dst=" + otherTeamNewPerformance );
if ( !IsDefined( choice ) )
{
choice = players[i];
closest = candidateValue;
}
else if ( candidateValue < closest )
{
scripts\_integration_base::LogDebug( candidateValue + " is the new best value " );
choice = players[i];
closest = candidateValue;
}
}
scripts\_integration_base::LogDebug( choice.name + " is the best candidate to swap" + " with closest=" + closest );
return choice;
}
GetTeamToJoin()
{
smallerTeam = GetSmallerTeam( 1 );
if ( IsDefined( smallerTeam ) )
{
return smallerTeam;
}
teamPerformances = GetTeamPerformances( self );
if ( teamPerformances[level.commonKeys.team1] < teamPerformances[level.commonKeys.team2] )
{
scripts\_integration_base::LogDebug( "Team1 performance is lower, so selecting Team1" );
return level.commonKeys.team1;
}
else
{
scripts\_integration_base::LogDebug( "Team2 performance is lower, so selecting Team2" );
return level.commonKeys.team2;
}
}
GetSmallerTeam( minDiff )
{
teamCounts = [[level.overrideMethods[level.commonFunctions.getTeamCounts]]]();
team1Count = teamCounts[level.commonKeys.team1];
team2Count = teamCounts[level.commonKeys.team2];
maxClients = [[level.overrideMethods[level.commonFunctions.getMaxClients]]]();
if ( team1Count == team2Count )
{
return undefined;
}
if ( team2Count == maxClients / 2 )
{
scripts\_integration_base::LogDebug( "Team2 is full, so selecting Team1" );
return level.commonKeys.team1;
}
if ( team1Count == maxClients / 2 )
{
scripts\_integration_base::LogDebug( "Team1 is full, so selecting Team2" );
return level.commonKeys.team2;
}
sizeDiscrepancy = Abs( team1Count - team2Count );
if ( sizeDiscrepancy > minDiff )
{
scripts\_integration_base::LogDebug( "Team size differs by more than 1" );
if ( team1Count < team2Count )
{
scripts\_integration_base::LogDebug( "Team1 is smaller, so selecting Team1" );
return level.commonKeys.team1;
}
else
{
scripts\_integration_base::LogDebug( "Team2 is smaller, so selecting Team2" );
return level.commonKeys.team2;
}
}
return undefined;
}
GetTeamPerformances( ignoredClient )
{
players = level.players;
team1 = 0;
team2 = 0;
for ( i = 0; i < players.size; i++ )
{
if ( IsDefined( ignoredClient ) && players[i] == ignoredClient )
{
continue;
}
performance = players[i] GetClientPerformanceOrDefault();
clientTeam = players[i] [[level.overrideMethods[level.commonFunctions.getClientTeam]]]();
if ( clientTeam == level.commonKeys.team1 )
{
team1 = team1 + performance;
}
else
{
team2 = team2 + performance;
}
}
result = [];
result[level.commonKeys.team1] = team1;
result[level.commonKeys.team2] = team2;
return result;
}
GetClientPerformanceOrDefault()
{
clientData = self.pers[level.clientDataKey];
performance = level.iw4madminIntegrationDefaultPerformance;
if ( IsDefined( clientData ) && IsDefined( clientData.performance ) )
{
performance = int( clientData.performance );
}
return performance;
}
GenerateJoinTeamString( isSpectator )
{
team = self.team;
if ( IsDefined( self.joining_team ) )
{
team = self.joining_team;
}
else
{
if ( isSpectator || !IsDefined( team ) )
{
team = "spectator";
}
}
guid = self [[level.overrideMethods[level.commonFunctions.getXuid]]]();
if ( guid == "0" )
{
guid = self.guid;
}
if ( !IsDefined( guid ) || guid == "0" )
{
guid = "undefined";
}
return "JT;" + guid + ";" + self getEntityNumber() + ";" + team + ";" + self.name + "\n";
}
// #end region

View File

@ -2,43 +2,90 @@
Init()
{
thread Setup();
level.eventBus.gamename = "T5";
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
waittillframeend;
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.eventBus.gamename = "T5";
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\_integration_base::RegisterLogger( ::Log2Console );
scripts\mp\_integration_base::RegisterLogger( ::Log2Console );
level.overrideMethods[level.commonFunctions.getTotalShotsFired] = ::GetTotalShotsFired;
level.overrideMethods[level.commonFunctions.setDvar] = ::SetDvarIfUninitializedWrapper;
level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper;
level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper;
level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper;
level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired;
level.overrideMethods["SetDvarIfUninitialized"] = ::_SetDvarIfUninitialized;
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
RegisterClientCommands();
level notify( level.notifyTypes.gameFunctionsInitialized );
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
level thread OnPlayerConnect();
}
OnPlayerConnect()
{
level endon ( "game_ended" );
for ( ;; )
{
level waittill( "connected", player );
if ( scripts\mp\_integration_base::_IsBot( player ) )
{
// we don't want to track bots
continue;
}
//player thread SetPersistentData();
player thread WaitForClientEvents();
}
}
RegisterClientCommands()
{
scripts\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
scripts\mp\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\mp\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\mp\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\mp\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\mp\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\mp\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\mp\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\mp\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\mp\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\mp\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\mp\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
WaitForClientEvents()
{
self endon( "disconnect" );
// example of requesting a meta value
lastServerMetaKey = "LastServerPlayed";
// self scripts\mp\_integration_base::RequestClientMeta( lastServerMetaKey );
for ( ;; )
{
self waittill( level.eventTypes.localClientEvent, event );
scripts\mp\_integration_base::LogDebug( "Received client event " + event.type );
if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey )
{
clientData = self.pers[level.clientDataKey];
lastServerPlayed = clientData.meta[lastServerMetaKey];
}
}
}
GetTotalShotsFired()
@ -46,12 +93,12 @@ GetTotalShotsFired()
return maps\mp\gametypes\_persistence::statGet( "total_shots" );
}
SetDvarIfUninitializedWrapper( dvar, value )
_SetDvarIfUninitialized(dvar, value)
{
maps\mp\_utility::set_dvar_if_unset(dvar, value);
}
WaitillNotifyOrTimeoutWrapper( msg, timer )
_waittill_notify_or_timeout( msg, timer )
{
self endon( msg );
wait( timer );
@ -64,6 +111,7 @@ Log2Console( logLevel, message )
God()
{
if ( !IsDefined( self.godmode ) )
{
self.godmode = false;
@ -81,16 +129,137 @@ God()
}
}
IsBotWrapper( client )
//////////////////////////////////
// GUID helpers
/////////////////////////////////
/*SetPersistentData()
{
return client maps\mp\_utility::is_bot();
self endon( "disconnect" );
guidHigh = self GetPlayerData( "bests", "none" );
guidLow = self GetPlayerData( "awards", "none" );
persistentGuid = guidHigh + "," + guidLow;
guidIsStored = guidHigh != 0 && guidLow != 0;
if ( guidIsStored )
{
// give IW4MAdmin time to collect IP
wait( 15 );
scripts\mp\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid );
scripts\mp\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid );
return;
}
GetXuidWrapper()
{
return self GetGuid();
guid = self SplitGuid();
scripts\mp\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow );
self SetPlayerData( "bests", "none", guid["high"] );
self SetPlayerData( "awards", "none", guid["low"] );
}
SplitGuid()
{
guid = self GetGuid();
if ( isDefined( self.guid ) )
{
guid = self.guid;
}
firstPart = 0;
secondPart = 0;
stringLength = 17;
firstPartExp = 0;
secondPartExp = 0;
for ( i = stringLength - 1; i > 0; i-- )
{
char = GetSubStr( guid, i - 1, i );
if ( char == "" )
{
char = "0";
}
if ( i > stringLength / 2 )
{
value = GetIntForHexChar( char );
power = Pow( 16, secondPartExp );
secondPart = secondPart + ( value * power );
secondPartExp++;
}
else
{
value = GetIntForHexChar( char );
power = Pow( 16, firstPartExp );
firstPart = firstPart + ( value * power );
firstPartExp++;
}
}
split = [];
split["low"] = int( secondPart );
split["high"] = int( firstPart );
return split;
}
Pow( num, exponent )
{
result = 1;
while( exponent != 0 )
{
result = result * num;
exponent--;
}
return result;
}
GetIntForHexChar( char )
{
char = ToLower( char );
// generated by co-pilot because I can't be bothered to make it more "elegant"
switch( char )
{
case "0":
return 0;
case "1":
return 1;
case "2":
return 2;
case "3":
return 3;
case "4":
return 4;
case "5":
return 5;
case "6":
return 6;
case "7":
return 7;
case "8":
return 8;
case "9":
return 9;
case "a":
return 10;
case "b":
return 11;
case "c":
return 12;
case "d":
return 13;
case "e":
return 14;
case "f":
return 15;
default:
return 0;
}
}*/
//////////////////////////////////
// Command Implementations
/////////////////////////////////
@ -226,7 +395,7 @@ NoClipImpl( event, data )
self IPrintLnBold( "NoClip enabled" );*/
scripts\_integration_base::LogWarning( "NoClip is not supported on T5!" );
scripts\mp\_integration_base::LogWarning( "NoClip is not supported on T5!" );
}

View File

@ -1,384 +0,0 @@
#include common_scripts\utility;
Init()
{
thread Setup();
}
Setup()
{
level endon( "end_game" );
waittillframeend;
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.eventBus.gamename = "T5";
level.eventTypes.gameEnd = "end_game";
scripts\_integration_base::RegisterLogger( ::Log2Console );
level.overrideMethods[level.commonFunctions.getTotalShotsFired] = ::GetTotalShotsFired;
level.overrideMethods[level.commonFunctions.setDvar] = ::SetDvarIfUninitializedWrapper;
level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper;
level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper;
level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper;
level.overrideMethods[level.commonFunction.getPlayerFromClientNum] = ::_GetPlayerFromClientNum;
RegisterClientCommands();
level notify( level.notifyTypes.gameFunctionsInitialized );
}
RegisterClientCommands()
{
scripts\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
GetTotalShotsFired()
{
return 0; //ZM has no shot tracking. TODO: add tracking function for event weapon_fired
}
SetDvarIfUninitializedWrapper( dvar, value )
{
if ( GetDvar( dvar ) == "" )
{
SetDvar( dvar, value );
return value;
}
return GetDvar( dvar );
}
WaitillNotifyOrTimeoutWrapper( msg, timer )
{
self endon( msg );
wait( timer );
}
Log2Console( logLevel, message )
{
Print( "[" + logLevel + "] " + message + "\n" );
}
God()
{
if ( !IsDefined( self.godmode ) )
{
self.godmode = false;
}
if (!self.godmode )
{
self enableInvulnerability();
self.godmode = true;
}
else
{
self.godmode = false;
self disableInvulnerability();
}
}
IsBotWrapper( client )
{
return ( IsDefined ( client.pers["isBot"] ) && client.pers["isBot"] != 0 );
}
GetXuidWrapper()
{
return self GetXUID();
}
_GetPlayerFromClientNum( clientNum )
{
if ( clientNum < 0 )
{
return undefined;
}
players = GetPlayers( "all" );
for ( i = 0; i < players.size; i++ )
{
scripts\_integration_base::LogDebug( i+"/"+players.size+ "=" + players[i].name );
if ( players[i] getEntityNumber() == clientNum )
{
return players[i];
}
}
return undefined;
}
//////////////////////////////////
// Command Implementations
/////////////////////////////////
GiveWeaponImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
self SwitchToWeapon( data["weaponName"] );
return self.name + "^7 has been given ^5" + data["weaponName"];
}
TakeWeaponsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
return "Took weapons from " + self.name;
}
TeamSwitchImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
if ( self.team == "allies" )
{
team = level.axis;
}
self IPrintLnBold( "You are being team switched" );
wait( 2 );
self [[team]]();
return self.name + "^7 switched to " + self.team;
}
LockControlsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
self.isControlLocked = false;
}
if ( !self.isControlLocked )
{
self freezeControls( true );
self God();
self Hide();
info = [];
info[ "alertType" ] = "Alert!";
info[ "message" ] = "You have been frozen!";
self AlertImpl( undefined, info );
self.isControlLocked = true;
return self.name + "\'s controls are locked";
}
else
{
self freezeControls( false );
self God();
self Show();
self.isControlLocked = false;
return self.name + "\'s controls are unlocked";
}
}
NoClipImpl( event, data )
{
/*if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
}
if ( !IsDefined ( self.isNoClipped ) )
{
self.isNoClipped = false;
}
if ( !self.isNoClipped )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = true;
self IPrintLnBold( "NoClip enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = false;
self IPrintLnBold( "NoClip disabled" );
}
self IPrintLnBold( "NoClip enabled" );*/
scripts\_integration_base::LogWarning( "NoClip is not supported on T5!" );
}
HideImpl( event, data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
{
self.isHidden = false;
}
if ( !self.isHidden )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Hide();
self.isHidden = true;
self IPrintLnBold( "Hide enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Show();
self.isHidden = false;
self IPrintLnBold( "Hide disabled" );
}
}
AlertImpl( event, data )
{
//self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "mpl_sab_ui_suitcasebomb_timer", 7.5 );
self IPrintLnBold( data["message"] );
return "Sent alert to " + self.name;
}
GotoImpl( event, data )
{
if ( IsDefined( event.target ) )
{
return self GotoPlayerImpl( event.target );
}
else
{
return self GotoCoordImpl( data );
}
}
GotoCoordImpl( data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
self SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
}
GotoPlayerImpl( target )
{
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
return;
}
self SetOrigin( target GetOrigin() );
self IPrintLnBold( "Moved to " + target.name );
}
PlayerToMeImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
}
KillImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
return "You killed " + self.name;
}
SetSpectatorImpl( event, data )
{
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}

View File

@ -1,395 +0,0 @@
#include common_scripts\utility;
#include maps\mp\_utility;
Init()
{
thread Setup();
}
Setup()
{
level endon( "game_ended" );
level endon( "end_game" );
waittillframeend;
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.eventBus.gamename = "T6";
if ( sessionmodeiszombiesgame() )
{
level.eventTypes.gameEnd = "end_game";
}
scripts\_integration_base::RegisterLogger( ::Log2Console );
level.overrideMethods[level.commonFunctions.getTotalShotsFired] = ::GetTotalShotsFired;
level.overrideMethods[level.commonFunctions.setDvar] = ::SetDvarIfUninitializedWrapper;
level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout] = ::WaitillNotifyOrTimeoutWrapper;
level.overrideMethods[level.commonFunctions.isBot] = ::IsBotWrapper;
level.overrideMethods[level.commonFunctions.getXuid] = ::GetXuidWrapper;
level.overrideMethods[level.commonFunctions.waitTillAnyTimeout] = ::WaitTillAnyTimeout;
RegisterClientCommands();
level notify( level.notifyTypes.gameFunctionsInitialized );
}
RegisterClientCommands()
{
scripts\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
GetTotalShotsFired()
{
return self.pers["total_shots"];
}
SetDvarIfUninitializedWrapper( dvar, value )
{
maps\mp\_utility::set_dvar_if_unset( dvar, value );
}
WaitillNotifyOrTimeoutWrapper( msg, timer )
{
self endon( msg );
wait( timer );
}
Log2Console( logLevel, message )
{
Print( "[" + logLevel + "] " + message + "\n" );
}
God()
{
if ( !IsDefined( self.godmode ) )
{
self.godmode = false;
}
if (!self.godmode )
{
self enableInvulnerability();
self.godmode = true;
}
else
{
self.godmode = false;
self disableInvulnerability();
}
}
IsBotWrapper( client )
{
return client maps\mp\_utility::is_bot();
}
GetXuidWrapper()
{
return self GetXUID();
}
WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 )
{
return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 );
}
//////////////////////////////////
// Command Implementations
/////////////////////////////////
GiveWeaponImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( isDefined( level.player_too_many_weapons_monitor ) && level.player_too_many_weapons_monitor )
{
level.player_too_many_weapons_monitor = false;
self notify( "stop_player_too_many_weapons_monitor" );
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
self SwitchToWeapon( data["weaponName"] );
return self.name + "^7 has been given ^5" + data["weaponName"];
}
TakeWeaponsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
return "Took weapons from " + self.name;
}
TeamSwitchImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
if ( self.team == "allies" )
{
team = level.axis;
}
self IPrintLnBold( "You are being team switched" );
wait( 2 );
self [[team]]();
return self.name + "^7 switched to " + self.team;
}
LockControlsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
self.isControlLocked = false;
}
if ( !self.isControlLocked )
{
self freezeControls( true );
self God();
self Hide();
info = [];
info[ "alertType" ] = "Alert!";
info[ "message" ] = "You have been frozen!";
self AlertImpl( undefined, info );
self.isControlLocked = true;
return self.name + "\'s controls are locked";
}
else
{
self freezeControls( false );
self God();
self Show();
self.isControlLocked = false;
return self.name + "\'s controls are unlocked";
}
}
NoClipImpl( event, data )
{
/*if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
}
if ( !IsDefined ( self.isNoClipped ) )
{
self.isNoClipped = false;
}
if ( !self.isNoClipped )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = true;
self IPrintLnBold( "NoClip enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = false;
self IPrintLnBold( "NoClip disabled" );
}
self IPrintLnBold( "NoClip enabled" );*/
scripts\_integration_base::LogWarning( "NoClip is not supported on T6!" );
}
HideImpl( event, data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
{
self.isHidden = false;
}
if ( !self.isHidden )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Hide();
self.isHidden = true;
self IPrintLnBold( "Hide enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Show();
self.isHidden = false;
self IPrintLnBold( "Hide disabled" );
}
}
AlertImpl( event, data )
{
self thread oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "mpl_sab_ui_suitcasebomb_timer", 7.5 );
return "Sent alert to " + self.name;
}
GotoImpl( event, data )
{
if ( IsDefined( event.target ) )
{
return self GotoPlayerImpl( event.target );
}
else
{
return self GotoCoordImpl( data );
}
}
GotoCoordImpl( data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
self SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
}
GotoPlayerImpl( target )
{
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
return;
}
self SetOrigin( target GetOrigin() );
self IPrintLnBold( "Moved to " + target.name );
}
PlayerToMeImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
}
KillImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
return "You killed " + self.name;
}
SetSpectatorImpl( event, data )
{
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}
//////////////////////////////////
// T6 specific functions
/////////////////////////////////
/*
1:1 the same on MP and ZM but in different includes. Since we probably want to be able to send Alerts on non teambased wagermatches use our own copy.
*/
oldnotifymessage( titletext, notifytext, iconname, glowcolor, sound, duration )
{
/*if ( level.wagermatch && !level.teambased )
{
return;
}*/
notifydata = spawnstruct();
notifydata.titletext = titletext;
notifydata.notifytext = notifytext;
notifydata.iconname = iconname;
notifydata.sound = sound;
notifydata.duration = duration;
self.startmessagenotifyqueue[ self.startmessagenotifyqueue.size ] = notifydata;
self notify( "received award" );
}

View File

@ -1,73 +0,0 @@
/*********************************************************************************
* DISCLAIMER: *
* *
* This script is optional and not required for *
* standard functionality. To use this script, a third-party *
* plugin named "t6-gsc-utils" must be installed on the *
* game server in the "*\storage\t6\plugins" folder *
* *
* The "t6-gsc-utils" plugin can be obtained from the GitHub *
* repository at: *
* https://github.com/fedddddd/t6-gsc-utils *
* *
* Please make sure to install the plugin before running this *
* script. *
*********************************************************************************/
/*********************************************************************************
* FUNCTIONALITY: *
* *
* This script extends the game interface to support the "file" *
* bus mode for Plutonium T6, which allows the game server and IW4M-Admin *
* to communicate via files, rather than over rcon using *
* dvars. *
* *
* By enabling the "file" bus mode, IW4M-Admin can send *
* commands and receive responses from the game server by *
* reading and writing to specific files. This provides a *
* flexible and efficient communication channel. *
* *
* Make sure to configure the server to use the "file" bus *
* mode and set the appropriate file path to *
* establish the communication between IW4M-Admin and the *
* game server. *
* *
* The wiki page for the setup of the game interface, and the bus mode *
* can be found on GitHub at: *
* https://github.com/RaidMax/IW4M-Admin/wiki/GameInterface#configuring-bus-mode *
*********************************************************************************/
Init()
{
thread Setup();
}
Setup()
{
level waittill( level.notifyTypes.sharedFunctionsInitialized );
level.overrideMethods[level.commonFunctions.getInboundData] = ::GetInboundData;
level.overrideMethods[level.commonFunctions.getOutboundData] = ::GetOutboundData;
level.overrideMethods[level.commonFunctions.setInboundData] = ::SetInboundData;
level.overrideMethods[level.commonFunctions.setOutboundData] = ::SetOutboundData;
scripts\_integration_base::_SetDvarIfUninitialized( level.commonKeys.busdir, GetDvar( "fs_homepath" ) );
}
GetInboundData( location )
{
return readFile( location );
}
GetOutboundData( location )
{
return readFile( location );
}
SetInboundData( location, data )
{
writeFile( location, data );
}
SetOutboundData( location, data )
{
writeFile( location, data );
}

View File

@ -1,93 +0,0 @@
Init()
{
level.startmessagedefaultduration = 2;
level.regulargamemessages = spawnstruct();
level.regulargamemessages.waittime = 6;
thread OnPlayerConnect();
}
OnPlayerConnect()
{
for ( ;; )
{
level waittill( "connecting", player );
player thread DisplayPopupsWaiter();
}
}
DisplayPopupsWaiter()
{
self endon( "disconnect" );
self.ranknotifyqueue = [];
if ( !IsDefined( self.pers[ "challengeNotifyQueue" ] ) )
{
self.pers[ "challengeNotifyQueue" ] = [];
}
if ( !IsDefined( self.pers[ "contractNotifyQueue" ] ) )
{
self.pers[ "contractNotifyQueue" ] = [];
}
self.messagenotifyqueue = [];
self.startmessagenotifyqueue = [];
self.wagernotifyqueue = [];
while ( !level.gameended )
{
if ( self.startmessagenotifyqueue.size == 0 && self.messagenotifyqueue.size == 0 )
{
self waittill( "received award" );
}
waittillframeend;
if ( level.gameended )
{
return;
}
else
{
if ( self.startmessagenotifyqueue.size > 0 )
{
nextnotifydata = self.startmessagenotifyqueue[ 0 ];
arrayremoveindex( self.startmessagenotifyqueue, 0, 0 );
if ( IsDefined( nextnotifydata.duration ) )
{
duration = nextnotifydata.duration;
}
else
{
duration = level.startmessagedefaultduration;
}
self maps\mp\gametypes_zm\_hud_message::shownotifymessage( nextnotifydata, duration );
wait ( duration );
continue;
}
else if ( self.messagenotifyqueue.size > 0 )
{
nextnotifydata = self.messagenotifyqueue[ 0 ];
arrayremoveindex( self.messagenotifyqueue, 0, 0 );
if ( IsDefined( nextnotifydata.duration ) )
{
duration = nextnotifydata.duration;
}
else
{
duration = level.regulargamemessages.waittime;
}
self maps\mp\gametypes_zm\_hud_message::shownotifymessage( nextnotifydata, duration );
continue;
}
else
{
wait ( 1 );
}
}
}
}

View File

@ -1,40 +0,0 @@
/*
* This file contains reusable preprocessor directives meant to be used on
* Plutonium & AlterWare clients that are up to date with the latest version.
* Older versions of Plutonium or other clients do not have support for loading
* or parsing "gsh" files.
*/
/*
* Turn off assertions by removing the following define
* gsc-tool will only emit assertions if developer_script dvar is set to 1
* In short, you should not need to remove this define. Just turn them off
* by using the dvar
*/
#define _INTEGRATION_DEBUG
#ifdef _INTEGRATION_DEBUG
#define _VERIFY( cond, msg ) \
assertEx( cond, msg )
#else
// This works as an "empty" define here with gsc-tool
#define _VERIFY( cond, msg )
#endif
// This function is meant to be used inside "client commands"
// If the client is not alive it shall return an error message
#define _IS_ALIVE( ent ) \
_VERIFY( ent, "player entity is not defined" ); \
if ( !IsAlive( ent ) ) \
{ \
return ent.name + "^7 is not alive"; \
}
// This function should be used to verify if a player entity is defined
#define _VERIFY_PLAYER_ENT( ent ) \
_VERIFY( ent, "player entity is not defined" )

View File

@ -1,88 +0,0 @@
Init()
{
// this gives the game interface time to setup
waittillframeend;
thread ModuleSetup();
}
ModuleSetup()
{
// waiting until the game specific functions are ready
level waittill( level.notifyTypes.gameFunctionsInitialized );
RegisterCustomCommands();
}
RegisterCustomCommands()
{
command = SpawnStruct();
// unique key for each command (how iw4madmin identifies the command)
command.eventKey = "PrintLineCommand";
// name of the command (cannot conflict with existing command names)
command.name = "println";
// short version of the command (cannot conflcit with existing command aliases)
command.alias = "pl";
// description of what the command does
command.description = "prints line to game";
// minimum permision required to execute
// valid values: User, Trusted, Moderator, Administrator, SeniorAdmin, Owner
command.minPermission = "Trusted";
// games the command is supported on
// separate with comma or don't define for all
// valid values: IW3, IW4, IW5, IW6, T4, T5, T6, T7, SHG1, CSGO, H1
command.supportedGames = "IW4,IW5,T5,T6";
// indicates if a target player must be provided to execvute on
command.requiresTarget = false;
// code to run when the command is executed
command.handler = ::PrintLnCommandCallback;
// register the command with integration to be send to iw4madmin
scripts\_integration_shared::RegisterScriptCommandObject( command );
// you can also register via parameters
scripts\_integration_shared::RegisterScriptCommand( "AffirmationCommand", "affirm", "af", "provide affirmations", "User", undefined, false, ::AffirmationCommandCallback );
}
PrintLnCommandCallback( event )
{
if ( IsDefined( event.data["args"] ) )
{
IPrintLnBold( event.data["args"] );
return;
}
scripts\_integration_base::LogDebug( "No data was provided for PrintLnCallback" );
}
AffirmationCommandCallback( event, _ )
{
level endon( level.eventTypes.gameEnd );
request = SpawnStruct();
request.url = "https://www.affirmations.dev";
request.method = "GET";
// If making a post request you can also provide more data
// request.body = "Body of the post message";
// request.headers = [];
// request.headers["Authorization"] = "api-key";
scripts\_integration_shared::RequestUrlObject( request );
request waittill( level.eventTypes.urlRequestCompleted, response );
// horrible json parsing.. but it's just an example
parsedResponse = strtok( response, "\"" );
if ( IsPlayer( self ) )
{
self IPrintLnBold ( "^5" + parsedResponse[parsedResponse.size - 2] );
}
}

View File

@ -3,7 +3,14 @@
Allows integration of IW4M-Admin to GSC, mainly used for special commands that need to use GSC in order to work.
But can also be used to read / write metadata from / to a profile and to get the player permission level.
## Installation Guide
## Installation Plutonium IW5
The documentation can be found here: [GameInterface](https://github.com/RaidMax/IW4M-Admin/wiki/GameInterface)
Move `_integration.gsc` to `%localappdata%\Plutonium\storage\iw5\scripts\`
## Installation IW4x
Move `_integration.gsc` to `IW4x/userraw/scripts`, `IW4x` being the root folder of your game server.

View File

@ -1,21 +1,14 @@
@echo off
ECHO "Pluto IW5"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts"
xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts"
xcopy /y .\GameInterface\_integration_iw5.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts"
xcopy /y .\GameInterface\_integration_utility.gsh "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
xcopy /y .\GameInterface\_integration_iw5.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
xcopy /y .\AntiCheat\IW5\storage\iw5\scripts\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
ECHO "Pluto T5"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts"
xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\mp"
xcopy /y .\GameInterface\_integration_t5.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\mp"
xcopy /y .\GameInterface\_integration_t5zm.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\sp\zom"
ECHO "Pluto T6"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts"
xcopy /y .\GameInterface\_integration_shared.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts"
xcopy /y .\GameInterface\_integration_t6.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts"
xcopy /y .\GameInterface\_integration_t6zm_helper.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\zm"
xcopy /y .\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\mp"
xcopy /y .\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc.src "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\mp"

View File

@ -40,6 +40,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\ParserT4.js = Plugins\ScriptPlugins\ParserT4.js
Plugins\ScriptPlugins\ParserT7.js = Plugins\ScriptPlugins\ParserT7.js
Plugins\ScriptPlugins\ParserTeknoMW3.js = Plugins\ScriptPlugins\ParserTeknoMW3.js
Plugins\ScriptPlugins\SampleScriptPluginCommand.js = Plugins\ScriptPlugins\SampleScriptPluginCommand.js
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
Plugins\ScriptPlugins\ParserPlutoniumT4.js = Plugins\ScriptPlugins\ParserPlutoniumT4.js
Plugins\ScriptPlugins\ParserS1x.js = Plugins\ScriptPlugins\ParserS1x.js
@ -51,9 +53,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\BanBroadcasting.js = Plugins\ScriptPlugins\BanBroadcasting.js
Plugins\ScriptPlugins\ParserH1MOD.js = Plugins\ScriptPlugins\ParserH1MOD.js
Plugins\ScriptPlugins\ParserPlutoniumT5.js = Plugins\ScriptPlugins\ParserPlutoniumT5.js
Plugins\ScriptPlugins\ServerBanner.js = Plugins\ScriptPlugins\ServerBanner.js
Plugins\ScriptPlugins\ParserBOIII.js = Plugins\ScriptPlugins\ParserBOIII.js
Plugins\ScriptPlugins\ParserL4D2SM.js = Plugins\ScriptPlugins\ParserL4D2SM.js
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"
@ -73,22 +72,11 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mute", "Plugins\Mute\Mute.csproj", "{259824F3-D860-4233-91D6-FF73D4DD8B18}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameFiles", "GameFiles", "{6CBF412C-EFEE-45F7-80FD-AC402C22CDB9}"
ProjectSection(SolutionItems) = preProject
GameFiles\deploy.bat = GameFiles\deploy.bat
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameInterface", "GameInterface", "{5C2BE2A8-EA1D-424F-88E1-7FC33EEC2E55}"
ProjectSection(SolutionItems) = preProject
GameFiles\GameInterface\_integration_base.gsc = GameFiles\GameInterface\_integration_base.gsc
GameFiles\GameInterface\_integration_iw4x.gsc = GameFiles\GameInterface\_integration_iw4x.gsc
GameFiles\GameInterface\_integration_iw5.gsc = GameFiles\GameInterface\_integration_iw5.gsc
GameFiles\GameInterface\_integration_shared.gsc = GameFiles\GameInterface\_integration_shared.gsc
GameFiles\GameInterface\_integration_t5.gsc = GameFiles\GameInterface\_integration_t5.gsc
GameFiles\GameInterface\_integration_t5zm.gsc = GameFiles\GameInterface\_integration_t5zm.gsc
GameFiles\GameInterface\_integration_t6.gsc = GameFiles\GameInterface\_integration_t6.gsc
GameFiles\GameInterface\_integration_t6zm_helper.gsc = GameFiles\GameInterface\_integration_t6zm_helper.gsc
GameFiles\GameInterface\example_module.gsc = GameFiles\GameInterface\example_module.gsc
GameFiles\GameInterface\_integration_t6_file_bus.gsc = GameFiles\GameInterface\_integration_t6_file_bus.gsc
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AntiCheat", "AntiCheat", "{AB83BAC0-C539-424A-BF00-78487C10753C}"

View File

@ -8,7 +8,6 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Integrations.Cod.SecureRcon;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore;
@ -25,7 +24,6 @@ namespace Integrations.Cod
public class CodRConConnection : IRConConnection
{
private static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new();
private const string PkPattern = "-----BEGIN PRIVATE KEY-----";
public IPEndPoint Endpoint { get; }
public string RConPassword { get; }
@ -58,11 +56,6 @@ namespace Integrations.Cod
{
return await SendQueryAsyncInternal(type, parameters, token);
}
catch (RConException ex) when (ex.IsOperationCancelled)
{
_log.LogDebug(ex, "Could not complete RCon request");
throw;
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
@ -73,8 +66,11 @@ namespace Integrations.Cod
throw;
}
finally
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogDebug("Releasing OnComplete {Count}", ActiveQueries[Endpoint].OnComplete.CurrentCount);
}
if (ActiveQueries[Endpoint].OnComplete.CurrentCount == 0)
{
@ -92,12 +88,8 @@ namespace Integrations.Cod
}
if (!ActiveQueries.TryGetValue(Endpoint, out var connectionState))
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogError("Could not retrieve connection state");
}
throw new InvalidOperationException("Could not get connection state");
}
@ -112,7 +104,7 @@ namespace Integrations.Cod
{
_log.LogDebug("OnComplete did not complete before timeout {Count}",
connectionState.OnComplete.CurrentCount);
throw new RConException("Timed out waiting for access to rcon socket", true);
throw new RConException("Timed out waiting for access to rcon socket");
}
var timeSinceLastQuery = (DateTime.Now - connectionState.LastQuery).TotalMilliseconds;
@ -127,9 +119,9 @@ namespace Integrations.Cod
}
catch (OperationCanceledException)
{
_log.LogDebug("Waiting for flood protect did not complete before timeout {Count}",
_log.LogDebug("Waiting for flood protect did not complete before timeout timeout {Count}",
connectionState.OnComplete.CurrentCount);
throw new RConException("Timed out waiting for flood protect to expire", true);
throw new RConException("Timed out waiting for flood protect to expire");
}
}
@ -153,29 +145,33 @@ namespace Integrations.Cod
switch (type)
{
case StaticHelpers.QueryType.GET_DVAR:
waitForResponse = true;
payload = BuildPayload(_config.CommandPrefixes.RConGetDvar, convertedRConPassword,
convertedParameters);
waitForResponse |= true;
payload = string
.Format(_config.CommandPrefixes.RConGetDvar, convertedRConPassword,
convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.SET_DVAR:
payload = BuildPayload(_config.CommandPrefixes.RConSetDvar, convertedRConPassword,
convertedParameters);
payload = string
.Format(_config.CommandPrefixes.RConSetDvar, convertedRConPassword,
convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.COMMAND:
payload = BuildPayload(_config.CommandPrefixes.RConCommand, convertedRConPassword,
convertedParameters);
payload = string
.Format(_config.CommandPrefixes.RConCommand, convertedRConPassword,
convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.GET_STATUS:
waitForResponse = true;
payload = (_config.CommandPrefixes.RConGetStatus + '\0').Select(Helpers.SafeConversion).ToArray();
waitForResponse |= true;
payload = (_config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.GET_INFO:
waitForResponse = true;
payload = (_config.CommandPrefixes.RConGetInfo + '\0').Select(Helpers.SafeConversion).ToArray();
waitForResponse |= true;
payload = (_config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.COMMAND_STATUS:
waitForResponse = true;
payload = BuildPayload(_config.CommandPrefixes.RConCommand, convertedRConPassword, "status");
waitForResponse |= true;
payload = string.Format(_config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0")
.Select(Convert.ToByte).ToArray();
break;
}
}
@ -193,9 +189,20 @@ namespace Integrations.Cod
throw new RConException("Invalid character encountered when converting encodings");
}
byte[][] response;
byte[][] response = null;
retrySend:
if (connectionState.ConnectionAttempts > 1)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogInformation(
"Retrying RCon message ({ConnectionAttempts}/{AllowedConnectionFailures} attempts) with parameters {Payload}",
connectionState.ConnectionAttempts,
_retryAttempts, parameters);
}
}
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
{
DontFragment = false,
@ -203,43 +210,57 @@ namespace Integrations.Cod
ExclusiveAddressUse = true,
})
{
// wait for send to be ready
try
{
await connectionState.OnSentData.WaitAsync(token);
}
catch (OperationCanceledException)
{
_log.LogDebug("OnSent did not complete in time");
throw new RConException("Timed out waiting for access to RCon send socket");
}
// wait for receive to be ready
try
{
await connectionState.OnReceivedData.WaitAsync(token);
}
catch (OperationCanceledException)
{
_log.LogDebug("OnReceived did not complete in time");
if (connectionState.OnSentData.CurrentCount == 0)
{
connectionState.OnSentData.Release();
}
throw new RConException("Timed out waiting for access to RCon receive socket");
}
connectionState.SendEventArgs.UserToken = new ConnectionUserToken
{
Socket = socket,
CancellationToken = token
};
if (!token.IsCancellationRequested)
{
connectionState.ConnectionAttempts++;
}
connectionState.ReceivedBytes.Clear();
connectionState.BytesReadPerSegment.Clear();
_log.LogDebug(
"Sending {PayloadLength} bytes to [{Endpoint}] ({ConnectionAttempts}/{AllowedConnectionFailures}) parameters {Payload}",
payload.Length, Endpoint, connectionState.ConnectionAttempts, _retryAttempts, parameters);
"Sending {PayloadLength} bytes to [{Endpoint}] ({ConnectionAttempts}/{AllowedConnectionFailures})",
payload.Length, Endpoint, connectionState.ConnectionAttempts, _retryAttempts);
try
{
connectionState.LastQuery = DateTime.Now;
var retryTimeout = StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts);
var overrideTimeout = _parser.OverrideTimeoutForCommand(parameters);
var maxTimeout = !overrideTimeout.HasValue || overrideTimeout == TimeSpan.Zero
? retryTimeout
: overrideTimeout.Value;
using var internalTokenSource = new CancellationTokenSource(maxTimeout);
using var chainedTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(token, internalTokenSource.Token);
if (connectionState.ConnectionAttempts > 1)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogInformation(
"Retrying RCon message ({ConnectionAttempts}/{AllowedConnectionFailures} attempts, {Timeout}ms timeout) with parameters {Payload}",
connectionState.ConnectionAttempts, _retryAttempts,
maxTimeout.TotalMilliseconds, parameters);
}
}
waitForResponse = waitForResponse && overrideTimeout.HasValue;
response = await SendPayloadAsync(socket, payload, waitForResponse, chainedTokenSource.Token);
var timeout = _parser.OverrideTimeoutForCommand(parameters);
waitForResponse = waitForResponse && timeout.HasValue;
response = await SendPayloadAsync(payload, waitForResponse,
timeout ?? TimeSpan.Zero, token);
if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse)
{
@ -252,23 +273,26 @@ namespace Integrations.Cod
catch (OperationCanceledException)
{
_log.LogDebug("OperationCanceledException when waiting for payload send to complete");
// if we timed out due to the cancellation token,
// we don't want to count that as an attempt
if (token.IsCancellationRequested)
{
if (connectionState.ConnectionAttempts > 0)
{
connectionState.ConnectionAttempts--;
_log.LogDebug("OperationCanceledException when waiting for payload send to complete");
connectionState.ConnectionAttempts = 0;
}
throw new RConException("Timed out waiting on retry delay for RCon socket",
token.IsCancellationRequested);
}
catch
{
// we want to retry with a delay
if (connectionState.ConnectionAttempts < _retryAttempts)
{
try
{
await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts), token);
}
catch (OperationCanceledException)
{
_log.LogDebug("OperationCancelled while waiting for retry");
throw;
}
goto retrySend;
}
@ -282,24 +306,25 @@ namespace Integrations.Cod
connectionState.ConnectionAttempts = 0;
throw new NetworkException("Reached maximum retry attempts to send RCon data to server");
}
catch (Exception ex)
finally
{
_log.LogDebug(ex, "RCon Exception");
if (connectionState.ConnectionAttempts < _retryAttempts)
try
{
goto retrySend;
if (connectionState.OnSentData.CurrentCount == 0)
{
connectionState.OnSentData.Release();
}
using (LogContext.PushProperty("Server", Endpoint.ToString()))
if (connectionState.OnReceivedData.CurrentCount == 0)
{
_log.LogWarning(
"Made {ConnectionAttempts} attempts to send RCon data to server, but received no response",
connectionState.ConnectionAttempts);
connectionState.OnReceivedData.Release();
}
}
catch
{
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
// this thread is not notified because it's an event
}
connectionState.ConnectionAttempts = 0;
throw new NetworkException("Reached maximum retry attempts to send RCon data to server");
}
}
@ -315,112 +340,6 @@ namespace Integrations.Cod
? ReassembleSegmentedStatus(response)
: RecombineMessages(response);
var validatedResponse = ValidateResponse(type, responseString);
return validatedResponse;
}
private byte[] BuildPayload(string queryTemplate, string convertedRConPassword, string convertedParameters)
{
byte[] payload;
if (!RConPassword.StartsWith(PkPattern))
{
payload = string
.Format(queryTemplate, convertedRConPassword,
convertedParameters + '\0').Select(Helpers.SafeConversion).ToArray();
}
else
{
var textContent = string
.Format(queryTemplate, "", convertedParameters)
.Replace("rcon", "rconSafe ")
.Replace(" ", "").Split(" ");
payload = Helpers.BuildSafeRconPayload(textContent[0], textContent[1], RConPassword);
}
return payload;
}
private async Task<byte[][]> SendPayloadAsync(Socket rconSocket, byte[] payload, bool waitForResponse,
CancellationToken token = default)
{
var connectionState = ActiveQueries[Endpoint];
if (rconSocket is null)
{
_log.LogDebug("Invalid state");
throw new InvalidOperationException("State is not valid for socket operation");
}
var sentByteCount = await rconSocket.SendToAsync(payload, SocketFlags.None, Endpoint, token);
var complete = sentByteCount == payload.Length;
if (!complete)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning("Could not send data to remote RCon socket on attempt #{ConnectionAttempts}",
connectionState.ConnectionAttempts);
}
rconSocket.Close();
throw new NetworkException("Could not send data to remote RCon socket", rconSocket);
}
if (!waitForResponse)
{
return Array.Empty<byte[]>();
}
_log.LogDebug("Waiting to asynchronously receive data on attempt #{ConnectionAttempts}",
connectionState.ConnectionAttempts);
await ReceiveAndStoreSocketData(rconSocket, token, connectionState);
if (_parser.GameName is Server.Game.IW3 or Server.Game.T4)
{
await Task.Delay(100, token); // CoD4x shenanigans
}
while (rconSocket.Available > 0)
{
await ReceiveAndStoreSocketData(rconSocket, token, connectionState);
}
rconSocket.Close();
return GetResponseData(connectionState);
}
private async Task ReceiveAndStoreSocketData(Socket rconSocket, CancellationToken token,
ConnectionState connectionState)
{
SocketReceiveFromResult result;
try
{
result = await rconSocket.ReceiveFromAsync(connectionState.ReceiveBuffer,
SocketFlags.None, Endpoint, token);
}
// windows quirk that occurs when remote server returns ICMP port unreachable
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionReset)
{
await Task.Delay(Timeout.Infinite, token);
return;
}
if (result.ReceivedBytes == 0)
{
return;
}
var storageBuffer = new byte[result.ReceivedBytes];
Array.Copy(connectionState.ReceiveBuffer, storageBuffer, storageBuffer.Length);
connectionState.ReceivedBytes.Add(storageBuffer);
}
#region Helpers
private string[] ValidateResponse(StaticHelpers.QueryType type, string responseString)
{
// note: not all games respond if the password is wrong or not set
if (responseString.Contains("Invalid password", StringComparison.InvariantCultureIgnoreCase) ||
responseString.Contains("rconpassword"))
@ -444,12 +363,8 @@ namespace Integrations.Cod
? _config.CommandPrefixes.RconGetInfoResponseHeader
: responseHeaderMatch);
if (headerSplit.Length == 2)
if (headerSplit.Length != 2)
{
return headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.StartsWith("^7") ? line[2..] : line).ToArray();
}
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning("Invalid response header from server. Expected {Expected}, but got {Response}",
@ -459,6 +374,12 @@ namespace Integrations.Cod
throw new RConException("Unexpected response header from server");
}
var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.StartsWith("^7") ? line[2..] : line).ToArray();
return splitResponse;
}
/// <summary>
/// reassembles broken status segments into the 'correct' ordering
/// <remarks>this is primarily for T7, and is really only reliable for 2 segments</remarks>
@ -516,11 +437,269 @@ namespace Integrations.Cod
return builder.ToString();
}
private static byte[][] GetResponseData(ConnectionState connectionState)
private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse, TimeSpan overrideTimeout,
CancellationToken token = default)
{
return connectionState.ReceivedBytes.ToArray();
var connectionState = ActiveQueries[Endpoint];
var rconSocket = ((ConnectionUserToken)connectionState.SendEventArgs.UserToken)?.Socket;
if (rconSocket is null)
{
_log.LogDebug("Invalid state");
throw new InvalidOperationException("State is not valid for socket operation");
}
#endregion
if (connectionState.ReceiveEventArgs.RemoteEndPoint == null &&
connectionState.SendEventArgs.RemoteEndPoint == null)
{
// setup the event handlers only once because we're reusing the event args
connectionState.SendEventArgs.Completed += OnDataSent;
connectionState.ReceiveEventArgs.Completed += OnDataReceived;
connectionState.ReceiveEventArgs.UserToken = connectionState.SendEventArgs.UserToken;
connectionState.SendEventArgs.RemoteEndPoint = Endpoint;
connectionState.ReceiveEventArgs.RemoteEndPoint = Endpoint;
connectionState.ReceiveEventArgs.DisconnectReuseSocket = true;
connectionState.SendEventArgs.DisconnectReuseSocket = true;
}
if (connectionState.ReceiveEventArgs.UserToken is ConnectionUserToken { CancellationToken.IsCancellationRequested: true })
{
// after a graceful restart we need to reset the receive user token as the cancellation has been updated
connectionState.ReceiveEventArgs.UserToken = connectionState.SendEventArgs.UserToken;
}
connectionState.SendEventArgs.SetBuffer(payload);
// send the data to the server
var sendDataPending = rconSocket.SendToAsync(connectionState.SendEventArgs);
if (sendDataPending)
{
// the send has not been completed asynchronously
// this really shouldn't ever happen because it's UDP
var complete = await connectionState.OnSentData.WaitAsync(StaticHelpers.SocketTimeout(4), token);
if (!complete)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning("Socket timed out while sending RCon data on attempt {Attempt}",
connectionState.ConnectionAttempts);
}
rconSocket.Close();
throw new NetworkException("Timed out sending RCon data", rconSocket);
}
}
if (!waitForResponse)
{
return Array.Empty<byte[]>();
}
connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer);
// get our response back
var receiveDataPending = rconSocket.ReceiveFromAsync(connectionState.ReceiveEventArgs);
if (receiveDataPending)
{
_log.LogDebug("Waiting to asynchronously receive data on attempt #{ConnectionAttempts}",
connectionState.ConnectionAttempts);
var completed = false;
try
{
completed = await connectionState.OnReceivedData.WaitAsync(
new[]
{
StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts),
overrideTimeout
}.Max(), token);
}
catch (OperationCanceledException)
{
// ignored
}
if (!completed)
{
if (connectionState.ConnectionAttempts > 1) // this reduces some spam for unstable connections
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning(
"Socket timed out while waiting for RCon response on attempt {Attempt} with timeout delay of {Timeout}",
connectionState.ConnectionAttempts,
StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts));
}
}
rconSocket.Close();
_log.LogDebug("OnDataReceived did not complete in allocated time");
throw new NetworkException("Timed out receiving RCon response", rconSocket);
}
}
rconSocket.Close();
return GetResponseData(connectionState);
}
private static byte[][] GetResponseData(ConnectionState connectionState)
{
var responseList = new List<byte[]>();
var totalBytesRead = 0;
foreach (var bytesRead in connectionState.BytesReadPerSegment)
{
responseList.Add(connectionState.ReceiveBuffer
.Skip(totalBytesRead)
.Take(bytesRead)
.ToArray());
totalBytesRead += bytesRead;
}
return responseList.ToArray();
}
private void OnDataReceived(object sender, SocketAsyncEventArgs e)
{
_log.LogDebug("Read {BytesTransferred} bytes from {Endpoint}", e.BytesTransferred,
e.RemoteEndPoint?.ToString());
// this occurs when we close the socket
if (e.BytesTransferred == 0)
{
_log.LogDebug("No bytes were transmitted so the connection was probably closed");
var semaphore = ActiveQueries[Endpoint].OnReceivedData;
try
{
if (semaphore.CurrentCount == 0)
{
semaphore.Release();
}
}
catch
{
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
// this thread is not notified because it's an event
}
return;
}
var state = ActiveQueries[Endpoint];
var cancellationRequested = ((ConnectionUserToken)e.UserToken)?.CancellationToken.IsCancellationRequested ??
false;
if (sender is not Socket sock || cancellationRequested)
{
var semaphore = ActiveQueries[Endpoint].OnReceivedData;
try
{
if (semaphore.CurrentCount == 0)
{
semaphore.Release();
}
}
catch
{
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
// this thread is not notified because it's an event
}
return;
}
state.BytesReadPerSegment.Add(e.BytesTransferred);
// I don't even want to know why this works for getting more data from Cod4x
// but I'm leaving it in here as long as it doesn't break anything.
// it's very stupid...
Thread.Sleep(150);
try
{
var totalBytesTransferred = e.BytesTransferred;
_log.LogDebug("{Total} total bytes transferred with {Available} bytes remaining", totalBytesTransferred,
sock.Available);
// we still have available data so the payload was segmented
while (sock.Available > 0)
{
_log.LogDebug("{Available} more bytes to be read", sock.Available);
var bufferSpaceAvailable = sock.Available + totalBytesTransferred - state.ReceiveBuffer.Length;
if (bufferSpaceAvailable >= 0)
{
_log.LogWarning(
"Not enough buffer space to store incoming data {BytesNeeded} additional bytes required",
bufferSpaceAvailable);
continue;
}
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, totalBytesTransferred, sock.Available);
if (sock.ReceiveAsync(state.ReceiveEventArgs))
{
_log.LogDebug("Remaining bytes are async");
continue;
}
_log.LogDebug("Read {BytesTransferred} synchronous bytes from {Endpoint}",
state.ReceiveEventArgs.BytesTransferred, e.RemoteEndPoint?.ToString());
// we need to increment this here because the callback isn't executed if there's no pending IO
state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred);
totalBytesTransferred += state.ReceiveEventArgs.BytesTransferred;
}
}
catch (ObjectDisposedException)
{
_log.LogDebug("Socket was disposed while receiving data");
}
finally
{
var semaphore = ActiveQueries[Endpoint].OnReceivedData;
try
{
if (semaphore.CurrentCount == 0)
{
semaphore.Release();
}
}
catch
{
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
// this thread is not notified because it's an event
}
}
}
private void OnDataSent(object sender, SocketAsyncEventArgs e)
{
_log.LogDebug("Sent {ByteCount} bytes to {Endpoint}", e.Buffer?.Length,
e.ConnectSocket?.RemoteEndPoint?.ToString());
var semaphore = ActiveQueries[Endpoint].OnSentData;
try
{
if (semaphore.CurrentCount == 0)
{
semaphore.Release();
}
}
catch
{
// ignored because we can have the socket operation cancelled (which releases the semaphore) but
// this thread is not notified because it's an event
}
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;
namespace Integrations.Cod
@ -12,13 +13,26 @@ namespace Integrations.Cod
~ConnectionState()
{
OnComplete.Dispose();
OnSentData.Dispose();
OnReceivedData.Dispose();
}
public int ConnectionAttempts { get; set; }
private const int BufferSize = 16384;
public readonly byte[] ReceiveBuffer = new byte[BufferSize];
public readonly SemaphoreSlim OnComplete = new(1, 1);
public List<byte[]> ReceivedBytes { get; } = new();
public readonly SemaphoreSlim OnSentData = new(1, 1);
public readonly SemaphoreSlim OnReceivedData = new (1, 1);
public List<int> BytesReadPerSegment { get; set; } = new();
public SocketAsyncEventArgs SendEventArgs { get; set; } = new();
public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new();
public DateTime LastQuery { get; set; } = DateTime.Now;
}
internal class ConnectionUserToken
{
public Socket Socket { get; set; }
public CancellationToken CancellationToken { get; set; }
}
}

View File

@ -16,8 +16,4 @@
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="protobuf-net" Version="3.2.26" />
</ItemGroup>
</Project>

View File

@ -1,57 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using ProtoBuf;
namespace Integrations.Cod.SecureRcon;
public static class Helpers
{
private static byte[] ToSerializedMessage(this SecureCommand command)
{
using var ms = new MemoryStream();
Serializer.Serialize(ms, command);
return ms.ToArray();
}
private static byte[] SignData(byte[] data, string privateKey)
{
using var rsa = new RSACryptoServiceProvider();
rsa.ImportFromPem(privateKey);
var rsaFormatter = new RSAPKCS1SignatureFormatter(rsa);
rsaFormatter.SetHashAlgorithm("SHA512");
var hash = SHA512.Create();
var hashedData = hash.ComputeHash(data);
var signature = rsaFormatter.CreateSignature(hashedData);
return signature;
}
public static byte SafeConversion(char c)
{
try
{
return Convert.ToByte(c);
}
catch
{
return (byte)'.';
}
}
public static byte[] BuildSafeRconPayload(string prefix, string command, string signingKey)
{
var message = command.Select(SafeConversion).ToArray();
var header = (prefix + "\n").Select(SafeConversion).ToArray();
var secureCommand = new SecureCommand
{
SecMessage = message,
Signature = SignData(message, signingKey)
};
return header.Concat(secureCommand.ToSerializedMessage()).ToArray();
}
}

View File

@ -1,13 +0,0 @@
using ProtoBuf;
namespace Integrations.Cod.SecureRcon;
[ProtoContract]
public class SecureCommand
{
[ProtoMember(1)]
public byte[] SecMessage { get; set; }
[ProtoMember(2)]
public byte[] Signature { get; set; }
}

View File

@ -104,7 +104,7 @@ namespace Integrations.Source
}
var split = response.TrimEnd('\n').Split('\n');
return split.Take(Math.Max(1, split.Length - 1)).ToArray();
return split.Take(split.Length - 1).ToArray();
}
catch (TaskCanceledException)

View File

@ -1,17 +0,0 @@
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Plugins.AutoMessageFeed;
public class AutoMessageFeedConfiguration : IBaseConfiguration
{
public bool EnableFeed { get; set; }
public string FeedUrl { get; set; }
public int MaxFeedItems { get; set; }
public IBaseConfiguration Generate()
{
return this;
}
public string Name() => "AutomessageFeedConfiguration";
}

View File

@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2023.4.5.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -0,0 +1,18 @@
using SharedLibraryCore.Interfaces;
namespace AutomessageFeed
{
class Configuration : IBaseConfiguration
{
public bool EnableFeed { get; set; }
public string FeedUrl { get; set; }
public int MaxFeedItems { get; set; }
public IBaseConfiguration Generate()
{
return this;
}
public string Name() => "AutomessageFeedConfiguration";
}
}

View File

@ -1,71 +1,54 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Threading.Tasks;
using Microsoft.SyndicationFeed.Rss;
using SharedLibraryCore.Configuration;
using System.Xml;
using Microsoft.SyndicationFeed;
using System.Collections.Generic;
using SharedLibraryCore.Helpers;
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore.Interfaces.Events;
namespace IW4MAdmin.Plugins.AutoMessageFeed;
public class Plugin : IPluginV2
namespace AutomessageFeed
{
public class Plugin : IPlugin
{
public string Name => "Automessage Feed";
public string Version => Utilities.GetVersionAsString();
public float Version => (float)Utilities.GetVersionAsDouble();
public string Author => "RaidMax";
private int _currentFeedItem;
private readonly AutoMessageFeedConfiguration _configuration;
private readonly IConfigurationHandler<Configuration> _configurationHandler;
public static void RegisterDependencies(IServiceCollection serviceCollection)
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory)
{
serviceCollection.AddConfiguration<AutoMessageFeedConfiguration>("AutomessageFeedPluginSettings");
}
public Plugin(AutoMessageFeedConfiguration configuration)
{
_configuration = configuration;
if (configuration.EnableFeed)
{
IManagementEventSubscriptions.Load += (manager, _) =>
{
manager.GetMessageTokens().Add(new MessageToken("FEED", GetNextFeedItem));
return Task.CompletedTask;
};
}
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<Configuration>("AutomessageFeedPluginSettings");
}
private async Task<string> GetNextFeedItem(Server server)
{
if (!_configuration.EnableFeed)
{
return null;
}
var items = new List<string>();
using (var reader = XmlReader.Create(_configuration.FeedUrl, new XmlReaderSettings { Async = true }))
using (var reader = XmlReader.Create(_configurationHandler.Configuration().FeedUrl, new XmlReaderSettings() { Async = true }))
{
var feedReader = new RssFeedReader(reader);
while (await feedReader.Read())
{
if (feedReader.ElementType != SyndicationElementType.Item)
switch (feedReader.ElementType)
{
continue;
}
case SyndicationElementType.Item:
var item = await feedReader.ReadItem();
items.Add(Regex.Replace(item.Title, @"\<.+\>.*\</.+\>", ""));
break;
}
}
}
if (_currentFeedItem < items.Count && (_configuration.MaxFeedItems == 0 || _currentFeedItem < _configuration.MaxFeedItems))
if (_currentFeedItem < items.Count && (_configurationHandler.Configuration().MaxFeedItems == 0 || _currentFeedItem < _configurationHandler.Configuration().MaxFeedItems))
{
_currentFeedItem++;
return items[_currentFeedItem - 1];
@ -74,4 +57,32 @@ public class Plugin : IPluginV2
_currentFeedItem = 0;
return Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_NO_ITEMS"];
}
public Task OnEventAsync(GameEvent E, Server S)
{
return Task.CompletedTask;
}
public async Task OnLoadAsync(IManager manager)
{
await _configurationHandler.BuildAsync();
if (_configurationHandler.Configuration() == null)
{
_configurationHandler.Set((Configuration)new Configuration().Generate());
await _configurationHandler.Save();
}
manager.GetMessageTokens().Add(new MessageToken("FEED", GetNextFeedItem));
}
public Task OnTickAsync(Server S)
{
throw new NotImplementedException();
}
public Task OnUnloadAsync()
{
return Task.CompletedTask;
}
}
}

View File

@ -1,9 +1,9 @@
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
namespace IW4MAdmin.Plugins.LiveRadar.Configuration;
public class LiveRadarConfiguration : IBaseConfiguration
namespace LiveRadar.Configuration
{
class LiveRadarConfiguration : IBaseConfiguration
{
public List<MapInfo> Maps { get; set; }
@ -388,3 +388,4 @@ public class LiveRadarConfiguration : IBaseConfiguration
public string Name() => "LiveRadar";
}
}

Some files were not shown because too many files have changed in this diff Show More