Compare commits
146 Commits
2023.01.24
...
release/pr
Author | SHA1 | Date | |
---|---|---|---|
95eb73da6e | |||
6ec0a24ca2 | |||
|
03b5b8b143 | ||
|
005a8b050d | ||
|
2c99f7b48e | ||
|
13d4ec3033 | ||
|
e6cdae5a6b | ||
|
d69a9ecf56 | ||
|
b6c32181b0 | ||
|
2017eebeba | ||
|
3192fe35e6 | ||
|
c1dace4af6 | ||
|
5c6ae3146a | ||
|
2e99db2275 | ||
|
79eec08590 | ||
|
69691f75f4 | ||
|
648eec25f2 | ||
|
80774853b6 | ||
|
08edbf9bd4 | ||
|
e472468c02 | ||
|
d4e266ed94 | ||
|
4e02e7841f | ||
|
dc707f75b3 | ||
|
41efe26a48 | ||
|
4740479ace | ||
|
6f28bc5b0b | ||
|
47ed505fae | ||
|
e2c07daece | ||
|
28fd712a63 | ||
|
3f11a4fe9f | ||
|
bcb063730c | ||
|
789981346a | ||
|
f79ba6466c | ||
|
871f8d75df | ||
|
ad89ecb39d | ||
|
2340e30c2d | ||
|
e7f5e6a841 | ||
|
50593f5a93 | ||
|
5a22a759a8 | ||
|
eb8ea5e222 | ||
|
3f0bdfe3a9 | ||
|
2fcbab9a37 | ||
|
e843f839f5 | ||
|
e4535e09a0 | ||
|
b4f93602ef | ||
|
bc34211e43 | ||
|
7323c6e3d7 | ||
|
ebdad2768d | ||
|
58e8d54373 | ||
|
3f71bc96f4 | ||
|
84ed9c8d8f | ||
|
81e2a2f6d4 | ||
|
088f7a51be | ||
|
7d436ac0c5 | ||
|
c26489d71f | ||
|
7f4eb230be | ||
|
003945c241 | ||
|
ba911f26ec | ||
|
d6d2717771 | ||
|
35f9eb5933 | ||
|
4233aab1ee | ||
|
cdf9485903 | ||
|
108dddb5cc | ||
|
399e082b61 | ||
|
35c4bbd2d5 | ||
|
cae77357ca | ||
|
f186e3ae4d | ||
|
1e88f5bac0 | ||
|
ce054c173e | ||
|
740df7c3ee | ||
|
466ae96874 | ||
|
6ae15261c9 | ||
|
72df5c9902 | ||
|
994dbe142e | ||
|
ed3f9f750f | ||
|
9b56ff520f | ||
|
123d84088f | ||
|
ddfcf6e138 | ||
|
92992dfb13 | ||
|
c53e0de7d0 | ||
|
29d0686f73 | ||
|
caddc06c70 | ||
|
75b93bb972 | ||
|
b022b08bc7 | ||
|
bb8f3fbe5b | ||
|
c3be7f7de5 | ||
|
520a76a15e | ||
|
e8ab56cd9b | ||
|
5490d6b358 | ||
|
5d53c2559b | ||
|
22af762a9d | ||
|
c550d424dd | ||
|
f4ded4cc1f | ||
|
d8c0cd47f5 | ||
|
1f77d10eed | ||
|
222f2ba5f8 | ||
|
8c48151ab6 | ||
|
c5a283a02e | ||
|
d0911b7b8a | ||
|
388434133b | ||
|
6bb97c7d83 | ||
|
c348283c94 | ||
|
a434420951 | ||
|
19bbdede45 | ||
|
129e70c82c | ||
|
c6c7ca6305 | ||
|
12ddb87fc2 | ||
|
bc0ec6c050 | ||
|
99e0990770 | ||
|
af2925287d | ||
|
ffb32ccc45 | ||
|
e558d912cf | ||
|
2e6a1efb47 | ||
|
4442826bcf | ||
|
6db1f6db07 | ||
|
d9d5a56ab0 | ||
|
f41ce39180 | ||
|
2e726ea9ed | ||
|
6fa172d757 | ||
|
da54c5d327 | ||
|
fb82cbe6f2 | ||
|
5f5c0f1cfb | ||
|
5f5fb8230e | ||
|
51fae05a73 | ||
|
c14042a109 | ||
|
fab3cf95d6 | ||
|
ad20572879 | ||
|
3364473ce2 | ||
|
710382d432 | ||
|
b258d51863 | ||
|
782201b086 | ||
|
676589a3e0 | ||
|
6c9ac1f7bb | ||
|
e8bdde70fb | ||
|
dab429776d | ||
|
5e32536821 | ||
|
59e3813fa7 | ||
|
66c0561e7f | ||
|
7b8f6421aa | ||
|
4ba56b53a4 | ||
|
a50e61318c | ||
|
83207b4b40 | ||
|
ba9e393363 | ||
|
2688790736 | ||
|
8fc47ec6c4 | ||
|
12e3fd9238 |
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using IW4MAdmin.Application.Plugin;
|
||||
using Newtonsoft.Json;
|
||||
using RestEase;
|
||||
using SharedLibraryCore.Helpers;
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharedLibraryCore.Alerts;
|
||||
using SharedLibraryCore.Configuration;
|
||||
@ -15,6 +16,7 @@ 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)
|
||||
{
|
||||
@ -38,8 +40,9 @@ public class AlertManager : IAlertManager
|
||||
|
||||
public IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client)
|
||||
{
|
||||
lock (_states)
|
||||
try
|
||||
{
|
||||
_onModifyingAlerts.Wait();
|
||||
var alerts = Enumerable.Empty<Alert.AlertState>();
|
||||
if (client.Level > Data.Models.Client.EFClient.Permission.Trusted)
|
||||
{
|
||||
@ -52,14 +55,22 @@ public class AlertManager : IAlertManager
|
||||
alerts = alerts.Concat(_states[client.ClientId].AsReadOnly());
|
||||
}
|
||||
|
||||
return alerts.OrderByDescending(alert => alert.OccuredAt);
|
||||
return alerts.OrderByDescending(alert => alert.OccuredAt).ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onModifyingAlerts.CurrentCount == 0)
|
||||
{
|
||||
_onModifyingAlerts.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkAlertAsRead(Guid alertId)
|
||||
{
|
||||
lock (_states)
|
||||
try
|
||||
{
|
||||
_onModifyingAlerts.Wait();
|
||||
foreach (var items in _states.Values)
|
||||
{
|
||||
var matchingEvent = items.FirstOrDefault(item => item.AlertId == alertId);
|
||||
@ -73,12 +84,20 @@ public class AlertManager : IAlertManager
|
||||
OnAlertConsumed?.Invoke(this, matchingEvent);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onModifyingAlerts.CurrentCount == 0)
|
||||
{
|
||||
_onModifyingAlerts.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkAllAlertsAsRead(int recipientId)
|
||||
{
|
||||
lock (_states)
|
||||
try
|
||||
{
|
||||
_onModifyingAlerts.Wait();
|
||||
foreach (var items in _states.Values)
|
||||
{
|
||||
items.RemoveAll(item =>
|
||||
@ -93,12 +112,20 @@ public class AlertManager : IAlertManager
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onModifyingAlerts.CurrentCount == 0)
|
||||
{
|
||||
_onModifyingAlerts.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddAlert(Alert.AlertState alert)
|
||||
{
|
||||
lock (_states)
|
||||
try
|
||||
{
|
||||
_onModifyingAlerts.Wait();
|
||||
if (alert.RecipientId is null)
|
||||
{
|
||||
_states[0].Add(alert);
|
||||
@ -119,6 +146,14 @@ public class AlertManager : IAlertManager
|
||||
|
||||
PruneOldAlerts();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onModifyingAlerts.CurrentCount == 0)
|
||||
{
|
||||
_onModifyingAlerts.Release(1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource)
|
||||
|
@ -21,11 +21,10 @@
|
||||
<Win32Resource />
|
||||
<RootNamespace>IW4MAdmin.Application</RootNamespace>
|
||||
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jint" Version="3.0.0-beta-2042" />
|
||||
<PackageReference Include="Jint" Version="3.0.0-beta-2049" />
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@ -33,11 +32,13 @@
|
||||
</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>false</ServerGarbageCollection>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||
<TieredCompilation>true</TieredCompilation>
|
||||
<LangVersion>Latest</LangVersion>
|
||||
|
@ -16,6 +16,7 @@ using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
@ -23,11 +24,18 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Context;
|
||||
using Data.Models;
|
||||
using IW4MAdmin.Application.Configuration;
|
||||
using IW4MAdmin.Application.Migration;
|
||||
using IW4MAdmin.Application.Plugin.Script;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Context;
|
||||
using SharedLibraryCore.Events;
|
||||
using SharedLibraryCore.Events.Management;
|
||||
using SharedLibraryCore.Events.Server;
|
||||
using SharedLibraryCore.Formatting;
|
||||
using SharedLibraryCore.Interfaces.Events;
|
||||
using static SharedLibraryCore.GameEvent;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
|
||||
@ -49,7 +57,7 @@ namespace IW4MAdmin.Application
|
||||
public IList<Func<GameEvent, bool>> CommandInterceptors { get; set; } =
|
||||
new List<Func<GameEvent, bool>>();
|
||||
public ITokenAuthentication TokenAuthenticator { get; }
|
||||
public CancellationToken CancellationToken => _tokenSource.Token;
|
||||
public CancellationToken CancellationToken => _isRunningTokenSource.Token;
|
||||
public string ExternalIPAddress { get; private set; }
|
||||
public bool IsRestartRequested { get; private set; }
|
||||
public IMiddlewareActionHandler MiddlewareActionHandler { get; }
|
||||
@ -63,29 +71,30 @@ namespace IW4MAdmin.Application
|
||||
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
||||
readonly IPageList PageList;
|
||||
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
|
||||
private CancellationTokenSource _tokenSource;
|
||||
private CancellationTokenSource _isRunningTokenSource;
|
||||
private CancellationTokenSource _eventHandlerTokenSource;
|
||||
private readonly Dictionary<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 IEventHandler _eventHandler;
|
||||
private readonly ICoreEventHandler _coreEventHandler;
|
||||
private readonly IScriptCommandFactory _scriptCommandFactory;
|
||||
private readonly IMetaRegistration _metaRegistration;
|
||||
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ChangeHistoryService _changeHistoryService;
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new ConcurrentDictionary<long, GameEvent>();
|
||||
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new();
|
||||
|
||||
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,
|
||||
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
|
||||
ICoreEventHandler coreEventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
|
||||
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
|
||||
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration)
|
||||
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager, IInteractionRegistration interactionRegistration, IEnumerable<IPluginV2> v2PLugins)
|
||||
{
|
||||
MiddlewareActionHandler = actionHandler;
|
||||
_servers = new ConcurrentBag<Server>();
|
||||
@ -100,14 +109,14 @@ namespace IW4MAdmin.Application
|
||||
AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
|
||||
TokenAuthenticator = new TokenAuthentication();
|
||||
_logger = logger;
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
_isRunningTokenSource = new CancellationTokenSource();
|
||||
_commands = commands.ToList();
|
||||
_translationLookup = translationLookup;
|
||||
_commandConfiguration = commandConfiguration;
|
||||
_serverInstanceFactory = serverInstanceFactory;
|
||||
_parserRegexFactory = parserRegexFactory;
|
||||
_customParserEvents = customParserEvents;
|
||||
_eventHandler = eventHandler;
|
||||
_coreEventHandler = coreEventHandler;
|
||||
_scriptCommandFactory = scriptCommandFactory;
|
||||
_metaRegistration = metaRegistration;
|
||||
_scriptPluginServiceResolver = scriptPluginServiceResolver;
|
||||
@ -116,6 +125,8 @@ namespace IW4MAdmin.Application
|
||||
_appConfig = appConfig;
|
||||
Plugins = plugins;
|
||||
InteractionRegistration = interactionRegistration;
|
||||
|
||||
IManagementEventSubscriptions.ClientPersistentIdReceived += OnClientPersistentIdReceived;
|
||||
}
|
||||
|
||||
public IEnumerable<IPlugin> Plugins { get; }
|
||||
@ -123,7 +134,7 @@ namespace IW4MAdmin.Application
|
||||
|
||||
public async Task ExecuteEvent(GameEvent newEvent)
|
||||
{
|
||||
ProcessingEvents.TryAdd(newEvent.Id, newEvent);
|
||||
ProcessingEvents.TryAdd(newEvent.IncrementalId, newEvent);
|
||||
|
||||
// the event has failed already
|
||||
if (newEvent.Failed)
|
||||
@ -141,12 +152,12 @@ namespace IW4MAdmin.Application
|
||||
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
_logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
|
||||
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
|
||||
}
|
||||
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
|
||||
_logger.LogDebug("Received quit signal for event id {EventId}, so we are aborting early", newEvent.IncrementalId);
|
||||
}
|
||||
|
||||
// this happens if a plugin requires login
|
||||
@ -185,11 +196,11 @@ namespace IW4MAdmin.Application
|
||||
}
|
||||
|
||||
skip:
|
||||
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null)
|
||||
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null && newEvent.CorrelationId is not null)
|
||||
{
|
||||
var correlatedEvents =
|
||||
ProcessingEvents.Values.Where(ev =>
|
||||
ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id)
|
||||
ev.CorrelationId == newEvent.CorrelationId && ev.IncrementalId != newEvent.IncrementalId)
|
||||
.ToList();
|
||||
|
||||
await Task.WhenAll(correlatedEvents.Select(ev =>
|
||||
@ -198,14 +209,16 @@ namespace IW4MAdmin.Application
|
||||
|
||||
foreach (var correlatedEvent in correlatedEvents)
|
||||
{
|
||||
ProcessingEvents.Remove(correlatedEvent.Id, out _);
|
||||
ProcessingEvents.Remove(correlatedEvent.IncrementalId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
// we don't want to remove events that are correlated to command
|
||||
if (ProcessingEvents.Values.ToList()?.Count(gameEvent => gameEvent.CorrelationId == newEvent.CorrelationId) == 1)
|
||||
if (ProcessingEvents.Values.Count(gameEvent =>
|
||||
newEvent.CorrelationId is not null && newEvent.CorrelationId == gameEvent.CorrelationId) == 1 ||
|
||||
newEvent.CorrelationId is null)
|
||||
{
|
||||
ProcessingEvents.Remove(newEvent.Id, out _);
|
||||
ProcessingEvents.Remove(newEvent.IncrementalId, out _);
|
||||
}
|
||||
|
||||
// tell anyone waiting for the output that we're done
|
||||
@ -225,75 +238,58 @@ namespace IW4MAdmin.Application
|
||||
|
||||
public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList();
|
||||
|
||||
public async Task UpdateServerStates()
|
||||
private Task UpdateServerStates()
|
||||
{
|
||||
// store the server hash code and task for it
|
||||
var runningUpdateTasks = new Dictionary<long, (Task task, CancellationTokenSource tokenSource, DateTime startTime)>();
|
||||
var timeout = TimeSpan.FromSeconds(60);
|
||||
|
||||
while (!_tokenSource.IsCancellationRequested) // main shutdown requested
|
||||
var index = 0;
|
||||
return Task.WhenAll(_servers.Select(server =>
|
||||
{
|
||||
// select the server ids that have completed the update task
|
||||
var serverTasksToRemove = runningUpdateTasks
|
||||
.Where(ut => ut.Value.task.IsCompleted)
|
||||
.Select(ut => ut.Key)
|
||||
.ToList();
|
||||
|
||||
// remove the update tasks as they have completed
|
||||
foreach (var serverId in serverTasksToRemove.Where(serverId => runningUpdateTasks.ContainsKey(serverId)))
|
||||
{
|
||||
if (!runningUpdateTasks[serverId].tokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
runningUpdateTasks[serverId].tokenSource.Cancel();
|
||||
}
|
||||
|
||||
runningUpdateTasks.Remove(serverId);
|
||||
}
|
||||
|
||||
// select the servers where the tasks have completed
|
||||
var newTaskServers = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
|
||||
|
||||
foreach (var server in Servers.Where(s => newTaskServers.Contains(s.EndPoint)))
|
||||
{
|
||||
var firstTokenSource = new CancellationTokenSource();
|
||||
firstTokenSource.CancelAfter(timeout);
|
||||
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(firstTokenSource.Token, _tokenSource.Token);
|
||||
runningUpdateTasks.Add(server.EndPoint, (ProcessUpdateHandler(server, linkedTokenSource.Token), linkedTokenSource, DateTime.Now));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
|
||||
}
|
||||
// if a cancellation is received, we want to return immediately after shutting down
|
||||
catch
|
||||
{
|
||||
foreach (var server in Servers.Where(s => newTaskServers.Contains(s.EndPoint)))
|
||||
{
|
||||
await server.ProcessUpdatesAsync(_tokenSource.Token);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
var thisIndex = index;
|
||||
Interlocked.Increment(ref index);
|
||||
return ProcessUpdateHandler(server, thisIndex);
|
||||
}));
|
||||
}
|
||||
|
||||
private async Task ProcessUpdateHandler(Server server, CancellationToken token)
|
||||
private async Task ProcessUpdateHandler(Server server, int index)
|
||||
{
|
||||
try
|
||||
const int delayScalar = 50; // Task.Delay is inconsistent enough there's no reason to try to prevent collisions
|
||||
var timeout = TimeSpan.FromMinutes(2);
|
||||
|
||||
while (!_isRunningTokenSource.IsCancellationRequested)
|
||||
{
|
||||
await server.ProcessUpdatesAsync(token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
using (LogContext.PushProperty("Server", server.ToString()))
|
||||
try
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update status");
|
||||
var delayFactor = Math.Min(_appConfig.RConPollRate, delayScalar * index);
|
||||
await Task.Delay(delayFactor, _isRunningTokenSource.Token);
|
||||
|
||||
using var timeoutTokenSource = new CancellationTokenSource();
|
||||
timeoutTokenSource.CancelAfter(timeout);
|
||||
using var linkedTokenSource =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token,
|
||||
_isRunningTokenSource.Token);
|
||||
await server.ProcessUpdatesAsync(linkedTokenSource.Token);
|
||||
|
||||
await Task.Delay(Math.Max(1000, _appConfig.RConPollRate - delayFactor),
|
||||
_isRunningTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
using (LogContext.PushProperty("Server", server.Id))
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update status");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.IsInitialized = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.IsInitialized = true;
|
||||
}
|
||||
|
||||
// run the final updates to clean up server
|
||||
await server.ProcessUpdatesAsync(_isRunningTokenSource.Token);
|
||||
}
|
||||
|
||||
public async Task Init()
|
||||
@ -304,25 +300,34 @@ namespace IW4MAdmin.Application
|
||||
#region DATABASE
|
||||
_logger.LogInformation("Beginning database migration sync");
|
||||
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]);
|
||||
await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
|
||||
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
|
||||
await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _isRunningTokenSource.Token);
|
||||
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _isRunningTokenSource.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)
|
||||
if (plugin is ScriptPlugin scriptPlugin && !plugin.IsParser)
|
||||
{
|
||||
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
|
||||
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
|
||||
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
|
||||
scriptPlugin.Watcher.Changed += async (sender, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
|
||||
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
|
||||
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
@ -388,13 +393,11 @@ namespace IW4MAdmin.Application
|
||||
if (string.IsNullOrEmpty(_appConfig.Id))
|
||||
{
|
||||
_appConfig.Id = Guid.NewGuid().ToString();
|
||||
await ConfigHandler.Save();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl))
|
||||
{
|
||||
_appConfig.WebfrontBindUrl = "http://0.0.0.0:1624";
|
||||
await ConfigHandler.Save();
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
@ -439,8 +442,8 @@ namespace IW4MAdmin.Application
|
||||
|
||||
serverConfig.ModifyParsers();
|
||||
}
|
||||
await ConfigHandler.Save();
|
||||
}
|
||||
await ConfigHandler.Save();
|
||||
}
|
||||
|
||||
if (_appConfig.Servers.Length == 0)
|
||||
@ -465,7 +468,7 @@ namespace IW4MAdmin.Application
|
||||
#endregion
|
||||
|
||||
#region COMMANDS
|
||||
if (await ClientSvc.HasOwnerAsync(_tokenSource.Token))
|
||||
if (await ClientSvc.HasOwnerAsync(_isRunningTokenSource.Token))
|
||||
{
|
||||
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
|
||||
}
|
||||
@ -523,7 +526,7 @@ namespace IW4MAdmin.Application
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
|
||||
await InitializeServers();
|
||||
IsInitialized = true;
|
||||
@ -540,26 +543,23 @@ namespace IW4MAdmin.Application
|
||||
try
|
||||
{
|
||||
// todo: this might not always be an IW4MServer
|
||||
var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
|
||||
using (LogContext.PushProperty("Server", ServerInstance.ToString()))
|
||||
var serverInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
|
||||
using (LogContext.PushProperty("Server", serverInstance!.ToString()))
|
||||
{
|
||||
_logger.LogInformation("Beginning server communication initialization");
|
||||
await ServerInstance.Initialize();
|
||||
await serverInstance.Initialize();
|
||||
|
||||
_servers.Add(ServerInstance);
|
||||
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname.StripColors()));
|
||||
_logger.LogInformation("Finishing initialization and now monitoring [{Server}]", ServerInstance.Hostname);
|
||||
_servers.Add(serverInstance);
|
||||
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(serverInstance.Hostname.StripColors()));
|
||||
_logger.LogInformation("Finishing initialization and now monitoring [{Server}]", serverInstance.Hostname);
|
||||
}
|
||||
|
||||
// add the start event for this server
|
||||
var e = new GameEvent()
|
||||
QueueEvent(new MonitorStartEvent
|
||||
{
|
||||
Type = EventType.Start,
|
||||
Data = $"{ServerInstance.GameName} started",
|
||||
Owner = ServerInstance
|
||||
};
|
||||
|
||||
AddEvent(e);
|
||||
Server = serverInstance,
|
||||
Source = this
|
||||
});
|
||||
|
||||
successServers++;
|
||||
}
|
||||
|
||||
@ -581,20 +581,36 @@ namespace IW4MAdmin.Application
|
||||
throw lastException;
|
||||
}
|
||||
|
||||
if (successServers != config.Servers.Length)
|
||||
if (successServers != config.Servers.Length && !AppContext.TryGetSwitch("NoConfirmPrompt", out _))
|
||||
{
|
||||
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
|
||||
if (!Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"].PromptBool())
|
||||
{
|
||||
throw lastException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Start() => await UpdateServerStates();
|
||||
public async Task Start()
|
||||
{
|
||||
_eventHandlerTokenSource = new CancellationTokenSource();
|
||||
|
||||
var eventHandlerThread = new Thread(() =>
|
||||
{
|
||||
_coreEventHandler.StartProcessing(_eventHandlerTokenSource.Token);
|
||||
})
|
||||
{
|
||||
Name = nameof(CoreEventHandler)
|
||||
};
|
||||
|
||||
eventHandlerThread.Start();
|
||||
await UpdateServerStates();
|
||||
_eventHandlerTokenSource.Cancel();
|
||||
eventHandlerThread.Join();
|
||||
}
|
||||
|
||||
public async Task Stop()
|
||||
{
|
||||
foreach (var plugin in Plugins)
|
||||
foreach (var plugin in Plugins.Where(plugin => !plugin.IsParser))
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -604,19 +620,32 @@ namespace IW4MAdmin.Application
|
||||
{
|
||||
_logger.LogError(ex, "Could not cleanly unload plugin {PluginName}", plugin.Name);
|
||||
}
|
||||
}
|
||||
|
||||
_tokenSource.Cancel();
|
||||
|
||||
}
|
||||
|
||||
_isRunningTokenSource.Cancel();
|
||||
|
||||
IsRunning = false;
|
||||
}
|
||||
|
||||
public void Restart()
|
||||
public async Task Restart()
|
||||
{
|
||||
IsRestartRequested = true;
|
||||
Stop().GetAwaiter().GetResult();
|
||||
_tokenSource.Dispose();
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
await Stop();
|
||||
|
||||
using var subscriptionTimeoutToken = new CancellationTokenSource();
|
||||
subscriptionTimeoutToken.CancelAfter(Utilities.DefaultCommandTimeout);
|
||||
|
||||
await IManagementEventSubscriptions.InvokeUnloadAsync(this, subscriptionTimeoutToken.Token);
|
||||
|
||||
IGameEventSubscriptions.ClearEventInvocations();
|
||||
IGameServerEventSubscriptions.ClearEventInvocations();
|
||||
IManagementEventSubscriptions.ClearEventInvocations();
|
||||
|
||||
_isRunningTokenSource.Dispose();
|
||||
_isRunningTokenSource = new CancellationTokenSource();
|
||||
|
||||
_eventHandlerTokenSource.Dispose();
|
||||
_eventHandlerTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
@ -658,9 +687,14 @@ namespace IW4MAdmin.Application
|
||||
|
||||
public void AddEvent(GameEvent gameEvent)
|
||||
{
|
||||
_eventHandler.HandleEvent(this, gameEvent);
|
||||
_coreEventHandler.QueueEvent(this, gameEvent);
|
||||
}
|
||||
|
||||
public void QueueEvent(CoreEvent coreEvent)
|
||||
{
|
||||
_coreEventHandler.QueueEvent(this, coreEvent);
|
||||
}
|
||||
|
||||
public IPageList GetPageList()
|
||||
{
|
||||
return PageList;
|
||||
@ -695,15 +729,166 @@ namespace IW4MAdmin.Application
|
||||
|
||||
public void AddAdditionalCommand(IManagerCommand command)
|
||||
{
|
||||
if (_commands.Any(_command => _command.Name == command.Name || _command.Alias == command.Alias))
|
||||
lock (_commands)
|
||||
{
|
||||
throw new InvalidOperationException($"Duplicate command name or alias ({command.Name}, {command.Alias})");
|
||||
}
|
||||
if (_commands.Any(cmd => cmd.Name == command.Name || cmd.Alias == command.Alias))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Duplicate command name or alias ({command.Name}, {command.Alias})");
|
||||
}
|
||||
|
||||
_commands.Add(command);
|
||||
_commands.Add(command);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
|
||||
public IAlertManager AlertManager => _alertManager;
|
||||
|
||||
private async Task OnServerValueRequested(ServerValueRequestEvent requestEvent, CancellationToken token)
|
||||
{
|
||||
if (requestEvent.Server is not IW4MServer server)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dvar<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@ -33,7 +34,7 @@ namespace IW4MAdmin.Application.Commands
|
||||
};
|
||||
}
|
||||
|
||||
public override Task ExecuteAsync(GameEvent gameEvent)
|
||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||
{
|
||||
var searchTerm = gameEvent.Data.Trim();
|
||||
var availableCommands = gameEvent.Owner.Manager.Commands.Distinct().Where(command =>
|
||||
@ -70,24 +71,25 @@ 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;
|
||||
}
|
||||
|
||||
gameEvent.Origin.Tell(helpResponse.ToString());
|
||||
messageList.Add(helpResponse.ToString());
|
||||
helpResponse = new StringBuilder();
|
||||
}
|
||||
|
||||
gameEvent.Origin.Tell(helpResponse.ToString());
|
||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_HELP_MOREINFO"]);
|
||||
}
|
||||
messageList.Add(helpResponse.ToString());
|
||||
|
||||
return Task.CompletedTask;
|
||||
await gameEvent.Origin.TellAsync(messageList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,20 +49,13 @@ namespace IW4MAdmin.Application.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
var index = 1;
|
||||
foreach (var inboxItem in inboxItems)
|
||||
await gameEvent.Origin.TellAsync(inboxItems.Select((inboxItem, index) =>
|
||||
{
|
||||
await gameEvent.Origin.Tell(_translationLookup["COMMANDS_READ_MESSAGE_SUCCESS"]
|
||||
.FormatExt($"{index}/{inboxItems.Count}", inboxItem.SourceClient.CurrentAlias.Name))
|
||||
.WaitAsync();
|
||||
var header = _translationLookup["COMMANDS_READ_MESSAGE_SUCCESS"]
|
||||
.FormatExt($"{index + 1}/{inboxItems.Count}", inboxItem.SourceClient.CurrentAlias.Name);
|
||||
|
||||
foreach (var messageFragment in inboxItem.Message.FragmentMessageForDisplay())
|
||||
{
|
||||
await gameEvent.Origin.Tell(messageFragment).WaitAsync();
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
return new[] { header }.Union(inboxItem.Message.FragmentMessageForDisplay());
|
||||
}).SelectMany(item => item));
|
||||
|
||||
inboxItems.ForEach(item => { item.IsDelivered = true; });
|
||||
|
||||
|
80
Application/Commands/SetLogLevelCommand.cs
Normal file
80
Application/Commands/SetLogLevelCommand.cs
Normal file
@ -0,0 +1,80 @@
|
||||
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()}" });
|
||||
}
|
||||
}
|
145
Application/CoreEventHandler.cs
Normal file
145
Application/CoreEventHandler.cs
Normal file
@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Events;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore.Events.Management;
|
||||
using SharedLibraryCore.Events.Server;
|
||||
using SharedLibraryCore.Interfaces.Events;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace IW4MAdmin.Application
|
||||
{
|
||||
public class CoreEventHandler : ICoreEventHandler
|
||||
{
|
||||
private const int MaxCurrentEvents = 25;
|
||||
private readonly ILogger _logger;
|
||||
private readonly SemaphoreSlim _onProcessingEvents = new(MaxCurrentEvents, MaxCurrentEvents);
|
||||
private readonly ManualResetEventSlim _onEventReady = new(false);
|
||||
private readonly ConcurrentQueue<(IManager, CoreEvent)> _runningEventTasks = new();
|
||||
private CancellationToken _cancellationToken;
|
||||
private int _activeTasks;
|
||||
|
||||
private static readonly GameEvent.EventType[] OverrideEvents =
|
||||
{
|
||||
GameEvent.EventType.Connect,
|
||||
GameEvent.EventType.Disconnect,
|
||||
GameEvent.EventType.Quit,
|
||||
GameEvent.EventType.Stop
|
||||
};
|
||||
|
||||
public CoreEventHandler(ILogger<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);
|
||||
}
|
||||
}
|
||||
}
|
@ -69,6 +69,39 @@
|
||||
}
|
||||
],
|
||||
"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": [
|
||||
@ -152,7 +185,11 @@
|
||||
{
|
||||
"Name": "twar",
|
||||
"Alias": "War"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "cmp",
|
||||
"Alias": "Zombies"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -274,6 +311,10 @@
|
||||
{
|
||||
"Name": "tdm",
|
||||
"Alias": "Team Deathmatch"
|
||||
},
|
||||
{
|
||||
"Name": "zom",
|
||||
"Alias": "Zombies"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -404,7 +445,15 @@
|
||||
{
|
||||
"Name": "tdm",
|
||||
"Alias": "Team Deathmatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "zclassic",
|
||||
"Alias": "Zombies Classic"
|
||||
},
|
||||
{
|
||||
"Name": "zstandard",
|
||||
"Alias": "Zombies"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -505,7 +554,11 @@
|
||||
{
|
||||
"Name": "hc_tdm",
|
||||
"Alias": "Hardcore Team Deathmatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "zclassic",
|
||||
"Alias": "Zombies Classic"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -795,7 +848,23 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1020,7 +1089,43 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1129,7 +1234,47 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -1576,6 +1721,70 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -7,6 +7,8 @@ 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;
|
||||
|
||||
@ -14,15 +16,19 @@ 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;
|
||||
|
||||
@ -50,7 +56,7 @@ namespace IW4MAdmin.Application.EventParsers
|
||||
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
|
||||
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
|
||||
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4);
|
||||
|
||||
|
||||
Configuration.JoinTeam.Pattern = @"^(JT);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(\w+);(.+)$";
|
||||
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.EventType, 1);
|
||||
Configuration.JoinTeam.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
|
||||
@ -58,7 +64,8 @@ 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);
|
||||
@ -73,7 +80,8 @@ 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);
|
||||
@ -95,24 +103,24 @@ namespace IW4MAdmin.Application.EventParsers
|
||||
|
||||
_regexMap = new Dictionary<ParserRegex, GameEvent.EventType>
|
||||
{
|
||||
{Configuration.Say, GameEvent.EventType.Say},
|
||||
{Configuration.Kill, GameEvent.EventType.Kill},
|
||||
{Configuration.MapChange, GameEvent.EventType.MapChange},
|
||||
{Configuration.MapEnd, GameEvent.EventType.MapEnd},
|
||||
{Configuration.JoinTeam, GameEvent.EventType.JoinTeam}
|
||||
{ Configuration.Say, GameEvent.EventType.Say },
|
||||
{ Configuration.Kill, GameEvent.EventType.Kill },
|
||||
{ Configuration.MapChange, GameEvent.EventType.MapChange },
|
||||
{ Configuration.MapEnd, GameEvent.EventType.MapEnd },
|
||||
{ Configuration.JoinTeam, GameEvent.EventType.JoinTeam }
|
||||
};
|
||||
|
||||
|
||||
_eventTypeMap = new Dictionary<string, GameEvent.EventType>
|
||||
{
|
||||
{"say", GameEvent.EventType.Say},
|
||||
{"sayteam", GameEvent.EventType.Say},
|
||||
{"chat", GameEvent.EventType.Say},
|
||||
{"chatteam", GameEvent.EventType.Say},
|
||||
{"K", GameEvent.EventType.Kill},
|
||||
{"D", GameEvent.EventType.Damage},
|
||||
{"J", GameEvent.EventType.PreConnect},
|
||||
{"JT", GameEvent.EventType.JoinTeam},
|
||||
{"Q", GameEvent.EventType.PreDisconnect}
|
||||
{ "say", GameEvent.EventType.Say },
|
||||
{ "sayteam", GameEvent.EventType.SayTeam },
|
||||
{ "chat", GameEvent.EventType.Say },
|
||||
{ "chatteam", GameEvent.EventType.SayTeam },
|
||||
{ "K", GameEvent.EventType.Kill },
|
||||
{ "D", GameEvent.EventType.Damage },
|
||||
{ "J", GameEvent.EventType.PreConnect },
|
||||
{ "JT", GameEvent.EventType.JoinTeam },
|
||||
{ "Q", GameEvent.EventType.PreDisconnect }
|
||||
};
|
||||
}
|
||||
|
||||
@ -126,28 +134,6 @@ namespace IW4MAdmin.Application.EventParsers
|
||||
|
||||
public string Name { get; set; } = "Call of Duty";
|
||||
|
||||
private (GameEvent.EventType type, string eventKey) GetEventTypeFromLine(string logLine)
|
||||
{
|
||||
var lineSplit = logLine.Split(';');
|
||||
if (lineSplit.Length > 1)
|
||||
{
|
||||
var type = lineSplit[0];
|
||||
return _eventTypeMap.ContainsKey(type) ? (_eventTypeMap[type], type): (GameEvent.EventType.Unknown, lineSplit[0]);
|
||||
}
|
||||
|
||||
foreach (var (key, value) in _regexMap)
|
||||
{
|
||||
var result = key.PatternMatcher.Match(logLine);
|
||||
if (result.Success)
|
||||
{
|
||||
return (value, null);
|
||||
}
|
||||
}
|
||||
|
||||
return (GameEvent.EventType.Unknown, null);
|
||||
}
|
||||
|
||||
|
||||
public virtual GameEvent GenerateGameEvent(string logLine)
|
||||
{
|
||||
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
|
||||
@ -155,7 +141,7 @@ namespace IW4MAdmin.Application.EventParsers
|
||||
|
||||
if (timeMatch.Success)
|
||||
{
|
||||
if (timeMatch.Values[0].Contains(":"))
|
||||
if (timeMatch.Values[0].Contains(':'))
|
||||
{
|
||||
gameTime = timeMatch
|
||||
.Values
|
||||
@ -163,299 +149,54 @@ namespace IW4MAdmin.Application.EventParsers
|
||||
// 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();
|
||||
logLine = logLine[timeMatch.Values.First().Length..].Trim();
|
||||
}
|
||||
|
||||
var eventParseResult = GetEventTypeFromLine(logLine);
|
||||
var eventType = eventParseResult.type;
|
||||
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);
|
||||
}
|
||||
|
||||
_logger.LogDebug(logLine);
|
||||
|
||||
if (eventType == GameEvent.EventType.Say)
|
||||
if (logLine.StartsWith("GSE;"))
|
||||
{
|
||||
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
|
||||
|
||||
if (matchResult.Success)
|
||||
return new GameScriptEvent
|
||||
{
|
||||
var message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
|
||||
.Replace(Configuration.LocalizeText, "")
|
||||
.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,
|
||||
ScriptData = logLine,
|
||||
GameTime = gameTime,
|
||||
Source = GameEvent.EventSource.Log
|
||||
};
|
||||
}
|
||||
|
||||
if (eventType == GameEvent.EventType.MapChange)
|
||||
if (eventKey is null || !_customEventRegistrations.ContainsKey(eventKey))
|
||||
{
|
||||
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
|
||||
};
|
||||
return GenerateDefaultEvent(logLine, gameTime);
|
||||
}
|
||||
|
||||
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];
|
||||
var eventModifier = _customEventRegistrations[eventKey];
|
||||
|
||||
try
|
||||
{
|
||||
@ -469,12 +210,17 @@ namespace IW4MAdmin.Application.EventParsers
|
||||
});
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(e, "Could not handle custom event generation");
|
||||
_logger.LogError(ex, "Could not handle custom log event generation");
|
||||
}
|
||||
|
||||
return new GameEvent()
|
||||
return GenerateDefaultEvent(logLine, gameTime);
|
||||
}
|
||||
|
||||
private static GameEvent GenerateDefaultEvent(string logLine, long gameTime)
|
||||
{
|
||||
return new GameEvent
|
||||
{
|
||||
Type = GameEvent.EventType.Unknown,
|
||||
Data = logLine,
|
||||
@ -486,8 +232,455 @@ namespace IW4MAdmin.Application.EventParsers
|
||||
};
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
foreach (var (key, value) in _regexMap)
|
||||
{
|
||||
var result = key.PatternMatcher.Match(logLine);
|
||||
if (result.Success)
|
||||
{
|
||||
return (value, null);
|
||||
}
|
||||
}
|
||||
|
||||
return (GameEvent.EventType.Unknown, null);
|
||||
}
|
||||
|
||||
/// <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))
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
using System.Linq;
|
||||
using Data.Models.Client.Stats;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
|
||||
namespace IW4MAdmin.Application.Extensions;
|
||||
|
||||
@ -26,9 +25,4 @@ public static class ScriptPluginExtensions
|
||||
{
|
||||
return set.Where(stat => clientIds.Contains(stat.ClientId) && stat.ServerId == (long)serverId).ToList();
|
||||
}
|
||||
|
||||
public static object GetId(this Server server)
|
||||
{
|
||||
return server.GetIdForServer().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ 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;
|
||||
@ -17,7 +18,10 @@ namespace IW4MAdmin.Application.Extensions
|
||||
{
|
||||
public static class StartupExtensions
|
||||
{
|
||||
private static ILogger _defaultLogger = null;
|
||||
private static ILogger _defaultLogger;
|
||||
private static readonly LoggingLevelSwitch LevelSwitch = new();
|
||||
private static readonly LoggingLevelSwitch MicrosoftLevelSwitch = new();
|
||||
private static readonly LoggingLevelSwitch SystemLevelSwitch = new();
|
||||
|
||||
public static IServiceCollection AddBaseLogger(this IServiceCollection services,
|
||||
ApplicationConfiguration appConfig)
|
||||
@ -29,21 +33,37 @@ namespace IW4MAdmin.Application.Extensions
|
||||
.Build();
|
||||
|
||||
var loggerConfig = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning);
|
||||
.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);
|
||||
|
||||
if (Utilities.IsDevelopment)
|
||||
{
|
||||
loggerConfig = loggerConfig.WriteTo.Console(
|
||||
outputTemplate:
|
||||
"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||
.MinimumLevel.Debug();
|
||||
"[{Timestamp:HH:mm:ss} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||
.MinimumLevel.Debug();
|
||||
}
|
||||
|
||||
_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));
|
||||
|
@ -1,13 +1,13 @@
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using SharedLibraryCore;
|
||||
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,16 +31,11 @@ namespace IW4MAdmin.Application.Factories
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission,
|
||||
bool isTargetRequired, IEnumerable<(string, bool)> args, Func<GameEvent, Task> executeAction, Server.Game[] supportedGames)
|
||||
bool isTargetRequired, IEnumerable<CommandArgument> args, Func<GameEvent, Task> executeAction, IEnumerable<Reference.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, argsArray, executeAction,
|
||||
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, args, executeAction,
|
||||
_config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>(), supportedGames);
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
213
Application/IO/BaseConfigurationHandlerV2.cs
Normal file
213
Application/IO/BaseConfigurationHandlerV2.cs
Normal file
@ -0,0 +1,213 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
60
Application/IO/ConfigurationWatcher.cs
Normal file
60
Application/IO/ConfigurationWatcher.cs
Normal file
@ -0,0 +1,60 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@ -27,8 +27,13 @@ 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
|
||||
@ -42,11 +47,13 @@ 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,
|
||||
@ -70,6 +77,19 @@ 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)
|
||||
@ -106,7 +126,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,
|
||||
@ -114,6 +134,11 @@ namespace IW4MAdmin
|
||||
};
|
||||
|
||||
Manager.AddEvent(e);
|
||||
Manager.QueueEvent(new ClientStateInitializeEvent
|
||||
{
|
||||
Client = client,
|
||||
Source = this,
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
@ -208,10 +233,17 @@ namespace IW4MAdmin
|
||||
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name,
|
||||
E.Origin.ToString());
|
||||
await cmd.ExecuteAsync(E);
|
||||
Manager.QueueEvent(new ClientExecuteCommandEvent
|
||||
{
|
||||
Command = cmd,
|
||||
Client = E.Origin,
|
||||
Source = this,
|
||||
CommandText = E.Data
|
||||
});
|
||||
}
|
||||
|
||||
var pluginTasks = Manager.Plugins
|
||||
.Select(async plugin => await CreatePluginTask(plugin, E));
|
||||
var pluginTasks = Manager.Plugins.Where(plugin => !plugin.IsParser)
|
||||
.Select(plugin => CreatePluginTask(plugin, E));
|
||||
|
||||
await Task.WhenAll(pluginTasks);
|
||||
}
|
||||
@ -248,7 +280,7 @@ namespace IW4MAdmin
|
||||
|
||||
try
|
||||
{
|
||||
await plugin.OnEventAsync(gameEvent, this).WithWaitCancellation(tokenSource.Token);
|
||||
await plugin.OnEventAsync(gameEvent, this);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@ -275,29 +307,7 @@ namespace IW4MAdmin
|
||||
{
|
||||
ServerLogger.LogDebug("processing event of type {type}", E.Type);
|
||||
|
||||
if (E.Type == GameEvent.EventType.Start)
|
||||
{
|
||||
var existingServer = (await _serverCache
|
||||
.FirstAsync(server => server.Id == EndPoint));
|
||||
|
||||
var serverId = await GetIdForServer(E.Owner);
|
||||
|
||||
if (existingServer == null)
|
||||
{
|
||||
var server = new EFServer()
|
||||
{
|
||||
Port = Port,
|
||||
EndPoint = ToString(),
|
||||
ServerId = serverId,
|
||||
GameName = (Reference.Game?)GameName,
|
||||
HostName = Hostname
|
||||
};
|
||||
|
||||
await _serverCache.AddAsync(server);
|
||||
}
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.ConnectionLost)
|
||||
if (E.Type == GameEvent.EventType.ConnectionLost)
|
||||
{
|
||||
var exception = E.Extra as Exception;
|
||||
ServerLogger.LogError(exception,
|
||||
@ -305,12 +315,12 @@ namespace IW4MAdmin
|
||||
|
||||
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
||||
{
|
||||
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
|
||||
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{ListenAddress}:{ListenPort}"));
|
||||
|
||||
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
|
||||
.WithCategory(Alert.AlertCategory.Error)
|
||||
.FromSource("System")
|
||||
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"))
|
||||
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{ListenAddress}:{ListenPort}"))
|
||||
.ExpiresIn(TimeSpan.FromDays(1));
|
||||
|
||||
Manager.AlertManager.AddAlert(alert);
|
||||
@ -322,16 +332,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($"{IP}:{Port}"));
|
||||
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{ListenAddress}:{ListenPort}"));
|
||||
|
||||
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
|
||||
.WithCategory(Alert.AlertCategory.Information)
|
||||
.FromSource("System")
|
||||
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"))
|
||||
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{ListenAddress}:{ListenPort}"))
|
||||
.ExpiresIn(TimeSpan.FromDays(1));
|
||||
|
||||
Manager.AlertManager.AddAlert(alert);
|
||||
@ -348,9 +358,18 @@ namespace IW4MAdmin
|
||||
else if (E.Type == GameEvent.EventType.ChangePermission)
|
||||
{
|
||||
var newPermission = (Permission) E.Extra;
|
||||
var oldPermission = E.Target.Level;
|
||||
ServerLogger.LogInformation("{origin} is setting {target} to permission level {newPermission}",
|
||||
E.Origin.ToString(), E.Target.ToString(), newPermission);
|
||||
await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin);
|
||||
|
||||
Manager.QueueEvent(new ClientPermissionChangeEvent
|
||||
{
|
||||
Client = E.Origin,
|
||||
Source = this,
|
||||
OldPermission = oldPermission,
|
||||
NewPermission = newPermission
|
||||
});
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.Connect)
|
||||
@ -358,7 +377,6 @@ 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()
|
||||
@ -498,6 +516,12 @@ namespace IW4MAdmin
|
||||
|
||||
await Manager.GetPenaltyService().Create(newPenalty);
|
||||
E.Target.SetLevel(Permission.Flagged, E.Origin);
|
||||
|
||||
Manager.QueueEvent(new ClientPenaltyEvent
|
||||
{
|
||||
Client = E.Target,
|
||||
Penalty = newPenalty
|
||||
});
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.Unflag)
|
||||
@ -515,8 +539,14 @@ 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);
|
||||
E.Target.GameName, E.Target.CurrentAlias?.IPAddress, new[] {EFPenalty.PenaltyType.Flag});
|
||||
await Manager.GetPenaltyService().Create(unflagPenalty);
|
||||
|
||||
Manager.QueueEvent(new ClientPenaltyRevokeEvent
|
||||
{
|
||||
Client = E.Target,
|
||||
Penalty = unflagPenalty
|
||||
});
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.Report)
|
||||
@ -552,6 +582,13 @@ namespace IW4MAdmin
|
||||
Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"]
|
||||
.FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner));
|
||||
}
|
||||
|
||||
Manager.QueueEvent(new ClientPenaltyEvent
|
||||
{
|
||||
Client = E.Target,
|
||||
Penalty = newReport,
|
||||
Source = this
|
||||
});
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.TempBan)
|
||||
@ -689,12 +726,12 @@ namespace IW4MAdmin
|
||||
{
|
||||
if (dict.ContainsKey("gametype"))
|
||||
{
|
||||
Gametype = dict["gametype"];
|
||||
UpdateGametype(dict["gametype"]);
|
||||
}
|
||||
|
||||
if (dict.ContainsKey("hostname"))
|
||||
{
|
||||
Hostname = dict["hostname"];
|
||||
UpdateHostname(dict["hostname"]);
|
||||
}
|
||||
|
||||
var newMapName = dict.ContainsKey("mapname")
|
||||
@ -707,24 +744,30 @@ namespace IW4MAdmin
|
||||
else
|
||||
{
|
||||
var dict = (Dictionary<string, string>)E.Extra;
|
||||
|
||||
if (dict.ContainsKey("g_gametype"))
|
||||
{
|
||||
Gametype = dict["g_gametype"];
|
||||
UpdateGametype(dict["g_gametype"]);
|
||||
}
|
||||
|
||||
if (dict.ContainsKey("sv_hostname"))
|
||||
{
|
||||
Hostname = dict["sv_hostname"];
|
||||
UpdateHostname(dict["sv_hostname"]);
|
||||
}
|
||||
|
||||
if (dict.ContainsKey("sv_maxclients"))
|
||||
{
|
||||
MaxClients = int.Parse(dict["sv_maxclients"]);
|
||||
UpdateMaxPlayers(int.Parse(dict["sv_maxclients"]));
|
||||
}
|
||||
|
||||
else if (dict.ContainsKey("com_maxclients"))
|
||||
{
|
||||
MaxClients = int.Parse(dict["com_maxclients"]);
|
||||
UpdateMaxPlayers(int.Parse(dict["com_maxclients"]));
|
||||
}
|
||||
|
||||
else if (dict.ContainsKey("com_maxplayers"))
|
||||
{
|
||||
UpdateMaxPlayers(int.Parse(dict["com_maxplayers"]));
|
||||
}
|
||||
|
||||
if (dict.ContainsKey("mapname"))
|
||||
@ -770,34 +813,6 @@ namespace IW4MAdmin
|
||||
{
|
||||
E.Origin.UpdateTeam(E.Extra as string);
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.MetaUpdated)
|
||||
{
|
||||
if (E.Extra is "PersistentClientGuid")
|
||||
{
|
||||
var parts = E.Data.Split(",");
|
||||
|
||||
if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
|
||||
int.TryParse(parts[1], out var low))
|
||||
{
|
||||
var guid = long.Parse(high.ToString("X") + low.ToString("X"), NumberStyles.HexNumber);
|
||||
|
||||
var penalties = await Manager.GetPenaltyService()
|
||||
.GetActivePenaltiesByIdentifier(null, guid, (Reference.Game)GameName);
|
||||
var banPenalty =
|
||||
penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
|
||||
|
||||
if (banPenalty is not null && E.Origin.Level != Permission.Banned)
|
||||
{
|
||||
ServerLogger.LogInformation(
|
||||
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
|
||||
E.Origin.ToString(), guid);
|
||||
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(guid),
|
||||
Utilities.IW4MAdminClient(this), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (ChatHistory)
|
||||
{
|
||||
@ -818,6 +833,53 @@ namespace IW4MAdmin
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnsureServerAdded()
|
||||
{
|
||||
var gameServer = await _serverCache
|
||||
.FirstAsync(server => server.EndPoint == base.Id);
|
||||
|
||||
if (gameServer == null)
|
||||
{
|
||||
gameServer = new EFServer
|
||||
{
|
||||
Port = ListenPort,
|
||||
EndPoint = base.Id,
|
||||
ServerId = BuildLegacyDatabaseId(),
|
||||
GameName = (Reference.Game?)GameName,
|
||||
HostName = ServerName
|
||||
};
|
||||
|
||||
await _serverCache.AddAsync(gameServer);
|
||||
}
|
||||
|
||||
await using var context = _serviceProvider.GetRequiredService<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);
|
||||
@ -907,41 +969,55 @@ namespace IW4MAdmin
|
||||
public override async Task<long> GetIdForServer(Server server = null)
|
||||
{
|
||||
server ??= this;
|
||||
|
||||
if ($"{server.IP}:{server.Port.ToString()}" == "66.150.121.184:28965")
|
||||
{
|
||||
return 886229536;
|
||||
}
|
||||
|
||||
// todo: this is not stable and will need to be migrated again...
|
||||
long id = HashCode.Combine(server.IP, server.Port);
|
||||
id = id < 0 ? Math.Abs(id) : id;
|
||||
|
||||
var serverId = (await _serverCache
|
||||
.FirstAsync(_server => _server.ServerId == server.EndPoint ||
|
||||
_server.EndPoint == server.ToString() ||
|
||||
_server.ServerId == id))?.ServerId;
|
||||
|
||||
return !serverId.HasValue ? id : serverId.Value;
|
||||
return (await _serverCache.FirstAsync(cachedServer =>
|
||||
cachedServer.EndPoint == server.Id || cachedServer.ServerId == server.EndPoint)).ServerId;
|
||||
}
|
||||
|
||||
private void UpdateMap(string mapname)
|
||||
private long BuildLegacyDatabaseId()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(mapname))
|
||||
long id = HashCode.Combine(ListenAddress, ListenPort);
|
||||
return id < 0 ? Math.Abs(id) : id;
|
||||
}
|
||||
|
||||
private void UpdateMap(string mapName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(mapName))
|
||||
{
|
||||
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map()
|
||||
{
|
||||
Alias = mapname,
|
||||
Name = mapname
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
var foundMap = Maps.Find(m => m.Name == mapName) ?? new Map
|
||||
{
|
||||
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))
|
||||
{
|
||||
Gametype = gameType;
|
||||
return;
|
||||
}
|
||||
|
||||
Gametype = gameType;
|
||||
|
||||
using(LogContext.PushProperty("Server", Id))
|
||||
{
|
||||
ServerLogger.LogDebug("Updating gametype to {Gametype}", gameType);
|
||||
}
|
||||
}
|
||||
|
||||
@ -952,7 +1028,7 @@ namespace IW4MAdmin
|
||||
return;
|
||||
}
|
||||
|
||||
using(LogContext.PushProperty("Server", ToString()))
|
||||
using(LogContext.PushProperty("Server", Id))
|
||||
{
|
||||
ServerLogger.LogDebug("Updating hostname to {HostName}", hostname);
|
||||
}
|
||||
@ -967,7 +1043,7 @@ namespace IW4MAdmin
|
||||
return;
|
||||
}
|
||||
|
||||
using(LogContext.PushProperty("Server", ToString()))
|
||||
using(LogContext.PushProperty("Server", Id))
|
||||
{
|
||||
ServerLogger.LogDebug("Updating max clients to {MaxPlayers}", maxPlayers);
|
||||
}
|
||||
@ -981,7 +1057,7 @@ namespace IW4MAdmin
|
||||
{
|
||||
await client.OnDisconnect();
|
||||
|
||||
var e = new GameEvent()
|
||||
var e = new GameEvent
|
||||
{
|
||||
Type = GameEvent.EventType.Disconnect,
|
||||
Owner = this,
|
||||
@ -992,6 +1068,14 @@ namespace IW4MAdmin
|
||||
|
||||
await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token);
|
||||
}
|
||||
|
||||
using var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(Utilities.DefaultCommandTimeout);
|
||||
|
||||
Manager.QueueEvent(new MonitorStopEvent
|
||||
{
|
||||
Server = this
|
||||
});
|
||||
}
|
||||
|
||||
private DateTime _lastMessageSent = DateTime.Now;
|
||||
@ -1073,6 +1157,16 @@ 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
|
||||
@ -1084,6 +1178,12 @@ namespace IW4MAdmin
|
||||
};
|
||||
|
||||
Manager.AddEvent(gameEvent);
|
||||
|
||||
Manager.QueueEvent(new ConnectionRestoreEvent
|
||||
{
|
||||
Server = this,
|
||||
Source = this
|
||||
});
|
||||
}
|
||||
|
||||
LastPoll = DateTime.Now;
|
||||
@ -1107,6 +1207,12 @@ namespace IW4MAdmin
|
||||
};
|
||||
|
||||
Manager.AddEvent(gameEvent);
|
||||
Manager.QueueEvent(new ConnectionInterruptEvent
|
||||
{
|
||||
Server = this,
|
||||
Source = this
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
@ -1157,22 +1263,20 @@ namespace IW4MAdmin
|
||||
ServerLogger.LogError(e, "Unexpected exception occured during processing updates");
|
||||
}
|
||||
|
||||
Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]"));
|
||||
Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{ListenAddress}:{ListenPort}]"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RunServerCollection()
|
||||
{
|
||||
var appConfig = _serviceProvider.GetService<ApplicationConfiguration>();
|
||||
|
||||
if (DateTime.Now - _lastPlayerCount < appConfig?.ServerDataCollectionInterval)
|
||||
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)
|
||||
{
|
||||
@ -1196,13 +1300,13 @@ namespace IW4MAdmin
|
||||
{
|
||||
ResolvedIpEndPoint =
|
||||
new IPEndPoint(
|
||||
(await Dns.GetHostAddressesAsync(IP)).First(address =>
|
||||
address.AddressFamily == AddressFamily.InterNetwork), Port);
|
||||
(await Dns.GetHostAddressesAsync(ListenAddress)).First(address =>
|
||||
address.AddressFamily == AddressFamily.InterNetwork), ListenPort);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ServerLogger.LogWarning(ex, "Could not resolve hostname or IP for RCon connection {IP}:{Port}", IP, Port);
|
||||
ResolvedIpEndPoint = new IPEndPoint(IPAddress.Parse(IP), Port);
|
||||
ServerLogger.LogWarning(ex, "Could not resolve hostname or IP for RCon connection {Address}:{Port}", ListenAddress, ListenPort);
|
||||
ResolvedIpEndPoint = new IPEndPoint(IPAddress.Parse(ListenAddress), ListenPort);
|
||||
}
|
||||
|
||||
RconParser = Manager.AdditionalRConParsers
|
||||
@ -1259,10 +1363,12 @@ 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", Manager.GetApplicationSettings().Configuration().CustomSayName,
|
||||
await this.SetDvarAsync("sv_sayname", CustomSayName,
|
||||
Manager.CancellationToken);
|
||||
}
|
||||
|
||||
@ -1300,12 +1406,14 @@ namespace IW4MAdmin
|
||||
}
|
||||
|
||||
WorkingDirectory = basepath.Value;
|
||||
this.Hostname = hostname;
|
||||
this.MaxClients = maxplayers;
|
||||
this.FSGame = game.Value;
|
||||
this.Gametype = gametype;
|
||||
this.IP = ip.Value is "localhost" or "0.0.0.0" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
|
||||
this.GamePassword = gamePassword.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;
|
||||
|
||||
UpdateMap(mapname);
|
||||
|
||||
if (RconParser.CanGenerateLogPath && string.IsNullOrEmpty(ServerConfig.ManualLogPath))
|
||||
@ -1467,6 +1575,12 @@ namespace IW4MAdmin
|
||||
.FormatExt(activeClient.Warnings, activeClient.Name, reason);
|
||||
activeClient.CurrentServer.Broadcast(message);
|
||||
}
|
||||
|
||||
Manager.QueueEvent(new ClientPenaltyEvent
|
||||
{
|
||||
Client = targetClient,
|
||||
Penalty = newPenalty
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task Kick(string reason, EFClient targetClient, EFClient originClient, EFPenalty previousPenalty)
|
||||
@ -1505,8 +1619,20 @@ 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
|
||||
@ -1538,6 +1664,12 @@ namespace IW4MAdmin
|
||||
ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
|
||||
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
|
||||
}
|
||||
|
||||
Manager.QueueEvent(new ClientPenaltyEvent
|
||||
{
|
||||
Client = targetClient,
|
||||
Penalty = newPenalty
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
|
||||
@ -1573,6 +1705,12 @@ namespace IW4MAdmin
|
||||
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
|
||||
await activeClient.CurrentServer.ExecuteCommandAsync(formattedString);
|
||||
}
|
||||
|
||||
Manager.QueueEvent(new ClientPenaltyEvent
|
||||
{
|
||||
Client = targetClient,
|
||||
Penalty = newPenalty
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task Unban(string reason, EFClient targetClient, EFClient originClient)
|
||||
@ -1594,6 +1732,12 @@ namespace IW4MAdmin
|
||||
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
|
||||
targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
|
||||
await Manager.GetPenaltyService().Create(unbanPenalty);
|
||||
|
||||
Manager.QueueEvent(new ClientPenaltyRevokeEvent
|
||||
{
|
||||
Client = targetClient,
|
||||
Penalty = unbanPenalty
|
||||
});
|
||||
}
|
||||
|
||||
public override void InitializeTokens()
|
||||
@ -1603,5 +1747,7 @@ namespace IW4MAdmin
|
||||
Manager.GetMessageTokens().Add(new MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.NextMapCommand.GetNextMap(s, _translationLookup)));
|
||||
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(ListAdminsCommand.OnlineAdmins(s, _translationLookup))));
|
||||
}
|
||||
|
||||
public override long LegacyDatabaseId => _cachedDatabaseServer.ServerId;
|
||||
}
|
||||
}
|
||||
|
@ -29,12 +29,16 @@ 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;
|
||||
@ -48,15 +52,36 @@ namespace IW4MAdmin.Application
|
||||
public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString());
|
||||
private static ApplicationManager _serverManager;
|
||||
private static Task _applicationTask;
|
||||
private static ServiceProvider _serviceProvider;
|
||||
private static IServiceProvider _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// entrypoint of the application
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static async Task Main(string[] args)
|
||||
public static async Task Main(bool noConfirm = false, int? maxConcurrentRequests = 25, int? requestQueueLimit = 25)
|
||||
{
|
||||
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;
|
||||
@ -69,7 +94,7 @@ namespace IW4MAdmin.Application
|
||||
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
|
||||
Console.WriteLine("=====================================================");
|
||||
|
||||
await LaunchAsync(args);
|
||||
await LaunchAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -95,13 +120,13 @@ namespace IW4MAdmin.Application
|
||||
/// task that initializes application and starts the application monitoring and runtime tasks
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static async Task LaunchAsync(string[] args)
|
||||
private static async Task LaunchAsync()
|
||||
{
|
||||
restart:
|
||||
ITranslationLookup translationLookup = null;
|
||||
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
|
||||
Utilities.DefaultLogger = logger;
|
||||
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version} {@Args}", Version, args);
|
||||
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version}", Version);
|
||||
|
||||
try
|
||||
{
|
||||
@ -109,23 +134,23 @@ namespace IW4MAdmin.Application
|
||||
ConfigurationMigration.MoveConfigFolder10518(null);
|
||||
ConfigurationMigration.CheckDirectories();
|
||||
ConfigurationMigration.RemoveObsoletePlugins20210322();
|
||||
|
||||
logger.LogDebug("Configuring services...");
|
||||
var services = await ConfigureServices(args);
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
var versionChecker = _serviceProvider.GetRequiredService<IMasterCommunication>();
|
||||
_serverManager = (ApplicationManager) _serviceProvider.GetRequiredService<IManager>();
|
||||
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
|
||||
|
||||
_applicationTask = RunApplicationTasksAsync(logger, services);
|
||||
var tasks = new[]
|
||||
{
|
||||
versionChecker.CheckVersion(),
|
||||
_applicationTask
|
||||
};
|
||||
var configHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
||||
await configHandler.BuildAsync();
|
||||
_serviceProvider = WebfrontCore.Program.InitializeServices(ConfigureServices,
|
||||
(configHandler.Configuration() ?? new ApplicationConfiguration()).WebfrontBindUrl);
|
||||
|
||||
_serverManager = (ApplicationManager)_serviceProvider.GetRequiredService<IManager>();
|
||||
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
|
||||
|
||||
await _serverManager.Init();
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
_applicationTask = RunApplicationTasksAsync(logger, _serverManager, _serviceProvider);
|
||||
|
||||
await _applicationTask;
|
||||
logger.LogInformation("Shutdown completed successfully");
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
@ -175,21 +200,55 @@ namespace IW4MAdmin.Application
|
||||
{
|
||||
goto restart;
|
||||
}
|
||||
|
||||
await _serviceProvider.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// runs the core application tasks
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
|
||||
private static Task RunApplicationTasksAsync(ILogger logger, ApplicationManager applicationManager,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
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.Init(_serverManager, _serviceProvider, services, _serverManager.CancellationToken)
|
||||
? 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();
|
||||
|
||||
})
|
||||
: Task.CompletedTask;
|
||||
|
||||
var collectionService = _serviceProvider.GetRequiredService<IServerDataCollector>();
|
||||
if (_serverManager.GetApplicationSettings().Configuration().EnableWebFront)
|
||||
{
|
||||
try
|
||||
{
|
||||
onWebfrontErrored.Wait(webfrontLifetime.ApplicationStarted);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored when webfront successfully starts
|
||||
}
|
||||
|
||||
if (onWebfrontErrored.IsSet)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -200,18 +259,15 @@ namespace IW4MAdmin.Application
|
||||
|
||||
var tasks = new[]
|
||||
{
|
||||
applicationManager.Start(),
|
||||
versionChecker.CheckVersion(),
|
||||
webfrontTask,
|
||||
_serverManager.Start(),
|
||||
_serviceProvider.GetRequiredService<IMasterCommunication>()
|
||||
.RunUploadStatus(_serverManager.CancellationToken),
|
||||
masterCommunicator.RunUploadStatus(_serverManager.CancellationToken),
|
||||
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
|
||||
};
|
||||
|
||||
logger.LogDebug("Starting webfront and input tasks");
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
logger.LogInformation("Shutdown completed successfully");
|
||||
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -299,8 +355,21 @@ namespace IW4MAdmin.Application
|
||||
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
|
||||
foreach (var pluginType in plugins)
|
||||
{
|
||||
defaultLogger.LogDebug("Registered plugin type {Name}", pluginType.FullName);
|
||||
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
|
||||
var isV2 = pluginType.GetInterface(nameof(IPluginV2), false) != null;
|
||||
|
||||
defaultLogger.LogDebug("Registering plugin type {Name}", pluginType.FullName);
|
||||
|
||||
serviceCollection.AddSingleton(!isV2 ? typeof(IPlugin) : typeof(IPluginV2), pluginType);
|
||||
|
||||
try
|
||||
{
|
||||
var registrationMethod = pluginType.GetMethod(nameof(IPluginV2.RegisterDependencies));
|
||||
registrationMethod?.Invoke(null, new object[] { serviceCollection });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
defaultLogger.LogError(ex, "Could not register plugin of type {Type}", pluginType.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// register the plugin commands
|
||||
@ -321,10 +390,13 @@ namespace IW4MAdmin.Application
|
||||
serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
|
||||
}
|
||||
|
||||
// register any script plugins
|
||||
foreach (var plugin in pluginImporter.DiscoverScriptPlugins())
|
||||
var scriptPlugins = pluginImporter.DiscoverScriptPlugins();
|
||||
|
||||
foreach (var scriptPlugin in scriptPlugins)
|
||||
{
|
||||
serviceCollection.AddSingleton(plugin);
|
||||
serviceCollection.AddSingleton(scriptPlugin.Item1, sp =>
|
||||
sp.GetRequiredService<IScriptPluginFactory>()
|
||||
.CreateScriptPlugin(scriptPlugin.Item1, scriptPlugin.Item2));
|
||||
}
|
||||
|
||||
// register any eventable types
|
||||
@ -345,22 +417,27 @@ namespace IW4MAdmin.Application
|
||||
/// <summary>
|
||||
/// Configures the dependency injection services
|
||||
/// </summary>
|
||||
private static async Task<IServiceCollection> ConfigureServices(string[] args)
|
||||
private static void ConfigureServices(IServiceCollection serviceCollection)
|
||||
{
|
||||
// todo: this is a quick fix
|
||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
|
||||
// setup the static resources (config/master api/translations)
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddConfiguration<ApplicationConfiguration>("IW4MAdminSettings")
|
||||
.AddConfiguration<DefaultSettings>()
|
||||
.AddConfiguration<CommandConfiguration>()
|
||||
.AddConfiguration<StatsConfiguration>("StatsPluginSettings");
|
||||
|
||||
// for legacy purposes. update at some point
|
||||
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
||||
await appConfigHandler.BuildAsync();
|
||||
var defaultConfigHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
|
||||
await defaultConfigHandler.BuildAsync();
|
||||
appConfigHandler.BuildAsync().GetAwaiter().GetResult();
|
||||
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration");
|
||||
await commandConfigHandler.BuildAsync();
|
||||
var statsCommandHandler = new BaseConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
|
||||
await statsCommandHandler.BuildAsync();
|
||||
var defaultConfig = defaultConfigHandler.Configuration();
|
||||
commandConfigHandler.BuildAsync().GetAwaiter().GetResult();
|
||||
|
||||
if (appConfigHandler.Configuration()?.MasterUrl == new Uri("http://api.raidmax.org:5000"))
|
||||
{
|
||||
appConfigHandler.Configuration().MasterUrl = new ApplicationConfiguration().MasterUrl;
|
||||
}
|
||||
|
||||
var appConfig = appConfigHandler.Configuration();
|
||||
var masterUri = Utilities.IsDevelopment
|
||||
? new Uri("http://127.0.0.1:8080")
|
||||
@ -372,12 +449,12 @@ namespace IW4MAdmin.Application
|
||||
};
|
||||
var masterRestClient = RestClient.For<IMasterApi>(httpClient);
|
||||
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
|
||||
|
||||
|
||||
if (appConfig == null)
|
||||
{
|
||||
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
|
||||
appConfigHandler.Set(appConfig);
|
||||
await appConfigHandler.Save();
|
||||
appConfigHandler.Save().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
// register override level names
|
||||
@ -390,17 +467,10 @@ 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())
|
||||
@ -450,27 +520,22 @@ 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);
|
||||
|
||||
if (args.Contains("serialevents"))
|
||||
{
|
||||
serviceCollection.AddSingleton<IEventHandler, SerialGameEventHandler>();
|
||||
}
|
||||
else
|
||||
{
|
||||
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
|
||||
}
|
||||
|
||||
|
||||
serviceCollection.AddSingleton<ICoreEventHandler, CoreEventHandler>();
|
||||
serviceCollection.AddSource();
|
||||
|
||||
return serviceCollection;
|
||||
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
|
||||
}
|
||||
|
||||
private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig)
|
||||
|
@ -49,8 +49,10 @@ 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)
|
||||
@ -66,6 +68,13 @@ namespace IW4MAdmin.Application.Misc
|
||||
ConfigurationFileName = FileName
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onSaving.CurrentCount == 0)
|
||||
{
|
||||
_onSaving.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Save()
|
||||
@ -76,6 +85,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
await using var fileStream = File.Create(FileName);
|
||||
await JsonSerializer.SerializeAsync(fileStream, _configuration, _serializerOptions);
|
||||
await fileStream.DisposeAsync();
|
||||
}
|
||||
|
||||
finally
|
||||
|
@ -1,27 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ 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;
|
||||
@ -86,8 +87,6 @@ public class InteractionRegistration : IInteractionRegistration
|
||||
int? clientId = null,
|
||||
Reference.Game? game = null, CancellationToken token = default)
|
||||
{
|
||||
return Enumerable.Empty<IInteractionData>();
|
||||
// fixme: multi-threading is broken when dealing with script plugins
|
||||
return await GetInteractionsInternal(interactionPrefix, clientId, game, token);
|
||||
}
|
||||
|
||||
@ -120,6 +119,18 @@ public class InteractionRegistration : IInteractionRegistration
|
||||
return scriptPlugin.ExecuteAction<string>(interaction.ScriptAction, token, 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)
|
||||
|
@ -103,17 +103,15 @@ namespace IW4MAdmin.Application.Misc
|
||||
await UploadStatus();
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not send heartbeat");
|
||||
_logger.LogWarning("Could not send heartbeat - {Message}", ex.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(Interval, token);
|
||||
}
|
||||
|
||||
catch
|
||||
{
|
||||
break;
|
||||
@ -149,13 +147,13 @@ namespace IW4MAdmin.Application.Misc
|
||||
Map = s.CurrentMap.Name,
|
||||
MaxClientNum = s.MaxClients,
|
||||
Id = s.EndPoint,
|
||||
Port = (short)s.Port,
|
||||
IPAddress = s.IP
|
||||
Port = (short)s.ListenPort,
|
||||
IPAddress = s.ListenAddress
|
||||
}).ToList(),
|
||||
WebfrontUrl = _appConfig.WebfrontUrl
|
||||
};
|
||||
|
||||
Response<ResultMessage> response = null;
|
||||
Response<ResultMessage> response;
|
||||
|
||||
if (_firstHeartBeat)
|
||||
{
|
||||
@ -170,7 +168,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,7 @@ 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;
|
||||
@ -22,7 +19,6 @@ 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)
|
||||
@ -30,7 +26,6 @@ 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,
|
||||
@ -69,26 +64,6 @@ 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,
|
||||
|
@ -1,177 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -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(decryptedAssembly => Assembly.Load(decryptedAssembly));
|
||||
.Select(Assembly.Load);
|
||||
}
|
||||
|
||||
public IEnumerable<string> DecryptScripts(string[] encryptedScripts)
|
||||
@ -38,24 +38,24 @@ namespace IW4MAdmin.Application.Misc
|
||||
return DecryptContent(encryptedScripts).Select(decryptedScript => Encoding.UTF8.GetString(decryptedScript));
|
||||
}
|
||||
|
||||
private byte[][] DecryptContent(string[] content)
|
||||
private IEnumerable<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 new byte[0][];
|
||||
return Array.Empty<byte[]>();
|
||||
}
|
||||
|
||||
var assemblies = content.Select(piece =>
|
||||
{
|
||||
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 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];
|
||||
|
||||
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));
|
||||
var keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id), IterationCount, HashAlgorithmName.SHA512);
|
||||
var encryption = new AesGcm(keyGen.GetBytes(KeyLength));
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -27,7 +28,7 @@ public class RemoteCommandService : IRemoteCommandService
|
||||
public async Task<IEnumerable<CommandResponseInfo>> Execute(int originId, int? targetId, string command,
|
||||
IEnumerable<string> arguments, Server server)
|
||||
{
|
||||
var (success, result) = await ExecuteWithResult(originId, targetId, command, arguments, server);
|
||||
var (_, result) = await ExecuteWithResult(originId, targetId, command, arguments, server);
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -56,7 +57,8 @@ public class RemoteCommandService : IRemoteCommandService
|
||||
: $"{_appConfig.CommandPrefix}{command}",
|
||||
Origin = client,
|
||||
Owner = server,
|
||||
IsRemote = true
|
||||
IsRemote = true,
|
||||
CorrelationId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
server.Manager.AddEvent(remoteEvent);
|
||||
@ -72,7 +74,7 @@ public class RemoteCommandService : IRemoteCommandService
|
||||
{
|
||||
response = new[]
|
||||
{
|
||||
new CommandResponseInfo()
|
||||
new CommandResponseInfo
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Response = Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"]
|
||||
@ -90,7 +92,7 @@ public class RemoteCommandService : IRemoteCommandService
|
||||
}
|
||||
}
|
||||
|
||||
catch (System.OperationCanceledException)
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
response = new[]
|
||||
{
|
||||
|
@ -1,96 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -12,8 +12,10 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Events.Management;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Interfaces.Events;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
@ -24,28 +26,20 @@ namespace IW4MAdmin.Application.Misc
|
||||
private readonly IManager _manager;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
private readonly IEventPublisher _eventPublisher;
|
||||
|
||||
private bool _inProgress;
|
||||
private TimeSpan _period;
|
||||
|
||||
public ServerDataCollector(ILogger<ServerDataCollector> logger, ApplicationConfiguration appConfig,
|
||||
IManager manager, IDatabaseContextFactory contextFactory, IEventPublisher eventPublisher)
|
||||
IManager manager, IDatabaseContextFactory contextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_appConfig = appConfig;
|
||||
_manager = manager;
|
||||
_contextFactory = contextFactory;
|
||||
_eventPublisher = eventPublisher;
|
||||
|
||||
_eventPublisher.OnClientConnect += SaveConnectionInfo;
|
||||
_eventPublisher.OnClientDisconnect += SaveConnectionInfo;
|
||||
}
|
||||
|
||||
~ServerDataCollector()
|
||||
{
|
||||
_eventPublisher.OnClientConnect -= SaveConnectionInfo;
|
||||
_eventPublisher.OnClientDisconnect -= SaveConnectionInfo;
|
||||
IManagementEventSubscriptions.ClientStateAuthorized += SaveConnectionInfo;
|
||||
IManagementEventSubscriptions.ClientStateDisposed += SaveConnectionInfo;
|
||||
}
|
||||
|
||||
public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default)
|
||||
@ -131,18 +125,19 @@ namespace IW4MAdmin.Application.Misc
|
||||
await context.SaveChangesAsync(token);
|
||||
}
|
||||
|
||||
private void SaveConnectionInfo(object sender, GameEvent gameEvent)
|
||||
private async Task SaveConnectionInfo(ClientStateEvent stateEvent, CancellationToken token)
|
||||
{
|
||||
using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
context.ConnectionHistory.Add(new EFClientConnectionHistory
|
||||
{
|
||||
ClientId = gameEvent.Origin.ClientId,
|
||||
ServerId = gameEvent.Owner.GetIdForServer().Result,
|
||||
ConnectionType = gameEvent.Type == GameEvent.EventType.Connect
|
||||
ClientId = stateEvent.Client.ClientId,
|
||||
ServerId = await stateEvent.Client.CurrentServer.GetIdForServer(),
|
||||
ConnectionType = stateEvent is ClientStateAuthorizeEvent
|
||||
? Reference.ConnectionType.Connect
|
||||
: Reference.ConnectionType.Disconnect
|
||||
});
|
||||
context.SaveChanges();
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ 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;
|
||||
@ -40,21 +41,31 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
|
||||
public async Task<(int?, DateTime?)>
|
||||
MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null,
|
||||
MaxConcurrentClientsAsync(long? serverId = null, Reference.Game? gameCode = null, TimeSpan? overPeriod = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
_snapshotCache.SetCacheItem(async (snapshots, cancellationToken) =>
|
||||
_snapshotCache.SetCacheItem(async (snapshots, ids, 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);
|
||||
|
||||
|
||||
int? maxClients;
|
||||
DateTime? maxClientsTime;
|
||||
|
||||
if (serverId != null)
|
||||
if (id != null)
|
||||
{
|
||||
var clients = await snapshots.Where(snapshot => snapshot.ServerId == serverId)
|
||||
var clients = await snapshots.Where(snapshot => snapshot.ServerId == id)
|
||||
.Where(snapshot => game == null || snapshot.Server.GameName == game)
|
||||
.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
|
||||
.OrderByDescending(snapshot => snapshot.ClientCount)
|
||||
.Select(snapshot => new
|
||||
@ -71,15 +82,16 @@ 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
|
||||
{
|
||||
ClientCount = grp.Sum(snapshot => (int?) snapshot.ClientCount),
|
||||
Time = grp.Max(snapshot => (DateTime?) snapshot.CapturedAt)
|
||||
ClientCount = grp.Sum(snapshot => (int?)snapshot.ClientCount),
|
||||
Time = grp.Max(snapshot => (DateTime?)snapshot.CapturedAt)
|
||||
})
|
||||
.OrderByDescending(snapshot => snapshot.ClientCount)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
|
||||
maxClients = clients?.ClientCount;
|
||||
maxClientsTime = clients?.Time;
|
||||
}
|
||||
@ -87,11 +99,12 @@ namespace IW4MAdmin.Application.Misc
|
||||
_logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients);
|
||||
|
||||
return (maxClients, maxClientsTime);
|
||||
}, nameof(MaxConcurrentClientsAsync), _cacheTimeSpan, true);
|
||||
}, nameof(MaxConcurrentClientsAsync), new object[] { gameCode, serverId }, _cacheTimeSpan, true);
|
||||
|
||||
try
|
||||
{
|
||||
return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync), token);
|
||||
return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync),
|
||||
new object[] { gameCode, serverId }, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -100,22 +113,30 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default)
|
||||
public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, Reference.Game? gameCode = null, CancellationToken token = default)
|
||||
{
|
||||
_serverStatsCache.SetCacheItem(async (set, cancellationToken) =>
|
||||
_serverStatsCache.SetCacheItem(async (set, ids, cancellationToken) =>
|
||||
{
|
||||
var count = await set.CountAsync(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 startOfPeriod =
|
||||
DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24);
|
||||
var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod,
|
||||
var recentCount = await set.CountAsync(client => (game == null || client.GameName == game) && client.LastConnection >= startOfPeriod,
|
||||
cancellationToken);
|
||||
|
||||
return (count, recentCount);
|
||||
}, nameof(_serverStatsCache), _cacheTimeSpan, true);
|
||||
}, nameof(_serverStatsCache), new object[] { gameCode }, _cacheTimeSpan, true);
|
||||
|
||||
try
|
||||
{
|
||||
return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), token);
|
||||
return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), new object[] { gameCode }, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -166,21 +187,28 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
|
||||
{
|
||||
_rankedClientsCache.SetCacheItem(async (set, cancellationToken) =>
|
||||
_rankedClientsCache.SetCacheItem((set, ids, cancellationToken) =>
|
||||
{
|
||||
long? id = null;
|
||||
|
||||
if (ids.Any())
|
||||
{
|
||||
id = (long?)ids.First();
|
||||
}
|
||||
|
||||
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
|
||||
return await set
|
||||
return set
|
||||
.Where(rating => rating.Newest)
|
||||
.Where(rating => rating.ServerId == serverId)
|
||||
.Where(rating => rating.ServerId == id)
|
||||
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
|
||||
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
|
||||
.Where(rating => rating.Ranking != null)
|
||||
.CountAsync(cancellationToken);
|
||||
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
|
||||
}, nameof(_rankedClientsCache), new object[] { serverId }, _cacheTimeSpan);
|
||||
|
||||
try
|
||||
{
|
||||
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token);
|
||||
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), new object[] { serverId }, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
207
Application/Plugin/PluginImporter.cs
Normal file
207
Application/Plugin/PluginImporter.cs
Normal file
@ -0,0 +1,207 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -1,14 +1,17 @@
|
||||
using SharedLibraryCore;
|
||||
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.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.Misc
|
||||
namespace IW4MAdmin.Application.Plugin.Script
|
||||
{
|
||||
/// <summary>
|
||||
/// generic script command implementation
|
||||
@ -20,8 +23,8 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
public ScriptCommand(string name, string alias, string description, bool isTargetRequired,
|
||||
EFClient.Permission permission,
|
||||
CommandArgument[] args, Func<GameEvent, Task> executeAction, CommandConfiguration config,
|
||||
ITranslationLookup layout, ILogger<ScriptCommand> logger, Server.Game[] supportedGames)
|
||||
IEnumerable<CommandArgument> args, Func<GameEvent, Task> executeAction, CommandConfiguration config,
|
||||
ITranslationLookup layout, ILogger<ScriptCommand> logger, IEnumerable<Reference.Game> supportedGames)
|
||||
: base(config, layout)
|
||||
{
|
||||
_executeAction = executeAction;
|
||||
@ -31,8 +34,8 @@ namespace IW4MAdmin.Application.Misc
|
||||
Description = description;
|
||||
RequiresTarget = isTargetRequired;
|
||||
Permission = permission;
|
||||
Arguments = args;
|
||||
SupportedGames = supportedGames;
|
||||
Arguments = args.ToArray();
|
||||
SupportedGames = supportedGames?.Select(game => (Server.Game)game).ToArray();
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent e)
|
||||
@ -48,7 +51,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,4 @@
|
||||
using System;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Runtime;
|
||||
using Microsoft.CSharp.RuntimeBinder;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -14,13 +6,25 @@ 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 ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
namespace IW4MAdmin.Application.Plugin.Script
|
||||
{
|
||||
/// <summary>
|
||||
/// implementation of IPlugin
|
||||
@ -70,7 +74,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
|
||||
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory,
|
||||
IScriptPluginServiceResolver serviceResolver)
|
||||
IScriptPluginServiceResolver serviceResolver, IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -130,11 +134,17 @@ namespace IW4MAdmin.Application.Misc
|
||||
.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;
|
||||
Name = pluginObject.name;
|
||||
Version = (float)pluginObject.version;
|
||||
@ -191,8 +201,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
catch (RuntimeBinderException)
|
||||
{
|
||||
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine);
|
||||
await configWrapper.InitializeAsync();
|
||||
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine, configHandler);
|
||||
|
||||
if (!loadComplete)
|
||||
{
|
||||
@ -252,7 +261,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
try
|
||||
{
|
||||
await _onProcessing.WaitAsync();
|
||||
await _onProcessing.WaitAsync(Utilities.DefaultCommandTimeout / 2);
|
||||
shouldRelease = true;
|
||||
WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
@ -269,7 +278,6 @@ namespace IW4MAdmin.Application.Misc
|
||||
_onProcessing.Release(1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Task OnLoadAsync(IManager manager)
|
||||
@ -279,8 +287,6 @@ namespace IW4MAdmin.Application.Misc
|
||||
WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
_scriptEngine.SetValue("_manager", manager);
|
||||
_scriptEngine.SetValue("getDvar", BeginGetDvar);
|
||||
_scriptEngine.SetValue("setDvar", BeginSetDvar);
|
||||
return _scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
|
||||
});
|
||||
|
||||
@ -289,12 +295,6 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
public Task OnTickAsync(Server server)
|
||||
{
|
||||
WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
_scriptEngine.SetValue("_server", server);
|
||||
return _scriptEngine.Evaluate("plugin.onTickAsync(_server)");
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@ -415,10 +415,10 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
|
||||
string permission = dynamicCommand.permission;
|
||||
List<Server.Game> supportedGames = null;
|
||||
List<Reference.Game> supportedGames = null;
|
||||
var targetRequired = false;
|
||||
|
||||
var args = new List<(string, bool)>();
|
||||
var args = new List<CommandArgument>();
|
||||
dynamic arguments = null;
|
||||
|
||||
try
|
||||
@ -445,7 +445,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
foreach (var arg in dynamicCommand.arguments)
|
||||
{
|
||||
args.Add((arg.name, (bool)arg.required));
|
||||
args.Add(new CommandArgument { Name = arg.name, Required = (bool)arg.required });
|
||||
}
|
||||
}
|
||||
|
||||
@ -453,8 +453,8 @@ namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
foreach (var game in dynamicCommand.supportedGames)
|
||||
{
|
||||
supportedGames ??= new List<Server.Game>();
|
||||
supportedGames.Add(Enum.Parse(typeof(Server.Game), game.ToString()));
|
||||
supportedGames ??= new List<Reference.Game>();
|
||||
supportedGames.Add(Enum.Parse(typeof(Reference.Game), game.ToString()));
|
||||
}
|
||||
}
|
||||
catch (RuntimeBinderException)
|
||||
@ -507,175 +507,12 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
|
||||
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission,
|
||||
targetRequired, args, Execute, supportedGames?.ToArray()));
|
||||
targetRequired, args, Execute, supportedGames));
|
||||
}
|
||||
|
||||
return commandList;
|
||||
}
|
||||
|
||||
private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
|
||||
{
|
||||
var operationTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
void OnComplete(IAsyncResult result)
|
||||
{
|
||||
try
|
||||
{
|
||||
_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)
|
||||
{
|
||||
using (LogContext.PushProperty("Server", server.ToString()))
|
||||
{
|
||||
_logger.LogError(ex, "Could not invoke BeginGetDvar callback for {Filename} {@Location}",
|
||||
Path.GetFileName(_fileName), ex.Location);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not complete {BeginGetDvar} for {Class}", nameof(BeginGetDvar), Name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onProcessing.CurrentCount == 0)
|
||||
{
|
||||
_onProcessing.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Thread(() =>
|
||||
{
|
||||
if (DateTime.Now - (server.MatchEndTime ?? server.MatchStartTime) < TimeSpan.FromSeconds(15))
|
||||
{
|
||||
using (LogContext.PushProperty("Server", server.ToString()))
|
||||
{
|
||||
_logger.LogDebug("Not getting DVar because match recently ended");
|
||||
}
|
||||
|
||||
OnComplete(new AsyncResult
|
||||
{
|
||||
IsCompleted = false,
|
||||
AsyncState = (false, (string)null)
|
||||
});
|
||||
}
|
||||
|
||||
using 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(() =>
|
||||
{
|
||||
if (DateTime.Now - (server.MatchEndTime ?? server.MatchStartTime) < TimeSpan.FromSeconds(15))
|
||||
{
|
||||
using (LogContext.PushProperty("Server", server.ToString()))
|
||||
{
|
||||
_logger.LogDebug("Not setting DVar because match recently ended");
|
||||
}
|
||||
|
||||
OnComplete(new AsyncResult
|
||||
{
|
||||
IsCompleted = false,
|
||||
AsyncState = false
|
||||
});
|
||||
}
|
||||
|
||||
using 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();
|
||||
}
|
||||
|
||||
private T WrapJavaScriptErrorHandling<T>(Func<T> work, object additionalData = null, Server server = null,
|
||||
[CallerMemberName] string methodName = "")
|
||||
{
|
130
Application/Plugin/Script/ScriptPluginConfigurationWrapper.cs
Normal file
130
Application/Plugin/Script/ScriptPluginConfigurationWrapper.cs
Normal file
@ -0,0 +1,130 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
32
Application/Plugin/Script/ScriptPluginFactory.cs
Normal file
32
Application/Plugin/Script/ScriptPluginFactory.cs
Normal file
@ -0,0 +1,32 @@
|
||||
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>());
|
||||
}
|
||||
}
|
143
Application/Plugin/Script/ScriptPluginHelper.cs
Normal file
143
Application/Plugin/Script/ScriptPluginHelper.cs
Normal file
@ -0,0 +1,143 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
namespace IW4MAdmin.Application.Plugin.Script
|
||||
{
|
||||
/// <summary>
|
||||
/// implementation of IScriptPluginServiceResolver
|
||||
@ -25,7 +25,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
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.Misc
|
||||
{
|
||||
var typeCollection = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(t => t.GetTypes());
|
||||
string generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
|
||||
var serviceType = typeCollection.FirstOrDefault(_type => _type.Name.ToLower() == generatedName);
|
||||
var generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
|
||||
var serviceType = typeCollection.FirstOrDefault(type => type.Name.ToLower() == generatedName);
|
||||
|
||||
if (serviceType == null)
|
||||
{
|
@ -6,8 +6,9 @@ using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc;
|
||||
namespace IW4MAdmin.Application.Plugin.Script;
|
||||
|
||||
[Obsolete("This architecture is superseded by the request notify delay architecture")]
|
||||
public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
|
||||
{
|
||||
private Timer _timer;
|
597
Application/Plugin/Script/ScriptPluginV2.cs
Normal file
597
Application/Plugin/Script/ScriptPluginV2.cs
Normal file
@ -0,0 +1,597 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
6
Application/Plugin/Script/ScriptPluginWebRequest.cs
Normal file
6
Application/Plugin/Script/ScriptPluginWebRequest.cs
Normal file
@ -0,0 +1,6 @@
|
||||
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);
|
@ -8,9 +8,11 @@ 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;
|
||||
|
||||
@ -18,6 +20,7 @@ namespace IW4MAdmin.Application.QueryHelpers;
|
||||
|
||||
public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequest, ClientResourceResponse>
|
||||
{
|
||||
public ApplicationConfiguration _appConfig { get; }
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly IGeoLocationService _geoLocationService;
|
||||
|
||||
@ -27,8 +30,10 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
|
||||
public EFAlias Alias { get; set; }
|
||||
}
|
||||
|
||||
public ClientResourceQueryHelper(IDatabaseContextFactory contextFactory, IGeoLocationService geoLocationService)
|
||||
public ClientResourceQueryHelper(IDatabaseContextFactory contextFactory, IGeoLocationService geoLocationService,
|
||||
ApplicationConfiguration appConfig)
|
||||
{
|
||||
_appConfig = appConfig;
|
||||
_contextFactory = contextFactory;
|
||||
_geoLocationService = geoLocationService;
|
||||
}
|
||||
@ -75,7 +80,9 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.ClientIp))
|
||||
{
|
||||
clientAliases = SearchByIp(query, clientAliases);
|
||||
clientAliases = SearchByIp(query, clientAliases,
|
||||
_appConfig.HasPermission(query.RequesterPermission, WebfrontEntity.ClientIPAddress,
|
||||
WebfrontPermission.Read));
|
||||
}
|
||||
|
||||
var iqGroupedClientAliases = clientAliases.GroupBy(a => new { a.Client.ClientId, a.Client.LastConnection });
|
||||
@ -142,12 +149,12 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
|
||||
});
|
||||
}
|
||||
|
||||
private static Func<IGrouping<int, ClientResourceResponse>, DateTime> SearchByAliasLocal(string? clientName,
|
||||
string? ipAddress)
|
||||
private static Func<IGrouping<int, ClientResourceResponse>, DateTime> SearchByAliasLocal(string clientName,
|
||||
string ipAddress)
|
||||
{
|
||||
return group =>
|
||||
{
|
||||
ClientResourceResponse? match = null;
|
||||
ClientResourceResponse match = null;
|
||||
var lowercaseClientName = clientName?.ToLower();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(lowercaseClientName))
|
||||
@ -203,7 +210,7 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
|
||||
}
|
||||
|
||||
private static IQueryable<ClientAlias> SearchByIp(ClientResourceRequest query,
|
||||
IQueryable<ClientAlias> clientAliases)
|
||||
IQueryable<ClientAlias> clientAliases, bool canSearchIP)
|
||||
{
|
||||
var ipString = query.ClientIp.Trim();
|
||||
var ipAddress = ipString.ConvertToIP();
|
||||
@ -213,7 +220,7 @@ public class ClientResourceQueryHelper : IResourceQueryHelper<ClientResourceRequ
|
||||
clientAliases = clientAliases.Where(clientAlias =>
|
||||
clientAlias.Alias.IPAddress != null && clientAlias.Alias.IPAddress == ipAddress);
|
||||
}
|
||||
else
|
||||
else if(canSearchIP)
|
||||
{
|
||||
clientAliases = clientAliases.Where(clientAlias =>
|
||||
EF.Functions.Like(clientAlias.Alias.SearchableIPAddress, $"{ipString}%"));
|
||||
|
@ -194,10 +194,14 @@ 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))
|
||||
{
|
||||
value = regex.Groups[parserRegex.GroupMapping[groupType]].ToString();
|
||||
continue;
|
||||
}
|
||||
|
||||
value = regex.Groups[parserRegex.GroupMapping[groupType]].ToString();
|
||||
break;
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
@ -304,7 +308,7 @@ namespace IW4MAdmin.Application.RConParsers
|
||||
{
|
||||
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
|
||||
|
||||
networkId = networkIdString.IsBotGuid() || (ip == null && ping == 999) ?
|
||||
networkId = networkIdString.IsBotGuid() || (ip == null && ping is 999 or 0) ?
|
||||
name.GenerateGuidFromString() :
|
||||
networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,10 +11,11 @@ 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>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
|
||||
void SetCacheItem(Func<DbSet<TEntityType>, IEnumerable<object>, 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, object id = null, CancellationToken token = default);
|
||||
|
||||
Task<TReturnType> GetCacheItem(string keyName, IEnumerable<object> ids = null, CancellationToken token = default);
|
||||
}
|
||||
}
|
||||
|
@ -131,6 +131,7 @@ 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);
|
||||
});
|
||||
@ -152,6 +153,8 @@ 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");
|
||||
|
@ -18,7 +18,7 @@ namespace Data.Helpers
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
|
||||
private readonly object _defaultKey = new();
|
||||
private readonly string _defaultKey = null;
|
||||
|
||||
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>, CancellationToken, Task<TCacheType>> Getter { get; set; }
|
||||
public Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TCacheType>> Getter { get; set; }
|
||||
public TCacheType Value { get; set; }
|
||||
public bool IsSet { get; set; }
|
||||
|
||||
@ -53,53 +53,58 @@ namespace Data.Helpers
|
||||
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||
TimeSpan? expirationTime = null, bool autoRefresh = false)
|
||||
{
|
||||
SetCacheItem(getter, key, null, expirationTime, autoRefresh);
|
||||
SetCacheItem((set, _, token) => getter(set, token), key, null, expirationTime, autoRefresh);
|
||||
}
|
||||
|
||||
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||
public void SetCacheItem(Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
|
||||
{
|
||||
ids ??= new[] { _defaultKey };
|
||||
|
||||
|
||||
if (!_cacheStates.ContainsKey(key))
|
||||
{
|
||||
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
|
||||
}
|
||||
|
||||
foreach (var id in ids)
|
||||
var cacheInstance = _cacheStates[key];
|
||||
var id = GenerateKeyFromIds(ids);
|
||||
|
||||
lock (_cacheStates)
|
||||
{
|
||||
if (_cacheStates[key].ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var state = new CacheState<TReturnType>
|
||||
{
|
||||
Key = key,
|
||||
Getter = getter,
|
||||
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
|
||||
};
|
||||
|
||||
_cacheStates[key].Add(id, state);
|
||||
|
||||
_autoRefresh = autoRefresh;
|
||||
|
||||
|
||||
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
|
||||
if (cacheInstance.ContainsKey(id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
|
||||
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
var state = new CacheState<TReturnType>
|
||||
{
|
||||
Key = key,
|
||||
Getter = getter,
|
||||
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
|
||||
};
|
||||
|
||||
lock (_cacheStates)
|
||||
{
|
||||
cacheInstance.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.Start();
|
||||
}
|
||||
|
||||
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
|
||||
await GetCacheItem(keyName, null, cancellationToken);
|
||||
public Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
|
||||
GetCacheItem(keyName, null, cancellationToken);
|
||||
|
||||
public async Task<TReturnType> GetCacheItem(string keyName, object id = null,
|
||||
public async Task<TReturnType> GetCacheItem(string keyName, IEnumerable<object> ids = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_cacheStates.ContainsKey(keyName))
|
||||
@ -107,26 +112,33 @@ namespace Data.Helpers
|
||||
throw new ArgumentException("No cache found for key {key}", keyName);
|
||||
}
|
||||
|
||||
var state = id is null ? _cacheStates[keyName].Values.First() : _cacheStates[keyName][id];
|
||||
var cacheInstance = _cacheStates[keyName];
|
||||
|
||||
CacheState<TReturnType> state;
|
||||
|
||||
lock (_cacheStates)
|
||||
{
|
||||
state = ids is null ? cacheInstance.Values.First() : _cacheStates[keyName][GenerateKeyFromIds(ids)];
|
||||
}
|
||||
|
||||
// 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, cancellationToken);
|
||||
await RunCacheUpdate(state, ids, cancellationToken);
|
||||
}
|
||||
|
||||
return state.Value;
|
||||
}
|
||||
|
||||
private async Task RunCacheUpdate(CacheState<TReturnType> state, CancellationToken token)
|
||||
|
||||
private async Task RunCacheUpdate(CacheState<TReturnType> state, IEnumerable<object> ids, 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, token);
|
||||
var value = await state.Getter(set, ids, token);
|
||||
state.Value = value;
|
||||
state.IsSet = true;
|
||||
state.LastRetrieval = DateTime.Now;
|
||||
@ -136,5 +148,8 @@ 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"));
|
||||
}
|
||||
}
|
||||
|
@ -8,107 +8,94 @@ 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
|
||||
{
|
||||
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);
|
||||
|
||||
public LookupCache(ILogger<LookupCache<T>> logger, IDatabaseContextFactory contextFactory)
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private Dictionary<long, T> _cachedItems;
|
||||
private readonly SemaphoreSlim _onOperation = new SemaphoreSlim(1, 1);
|
||||
_logger = logger;
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
public LookupCache(ILogger<LookupCache<T>> logger, IDatabaseContextFactory contextFactory)
|
||||
public async Task<T> AddAsync(T item)
|
||||
{
|
||||
await _onOperation.WaitAsync();
|
||||
T existingItem = null;
|
||||
|
||||
if (_cachedItems.ContainsKey(item.Id))
|
||||
{
|
||||
_logger = logger;
|
||||
_contextFactory = contextFactory;
|
||||
existingItem = _cachedItems[item.Id];
|
||||
}
|
||||
|
||||
public async Task<T> AddAsync(T item)
|
||||
if (existingItem != null)
|
||||
{
|
||||
await _onOperation.WaitAsync();
|
||||
T existingItem = null;
|
||||
_logger.LogDebug("Cached item already added for {Type} {Id} {Value}", typeof(T).Name, item.Id,
|
||||
item.Value);
|
||||
_onOperation.Release();
|
||||
return existingItem;
|
||||
}
|
||||
|
||||
if (_cachedItems.ContainsKey(item.Id))
|
||||
try
|
||||
{
|
||||
_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();
|
||||
_cachedItems.Add(item.Id, item);
|
||||
return item;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not add item to cache for {Type}", typeof(T).Name);
|
||||
throw new Exception("Could not add item to cache");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onOperation.CurrentCount == 0)
|
||||
{
|
||||
existingItem = _cachedItems[item.Id];
|
||||
}
|
||||
|
||||
if (existingItem != null)
|
||||
{
|
||||
_logger.LogDebug("Cached item already added for {type} {id} {value}", typeof(T).Name, item.Id,
|
||||
item.Value);
|
||||
_onOperation.Release();
|
||||
return existingItem;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_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();
|
||||
_cachedItems.Add(item.Id, item);
|
||||
return item;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not add item to cache for {type}", typeof(T).Name);
|
||||
throw new Exception("Could not add item to cache");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onOperation.CurrentCount == 0)
|
||||
{
|
||||
_onOperation.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> FirstAsync(Func<T, bool> query)
|
||||
{
|
||||
await _onOperation.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var cachedResult = _cachedItems.Values.Where(query);
|
||||
|
||||
if (cachedResult.Any())
|
||||
{
|
||||
return cachedResult.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
finally
|
||||
{
|
||||
if (_onOperation.CurrentCount == 0)
|
||||
{
|
||||
_onOperation.Release(1);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetAll()
|
||||
{
|
||||
return _cachedItems.Values;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
_cachedItems = await context.Set<T>().ToDictionaryAsync(item => item.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not initialize caching for {cacheType}", typeof(T).Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> FirstAsync(Func<T, bool> query)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _onOperation.WaitAsync();
|
||||
var cachedResult = _cachedItems.Values.Where(query);
|
||||
return cachedResult.FirstOrDefault();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onOperation.CurrentCount == 0)
|
||||
{
|
||||
_onOperation.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetAll()
|
||||
{
|
||||
return _cachedItems.Values;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
_cachedItems = await context.Set<T>().ToDictionaryAsync(item => item.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not initialize caching for {CacheType}", typeof(T).Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -808,7 +808,8 @@ namespace Data.Migrations.MySql
|
||||
|
||||
b.Property<string>("SearchableIPAddress")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("longtext")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("varchar(255)")
|
||||
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
||||
|
||||
b.Property<string>("SearchableName")
|
||||
|
@ -11,7 +11,8 @@ namespace Data.Migrations.MySql
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SearchableIPAddress",
|
||||
table: "EFAlias",
|
||||
type: "longtext",
|
||||
type: "varchar(255)",
|
||||
maxLength: 255,
|
||||
nullable: true,
|
||||
computedColumnSql: "CONCAT((IPAddress & 255), \".\", ((IPAddress >> 8) & 255), \".\", ((IPAddress >> 16) & 255), \".\", ((IPAddress >> 24) & 255))",
|
||||
stored: true)
|
||||
|
@ -808,6 +808,7 @@ 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);
|
||||
|
||||
|
1644
Data/Migrations/MySql/20230705133025_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
1644
Data/Migrations/MySql/20230705133025_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -814,6 +814,7 @@ 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);
|
||||
|
||||
@ -1110,6 +1111,8 @@ namespace Data.Migrations.MySql
|
||||
|
||||
b.HasKey("ServerSnapshotId");
|
||||
|
||||
b.HasIndex("CapturedAt");
|
||||
|
||||
b.HasIndex("MapId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
1701
Data/Migrations/Postgresql/20230705133135_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
1701
Data/Migrations/Postgresql/20230705133135_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,52 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -853,7 +853,8 @@ namespace Data.Migrations.Postgresql
|
||||
|
||||
b.Property<string>("SearchableIPAddress")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("text")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)")
|
||||
.HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true);
|
||||
|
||||
b.Property<string>("SearchableName")
|
||||
@ -1163,6 +1164,8 @@ namespace Data.Migrations.Postgresql
|
||||
|
||||
b.HasKey("ServerSnapshotId");
|
||||
|
||||
b.HasIndex("CapturedAt");
|
||||
|
||||
b.HasIndex("MapId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
1642
Data/Migrations/Sqlite/20230705132822_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
1642
Data/Migrations/Sqlite/20230705132822_AddIndexToEFServerSnapshotCapturedAt.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -812,6 +812,7 @@ 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);
|
||||
|
||||
@ -1108,6 +1109,8 @@ namespace Data.Migrations.Sqlite
|
||||
|
||||
b.HasKey("ServerSnapshotId");
|
||||
|
||||
b.HasIndex("CapturedAt");
|
||||
|
||||
b.HasIndex("MapId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
@ -16,7 +16,8 @@
|
||||
T7 = 8,
|
||||
SHG1 = 9,
|
||||
CSGO = 10,
|
||||
H1 = 11
|
||||
H1 = 11,
|
||||
L4D2 = 12,
|
||||
}
|
||||
|
||||
public enum ConnectionType
|
||||
|
@ -6,6 +6,7 @@ trigger:
|
||||
include:
|
||||
- release/pre
|
||||
- master
|
||||
- develop
|
||||
|
||||
pr: none
|
||||
|
||||
@ -20,227 +21,233 @@ variables:
|
||||
buildConfiguration: Stable
|
||||
isPreRelease: false
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .NET Core 6 SDK'
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '6.0.x'
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: NuGetToolInstaller@1
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: 'Setup Pre-Release configuration'
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/release/pre')
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
echo '##vso[task.setvariable variable=releaseType]prerelease'
|
||||
echo '##vso[task.setvariable variable=buildConfiguration]Prerelease'
|
||||
echo '##vso[task.setvariable variable=isPreRelease]true'
|
||||
failOnStderr: true
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Restore nuget packages'
|
||||
inputs:
|
||||
restoreSolution: '$(solution)'
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: 'Preload external resources'
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
Write-Host 'Build Configuration is $(buildConfiguration), Release Type is $(releaseType)'
|
||||
md -Force lib\open-iconic\font\css
|
||||
wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o lib\open-iconic\font\css\open-iconic-bootstrap-override.scss
|
||||
cd lib\open-iconic\font\css
|
||||
(Get-Content open-iconic-bootstrap-override.scss).replace('../fonts/', '/font/') | Set-Content open-iconic-bootstrap-override.scss
|
||||
failOnStderr: true
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore\wwwroot'
|
||||
|
||||
- task: VSBuild@1
|
||||
displayName: 'Build projects'
|
||||
inputs:
|
||||
solution: '$(solution)'
|
||||
msbuildArgs: '/p:DeployOnBuild=false /p:PackageAsSingleFile=false /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)" /p:Version=$(Build.BuildNumber)'
|
||||
platform: '$(buildPlatform)'
|
||||
configuration: '$(buildConfiguration)'
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: 'Bundle JS Files'
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
Write-Host 'Getting dotnet bundle'
|
||||
wget http://raidmax.org/IW4MAdmin/res/dotnet-bundle.zip -o $(Build.Repository.LocalPath)\dotnet-bundle.zip
|
||||
Write-Host 'Unzipping download'
|
||||
Expand-Archive -LiteralPath $(Build.Repository.LocalPath)\dotnet-bundle.zip -DestinationPath $(Build.Repository.LocalPath)
|
||||
Write-Host 'Executing dotnet-bundle'
|
||||
$(Build.Repository.LocalPath)\dotnet-bundle.exe clean $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json
|
||||
$(Build.Repository.LocalPath)\dotnet-bundle.exe $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json
|
||||
failOnStderr: true
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Publish projects'
|
||||
inputs:
|
||||
command: 'publish'
|
||||
publishWebProjects: false
|
||||
projects: |
|
||||
**/WebfrontCore.csproj
|
||||
**/Application.csproj
|
||||
arguments: '-c $(buildConfiguration) -o $(outputFolder) /p:Version=$(Build.BuildNumber)'
|
||||
zipAfterPublish: false
|
||||
modifyOutputPath: false
|
||||
jobs:
|
||||
- job: Build_Deploy
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .NET Core 6 SDK'
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '6.0.x'
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: 'Run publish script 1'
|
||||
inputs:
|
||||
filePath: 'DeploymentFiles/PostPublish.ps1'
|
||||
arguments: '$(outputFolder)'
|
||||
failOnStderr: true
|
||||
workingDirectory: '$(Build.Repository.LocalPath)'
|
||||
|
||||
- task: BatchScript@1
|
||||
displayName: 'Run publish script 2'
|
||||
inputs:
|
||||
filename: 'Application\BuildScripts\PostPublish.bat'
|
||||
workingFolder: '$(Build.Repository.LocalPath)'
|
||||
arguments: '$(outputFolder) $(Build.Repository.LocalPath)'
|
||||
failOnStandardError: true
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: 'Download dos2unix for line endings'
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: 'wget https://raidmax.org/downloads/dos2unix.exe'
|
||||
failOnStderr: true
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: 'Convert Linux start script line endings'
|
||||
inputs:
|
||||
script: |
|
||||
echo changing to encoding for linux start script
|
||||
dos2unix $(outputFolder)\StartIW4MAdmin.sh
|
||||
dos2unix $(outputFolder)\UpdateIW4MAdmin.sh
|
||||
echo creating website version filename
|
||||
@echo IW4MAdmin-$(Build.BuildNumber) > $(Build.ArtifactStagingDirectory)\version_$(releaseType).txt
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Move script plugins into publish directory'
|
||||
inputs:
|
||||
SourceFolder: '$(Build.Repository.LocalPath)\Plugins\ScriptPlugins'
|
||||
Contents: '*.js'
|
||||
TargetFolder: '$(outputFolder)\Plugins'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Move binary plugins into publish directory'
|
||||
inputs:
|
||||
SourceFolder: '$(Build.Repository.LocalPath)\BUILD\Plugins\'
|
||||
Contents: '*.dll'
|
||||
TargetFolder: '$(outputFolder)\Plugins'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: 'Move webfront resources into publish directory'
|
||||
inputs:
|
||||
script: 'xcopy /s /y /f wwwroot $(outputFolder)\wwwroot'
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\BUILD\Plugins'
|
||||
failOnStderr: true
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: 'Move gamescript files into publish directory'
|
||||
inputs:
|
||||
script: 'echo d | xcopy /s /y /f GameFiles $(outputFolder)\GameFiles'
|
||||
workingDirectory: '$(Build.Repository.LocalPath)'
|
||||
failOnStderr: true
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Generate final zip file'
|
||||
inputs:
|
||||
rootFolderOrFile: '$(outputFolder)'
|
||||
includeRootFolder: false
|
||||
archiveType: 'zip'
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip'
|
||||
replaceExistingArchive: true
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
targetPath: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip'
|
||||
artifact: 'IW4MAdmin-$(Build.BuildNumber).zip'
|
||||
|
||||
- task: FtpUpload@2
|
||||
displayName: 'Upload zip file to website'
|
||||
inputs:
|
||||
credentialsOption: 'inputs'
|
||||
serverUrl: '$(FTPUrl)'
|
||||
username: '$(FTPUsername)'
|
||||
password: '$(FTPPassword)'
|
||||
rootDirectory: '$(Build.ArtifactStagingDirectory)'
|
||||
filePatterns: '*.zip'
|
||||
remoteDirectory: 'IW4MAdmin/Download'
|
||||
clean: false
|
||||
cleanContents: false
|
||||
preservePaths: false
|
||||
trustSSL: false
|
||||
|
||||
- task: FtpUpload@2
|
||||
displayName: 'Upload version info to website'
|
||||
inputs:
|
||||
credentialsOption: 'inputs'
|
||||
serverUrl: '$(FTPUrl)'
|
||||
username: '$(FTPUsername)'
|
||||
password: '$(FTPPassword)'
|
||||
rootDirectory: '$(Build.ArtifactStagingDirectory)'
|
||||
filePatterns: 'version_$(releaseType).txt'
|
||||
remoteDirectory: 'IW4MAdmin'
|
||||
clean: false
|
||||
cleanContents: false
|
||||
preservePaths: false
|
||||
trustSSL: false
|
||||
|
||||
- task: GitHubRelease@1
|
||||
displayName: 'Make GitHub release'
|
||||
inputs:
|
||||
gitHubConnection: 'github.com_RaidMax'
|
||||
repositoryName: 'RaidMax/IW4M-Admin'
|
||||
action: 'create'
|
||||
target: '$(Build.SourceVersion)'
|
||||
tagSource: 'userSpecifiedTag'
|
||||
tag: '$(Build.BuildNumber)-$(releaseType)'
|
||||
title: 'IW4MAdmin $(Build.BuildNumber) ($(releaseType))'
|
||||
assets: '$(Build.ArtifactStagingDirectory)/*.zip'
|
||||
isPreRelease: $(isPreRelease)
|
||||
releaseNotesSource: 'inline'
|
||||
releaseNotesInline: 'todo'
|
||||
changeLogCompareToRelease: 'lastNonDraftRelease'
|
||||
changeLogType: 'commitBased'
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: 'Update master version'
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
$payload = @{
|
||||
'current-version-$(releaseType)' = '$(Build.BuildNumber)'
|
||||
'jwt-secret' = '$(JWTSecret)'
|
||||
} | ConvertTo-Json
|
||||
|
||||
- task: NuGetToolInstaller@1
|
||||
|
||||
- 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'))
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
echo '##vso[task.setvariable variable=releaseType]prerelease'
|
||||
echo '##vso[task.setvariable variable=buildConfiguration]Prerelease'
|
||||
echo '##vso[task.setvariable variable=isPreRelease]true'
|
||||
failOnStderr: true
|
||||
|
||||
$params = @{
|
||||
Uri = 'http://api.raidmax.org:5000/version'
|
||||
Method = 'POST'
|
||||
Body = $payload
|
||||
ContentType = 'application/json'
|
||||
}
|
||||
|
||||
Invoke-RestMethod @params
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Restore nuget packages'
|
||||
inputs:
|
||||
restoreSolution: '$(solution)'
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: 'Preload external resources'
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
Write-Host 'Build Configuration is $(buildConfiguration), Release Type is $(releaseType)'
|
||||
md -Force lib\open-iconic\font\css
|
||||
wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o lib\open-iconic\font\css\open-iconic-bootstrap-override.scss
|
||||
cd lib\open-iconic\font\css
|
||||
(Get-Content open-iconic-bootstrap-override.scss).replace('../fonts/', '/font/') | Set-Content open-iconic-bootstrap-override.scss
|
||||
failOnStderr: true
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore\wwwroot'
|
||||
|
||||
- task: VSBuild@1
|
||||
displayName: 'Build projects'
|
||||
inputs:
|
||||
solution: '$(solution)'
|
||||
msbuildArgs: '/p:DeployOnBuild=false /p:PackageAsSingleFile=false /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)" /p:Version=$(Build.BuildNumber)'
|
||||
platform: '$(buildPlatform)'
|
||||
configuration: '$(buildConfiguration)'
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: 'Bundle JS Files'
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
Write-Host 'Getting dotnet bundle'
|
||||
wget http://raidmax.org/IW4MAdmin/res/dotnet-bundle.zip -o $(Build.Repository.LocalPath)\dotnet-bundle.zip
|
||||
Write-Host 'Unzipping download'
|
||||
Expand-Archive -LiteralPath $(Build.Repository.LocalPath)\dotnet-bundle.zip -DestinationPath $(Build.Repository.LocalPath)
|
||||
Write-Host 'Executing dotnet-bundle'
|
||||
$(Build.Repository.LocalPath)\dotnet-bundle.exe clean $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json
|
||||
$(Build.Repository.LocalPath)\dotnet-bundle.exe $(Build.Repository.LocalPath)\WebfrontCore\bundleconfig.json
|
||||
failOnStderr: true
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Publish projects'
|
||||
inputs:
|
||||
command: 'publish'
|
||||
publishWebProjects: false
|
||||
projects: |
|
||||
**/WebfrontCore.csproj
|
||||
**/Application.csproj
|
||||
arguments: '-c $(buildConfiguration) -o $(outputFolder) /p:Version=$(Build.BuildNumber)'
|
||||
zipAfterPublish: false
|
||||
modifyOutputPath: false
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: 'Run publish script 1'
|
||||
inputs:
|
||||
filePath: 'DeploymentFiles/PostPublish.ps1'
|
||||
arguments: '$(outputFolder)'
|
||||
failOnStderr: true
|
||||
workingDirectory: '$(Build.Repository.LocalPath)'
|
||||
|
||||
- task: BatchScript@1
|
||||
displayName: 'Run publish script 2'
|
||||
inputs:
|
||||
filename: 'Application\BuildScripts\PostPublish.bat'
|
||||
workingFolder: '$(Build.Repository.LocalPath)'
|
||||
arguments: '$(outputFolder) $(Build.Repository.LocalPath)'
|
||||
failOnStandardError: true
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: 'Download dos2unix for line endings'
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: 'wget https://raidmax.org/downloads/dos2unix.exe'
|
||||
failOnStderr: true
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: 'Convert Linux start script line endings'
|
||||
inputs:
|
||||
script: |
|
||||
echo changing to encoding for linux start script
|
||||
dos2unix $(outputFolder)\StartIW4MAdmin.sh
|
||||
dos2unix $(outputFolder)\UpdateIW4MAdmin.sh
|
||||
echo creating website version filename
|
||||
@echo IW4MAdmin-$(Build.BuildNumber) > $(Build.ArtifactStagingDirectory)\version_$(releaseType).txt
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Move script plugins into publish directory'
|
||||
inputs:
|
||||
SourceFolder: '$(Build.Repository.LocalPath)\Plugins\ScriptPlugins'
|
||||
Contents: '*.js'
|
||||
TargetFolder: '$(outputFolder)\Plugins'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Move binary plugins into publish directory'
|
||||
inputs:
|
||||
SourceFolder: '$(Build.Repository.LocalPath)\BUILD\Plugins\'
|
||||
Contents: '*.dll'
|
||||
TargetFolder: '$(outputFolder)\Plugins'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: 'Move webfront resources into publish directory'
|
||||
inputs:
|
||||
script: 'xcopy /s /y /f wwwroot $(outputFolder)\wwwroot'
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\BUILD\Plugins'
|
||||
failOnStderr: true
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: 'Move gamescript files into publish directory'
|
||||
inputs:
|
||||
script: 'echo d | xcopy /s /y /f GameFiles $(outputFolder)\GameFiles'
|
||||
workingDirectory: '$(Build.Repository.LocalPath)'
|
||||
failOnStderr: true
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: 'Publish artifact for analysis'
|
||||
inputs:
|
||||
targetPath: '$(outputFolder)'
|
||||
artifact: 'IW4MAdmin.$(buildConfiguration)'
|
||||
publishLocation: 'pipeline'
|
||||
- task: ArchiveFiles@2
|
||||
displayName: 'Generate final zip file'
|
||||
inputs:
|
||||
rootFolderOrFile: '$(outputFolder)'
|
||||
includeRootFolder: false
|
||||
archiveType: 'zip'
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip'
|
||||
replaceExistingArchive: true
|
||||
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
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'
|
||||
serverUrl: '$(FTPUrl)'
|
||||
username: '$(FTPUsername)'
|
||||
password: '$(FTPPassword)'
|
||||
rootDirectory: '$(Build.ArtifactStagingDirectory)'
|
||||
filePatterns: '*.zip'
|
||||
remoteDirectory: 'IW4MAdmin/Download'
|
||||
clean: false
|
||||
cleanContents: false
|
||||
preservePaths: false
|
||||
trustSSL: false
|
||||
|
||||
- task: FtpUpload@2
|
||||
condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop')
|
||||
displayName: 'Upload version info to website'
|
||||
inputs:
|
||||
credentialsOption: 'inputs'
|
||||
serverUrl: '$(FTPUrl)'
|
||||
username: '$(FTPUsername)'
|
||||
password: '$(FTPPassword)'
|
||||
rootDirectory: '$(Build.ArtifactStagingDirectory)'
|
||||
filePatterns: 'version_$(releaseType).txt'
|
||||
remoteDirectory: 'IW4MAdmin'
|
||||
clean: false
|
||||
cleanContents: false
|
||||
preservePaths: false
|
||||
trustSSL: false
|
||||
|
||||
- task: GitHubRelease@1
|
||||
condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop')
|
||||
displayName: 'Make GitHub release'
|
||||
inputs:
|
||||
gitHubConnection: 'github.com_RaidMax'
|
||||
repositoryName: 'RaidMax/IW4M-Admin'
|
||||
action: 'create'
|
||||
target: '$(Build.SourceVersion)'
|
||||
tagSource: 'userSpecifiedTag'
|
||||
tag: '$(Build.BuildNumber)-$(releaseType)'
|
||||
title: 'IW4MAdmin $(Build.BuildNumber) ($(releaseType))'
|
||||
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)'
|
||||
changeLogCompareToRelease: 'lastNonDraftRelease'
|
||||
changeLogType: 'commitBased'
|
||||
|
||||
- task: PowerShell@2
|
||||
condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop')
|
||||
displayName: 'Update master version'
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
$payload = @{
|
||||
'current-version-$(releaseType)' = '$(Build.BuildNumber)'
|
||||
'jwt-secret' = '$(JWTSecret)'
|
||||
} | ConvertTo-Json
|
||||
|
||||
|
||||
$params = @{
|
||||
Uri = 'http://api.raidmax.org:5000/version'
|
||||
Method = 'POST'
|
||||
Body = $payload
|
||||
ContentType = 'application/json'
|
||||
}
|
||||
|
||||
Invoke-RestMethod @params
|
||||
|
@ -41,11 +41,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();
|
||||
|
@ -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();
|
||||
|
Binary file not shown.
@ -1,264 +0,0 @@
|
||||
#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]]();
|
||||
}
|
@ -1,16 +1,14 @@
|
||||
#include common_scripts\utility;
|
||||
#include maps\mp\_utility;
|
||||
#include maps\mp\gametypes\_hud_util;
|
||||
|
||||
Init()
|
||||
{
|
||||
level thread Setup();
|
||||
thread Setup();
|
||||
}
|
||||
|
||||
Setup()
|
||||
{
|
||||
level endon( "game_ended" );
|
||||
|
||||
|
||||
// setup default vars
|
||||
level.eventBus = spawnstruct();
|
||||
level.eventBus.inVar = "sv_iw4madmin_in";
|
||||
@ -18,28 +16,54 @@ Setup()
|
||||
level.eventBus.failKey = "fail";
|
||||
level.eventBus.timeoutKey = "timeout";
|
||||
level.eventBus.timeout = 30;
|
||||
|
||||
level.commonFunctions = spawnstruct();
|
||||
level.commonFunctions.setDvar = "SetDvarIfUninitialized";
|
||||
|
||||
|
||||
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.notifyTypes = spawnstruct();
|
||||
level.notifyTypes.gameFunctionsInitialized = "GameFunctionsInitialized";
|
||||
level.notifyTypes.sharedFunctionsInitialized = "SharedFunctionsInitialized";
|
||||
level.notifyTypes.integrationBootstrapInitialized = "IntegrationBootstrapInitialized";
|
||||
|
||||
|
||||
level.clientDataKey = "clientData";
|
||||
|
||||
level.eventTypes = spawnstruct();
|
||||
level.eventTypes.localClientEvent = "client_event";
|
||||
level.eventTypes.eventAvailable = "EventAvailable";
|
||||
level.eventTypes.clientDataReceived = "ClientDataReceived";
|
||||
level.eventTypes.clientDataRequested = "ClientDataRequested";
|
||||
level.eventTypes.setClientDataRequested = "SetClientDataRequested";
|
||||
level.eventTypes.setClientDataCompleted = "SetClientDataCompleted";
|
||||
level.eventTypes.executeCommandRequested = "ExecuteCommandRequested";
|
||||
|
||||
|
||||
level.iw4madminIntegrationDebug = 0;
|
||||
|
||||
|
||||
// map the event type to the handler
|
||||
level.eventCallbacks = [];
|
||||
level.eventCallbacks[level.eventTypes.clientDataReceived] = ::OnClientDataReceived;
|
||||
@ -49,177 +73,71 @@ Setup()
|
||||
level.clientCommandCallbacks = [];
|
||||
level.clientCommandRusAsTarget = [];
|
||||
level.logger = spawnstruct();
|
||||
level.overrideMethods = [];
|
||||
|
||||
level.iw4madminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" );
|
||||
InitializeLogger();
|
||||
|
||||
wait ( 0.05 ); // needed to give script engine time to propagate notifies
|
||||
|
||||
|
||||
wait ( 0.05 * 2 ); // needed to give script engine time to propagate notifies
|
||||
|
||||
level notify( level.notifyTypes.integrationBootstrapInitialized );
|
||||
level waittill( level.notifyTypes.gameFunctionsInitialized );
|
||||
|
||||
|
||||
LogDebug( "Integration received notify that game functions are initialized" );
|
||||
|
||||
|
||||
_SetDvarIfUninitialized( level.eventBus.inVar, "" );
|
||||
_SetDvarIfUninitialized( level.eventBus.outVar, "" );
|
||||
_SetDvarIfUninitialized( "sv_iw4madmin_integration_enabled", 1 );
|
||||
_SetDvarIfUninitialized( level.commonKeys.enabled, 1 );
|
||||
_SetDvarIfUninitialized( level.commonKeys.busMode, "rcon" );
|
||||
_SetDvarIfUninitialized( level.commonKeys.busdir, "" );
|
||||
_SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 );
|
||||
|
||||
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
|
||||
_SetDvarIfUninitialized( "GroupSeparatorChar", "" );
|
||||
_SetDvarIfUninitialized( "RecordSeparatorChar", "" );
|
||||
_SetDvarIfUninitialized( "UnitSeparatorChar", "" );
|
||||
|
||||
if ( GetDvarInt( level.commonKeys.enabled ) != 1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// start long running tasks
|
||||
level thread MonitorClientEvents();
|
||||
level thread MonitorBus();
|
||||
level thread OnPlayerConnect();
|
||||
thread MonitorEvents();
|
||||
thread MonitorBus();
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// Client Methods
|
||||
//////////////////////////////////
|
||||
|
||||
OnPlayerConnect()
|
||||
MonitorEvents()
|
||||
{
|
||||
level endon ( "game_ended" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
level waittill( "connected", 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();
|
||||
}
|
||||
}
|
||||
|
||||
OnPlayerSpawned()
|
||||
{
|
||||
self endon( "disconnect" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
self waittill( "spawned_player" );
|
||||
self PlayerSpawnEvents();
|
||||
}
|
||||
}
|
||||
|
||||
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" );
|
||||
level endon( level.eventTypes.gameEnd );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
level waittill( level.eventTypes.localClientEvent, client );
|
||||
level waittill( level.eventTypes.eventAvailable, event );
|
||||
|
||||
LogDebug( "Processing Event " + client.event.type + "-" + client.event.subtype );
|
||||
|
||||
eventHandler = level.eventCallbacks[client.event.type];
|
||||
LogDebug( "Processing Event " + event.type + "-" + event.subtype );
|
||||
|
||||
eventHandler = level.eventCallbacks[event.type];
|
||||
|
||||
if ( IsDefined( eventHandler ) )
|
||||
{
|
||||
client [[eventHandler]]( client.event );
|
||||
LogDebug( "notify client for " + client.event.type );
|
||||
client notify( level.eventTypes.localClientEvent, client.event );
|
||||
if ( IsDefined( event.entity ) )
|
||||
{
|
||||
event.entity [[eventHandler]]( event );
|
||||
}
|
||||
else
|
||||
{
|
||||
[[eventHandler]]( 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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,11 +145,13 @@ MonitorClientEvents()
|
||||
// Helper Methods
|
||||
//////////////////////////////////
|
||||
|
||||
_IsBot( entity )
|
||||
NotImplementedFunction( a, b, c, d, e, f )
|
||||
{
|
||||
// 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"];
|
||||
LogWarning( "Function not implemented" );
|
||||
if ( IsDefined ( a ) )
|
||||
{
|
||||
LogWarning( a );
|
||||
}
|
||||
}
|
||||
|
||||
_SetDvarIfUninitialized( dvarName, dvarValue )
|
||||
@ -239,9 +159,44 @@ _SetDvarIfUninitialized( dvarName, dvarValue )
|
||||
[[level.overrideMethods[level.commonFunctions.setDvar]]]( dvarName, dvarValue );
|
||||
}
|
||||
|
||||
NotImplementedFunction( a, b, c, d, e, f )
|
||||
_GetPlayerFromClientNum( clientNum )
|
||||
{
|
||||
LogWarning( "Function not implemented" );
|
||||
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 );
|
||||
}
|
||||
|
||||
// Not every game can output to console or even game log.
|
||||
@ -262,7 +217,7 @@ _Log( LogLevel, message )
|
||||
{
|
||||
for( i = 0; i < level.logger._logger.size; i++ )
|
||||
{
|
||||
[[level.logger._logger[i]]]( LogLevel, message );
|
||||
[[level.logger._logger[i]]]( LogLevel, GetSubStr( message, 0, 1000 ) );
|
||||
}
|
||||
}
|
||||
|
||||
@ -324,13 +279,13 @@ RegisterLogger( logger )
|
||||
RequestClientMeta( metaKey )
|
||||
{
|
||||
getClientMetaEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "Meta", self, metaKey );
|
||||
level thread QueueEvent( getClientMetaEvent, level.eventTypes.clientDataRequested, self );
|
||||
thread QueueEvent( getClientMetaEvent, level.eventTypes.clientDataRequested, self );
|
||||
}
|
||||
|
||||
RequestClientBasicData()
|
||||
{
|
||||
getClientDataEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "None", self, "" );
|
||||
level thread QueueEvent( getClientDataEvent, level.eventTypes.clientDataRequested, self );
|
||||
thread QueueEvent( getClientDataEvent, level.eventTypes.clientDataRequested, self );
|
||||
}
|
||||
|
||||
IncrementClientMeta( metaKey, incrementValue, clientId )
|
||||
@ -343,51 +298,22 @@ 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 = "key=" + metaKey + "|value=" + metaValue;
|
||||
data = [];
|
||||
data["key"] = metaKey;
|
||||
data["value"] = metaValue;
|
||||
clientNumber = -1;
|
||||
|
||||
if ( IsDefined ( clientId ) )
|
||||
{
|
||||
data = data + "|clientId=" + clientId;
|
||||
data["clientId"] = clientId;
|
||||
clientNumber = -1;
|
||||
}
|
||||
|
||||
if ( IsDefined( direction ) )
|
||||
{
|
||||
data = data + "|direction=" + direction;
|
||||
data["direction"] = direction;
|
||||
}
|
||||
|
||||
if ( IsPlayer( self ) )
|
||||
@ -396,40 +322,7 @@ SetClientMeta( metaKey, metaValue, clientId, direction )
|
||||
}
|
||||
|
||||
setClientMetaEvent = BuildEventRequest( true, level.eventTypes.setClientDataRequested, "Meta", clientNumber, data );
|
||||
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 );
|
||||
thread QueueEvent( setClientMetaEvent, level.eventTypes.setClientDataRequested, self );
|
||||
}
|
||||
|
||||
BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
|
||||
@ -438,79 +331,97 @@ BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
|
||||
{
|
||||
data = "";
|
||||
}
|
||||
|
||||
|
||||
if ( !IsDefined( eventSubtype ) )
|
||||
{
|
||||
eventSubtype = "None";
|
||||
}
|
||||
|
||||
if ( !IsDefined( entOrId ) )
|
||||
{
|
||||
entOrId = "-1";
|
||||
}
|
||||
|
||||
if ( IsPlayer( entOrId ) )
|
||||
{
|
||||
entOrId = entOrId getEntityNumber();
|
||||
}
|
||||
|
||||
|
||||
request = "0";
|
||||
|
||||
|
||||
if ( responseExpected )
|
||||
{
|
||||
request = "1";
|
||||
}
|
||||
|
||||
request = request + ";" + eventType + ";" + eventSubtype + ";" + entOrId + ";" + data;
|
||||
|
||||
data = BuildDataString( data );
|
||||
groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 );
|
||||
request = request + groupSeparator + eventType + groupSeparator + eventSubtype + groupSeparator + entOrId + groupSeparator + data;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
MonitorBus()
|
||||
{
|
||||
level endon( "game_ended" );
|
||||
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, "" );
|
||||
|
||||
for( ;; )
|
||||
{
|
||||
wait ( 0.1 );
|
||||
|
||||
|
||||
// check to see if IW4MAdmin is ready to receive more data
|
||||
if ( getDvar( level.eventBus.inVar ) == "" )
|
||||
inVal = [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation );
|
||||
|
||||
if ( !IsDefined( inVal ) || inVal == "" )
|
||||
{
|
||||
level notify( "bus_ready" );
|
||||
}
|
||||
|
||||
eventString = getDvar( level.eventBus.outVar );
|
||||
|
||||
if ( eventString == "" )
|
||||
|
||||
eventString = [[level.busMethods[level.commonFunctions.getOutboundData]]]( level.eventBus.outLocation );
|
||||
|
||||
if ( !IsDefined( eventString ) || eventString == "" )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
LogDebug( "-> " + eventString );
|
||||
|
||||
NotifyClientEvent( strtok( eventString, ";" ) );
|
||||
|
||||
SetDvar( level.eventBus.outVar, "" );
|
||||
|
||||
groupSeparator = GetSubStr( GetDvar( "GroupSeparatorChar" ), 0, 1 );
|
||||
NotifyEvent( strtok( eventString, groupSeparator ) );
|
||||
|
||||
[[level.busMethods[level.commonFunctions.SetOutboundData]]]( level.eventBus.outLocation, "" );
|
||||
}
|
||||
}
|
||||
|
||||
QueueEvent( request, eventType, notifyEntity )
|
||||
{
|
||||
level endon( "game_ended" );
|
||||
level endon( level.eventTypes.gameEnd );
|
||||
|
||||
start = GetTime();
|
||||
maxWait = level.eventBus.timeout * 1000; // 30 seconds
|
||||
timedOut = "";
|
||||
|
||||
while ( GetDvar( level.eventBus.inVar ) != "" && ( GetTime() - start ) < maxWait )
|
||||
|
||||
while ( [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ) != "" && ( GetTime() - start ) < maxWait )
|
||||
{
|
||||
level [[level.overrideMethods["waittill_notify_or_timeout"]]]( "bus_ready", 1 );
|
||||
level [[level.overrideMethods[level.commonFunctions.waittillNotifyOrTimeout]]]( "bus_ready", 1 );
|
||||
|
||||
if ( GetDvar( level.eventBus.inVar ) != "" )
|
||||
if ( [[level.busMethods[level.commonFunctions.getInboundData]]]( level.eventBus.inLocation ) != "" )
|
||||
{
|
||||
LogDebug( "A request is already in progress..." );
|
||||
timedOut = "set";
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
timedOut = "unset";
|
||||
}
|
||||
|
||||
if ( timedOut == "set")
|
||||
|
||||
if ( timedOut == "set" )
|
||||
{
|
||||
LogDebug( "Timed out waiting for response..." );
|
||||
|
||||
@ -519,14 +430,14 @@ QueueEvent( request, eventType, notifyEntity )
|
||||
notifyEntity NotifyClientEventTimeout( eventType );
|
||||
}
|
||||
|
||||
SetDvar( level.eventBus.inVar, "" );
|
||||
[[level.busMethods[level.commonFunctions.SetInboundData]]]( level.eventBus.inLocation, "" );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LogDebug("<- " + request );
|
||||
|
||||
SetDvar( level.eventBus.inVar, request );
|
||||
|
||||
LogDebug( "<- " + request );
|
||||
|
||||
[[level.busMethods[level.commonFunctions.setInboundData]]]( level.eventBus.inLocation, request );
|
||||
}
|
||||
|
||||
ParseDataString( data )
|
||||
@ -536,23 +447,43 @@ ParseDataString( data )
|
||||
LogDebug( "No data to parse" );
|
||||
return [];
|
||||
}
|
||||
|
||||
dataParts = strtok( data, "|" );
|
||||
|
||||
dataParts = strtok( data, GetSubStr( GetDvar( "RecordSeparatorChar" ), 0, 1 ) );
|
||||
dict = [];
|
||||
|
||||
|
||||
for ( i = 0; i < dataParts.size; i++ )
|
||||
{
|
||||
part = dataParts[i];
|
||||
splitPart = strtok( part, "=" );
|
||||
splitPart = strtok( part, GetSubStr( GetDvar( "UnitSeparatorChar" ), 0, 1 ) );
|
||||
key = splitPart[0];
|
||||
value = splitPart[1];
|
||||
dict[key] = value;
|
||||
dict[i] = key;
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
@ -562,23 +493,18 @@ NotifyClientEventTimeout( eventType )
|
||||
}
|
||||
}
|
||||
|
||||
NotifyClientEvent( eventInfo )
|
||||
NotifyEvent( eventInfo )
|
||||
{
|
||||
origin = getPlayerFromClientNum( int( eventInfo[3] ) );
|
||||
target = getPlayerFromClientNum( int( eventInfo[4] ) );
|
||||
|
||||
origin = [[level.overrideMethods[level.commonFunctions.getPlayerFromClientNum]]]( int( eventInfo[3] ) );
|
||||
target = [[level.overrideMethods[level.commonFunctions.getPlayerFromClientNum]]]( int( eventInfo[4] ) );
|
||||
|
||||
event = spawnstruct();
|
||||
event.type = eventInfo[1];
|
||||
event.subtype = eventInfo[2];
|
||||
event.data = eventInfo[5];
|
||||
event.data = ParseDataString( 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] ) );
|
||||
@ -588,41 +514,15 @@ NotifyClientEvent( eventInfo )
|
||||
LogDebug( "target is null but the slot id is " + int( eventInfo[4] ) );
|
||||
}
|
||||
|
||||
if ( IsDefined( target ) )
|
||||
client = event.origin;
|
||||
|
||||
if ( !IsDefined( client ) )
|
||||
{
|
||||
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" );
|
||||
|
||||
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;
|
||||
event.entity = client;
|
||||
level notify( level.eventTypes.eventAvailable, event );
|
||||
}
|
||||
|
||||
AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
|
||||
@ -631,7 +531,7 @@ AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
level.clientCommandCallbacks[commandName] = callback;
|
||||
level.clientCommandRusAsTarget[commandName] = shouldRunAsTarget == true; //might speed up things later in case someone gives us a string or number instead of a boolean
|
||||
}
|
||||
@ -642,7 +542,7 @@ AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
|
||||
|
||||
OnClientDataReceived( event )
|
||||
{
|
||||
event.data = ParseDataString( event.data );
|
||||
assertEx( isDefined( self ), "player entity is not defined");
|
||||
clientData = self.pers[level.clientDataKey];
|
||||
|
||||
if ( event.subtype == "Fail" )
|
||||
@ -658,15 +558,15 @@ OnClientDataReceived( event )
|
||||
{
|
||||
clientData.meta = [];
|
||||
}
|
||||
|
||||
|
||||
metaKey = event.data[0];
|
||||
clientData.meta[metaKey] = event.data[metaKey];
|
||||
|
||||
LogDebug( "Meta Key=" + metaKey + ", Meta Value=" + event.data[metaKey] );
|
||||
|
||||
LogDebug( "Meta Key=" + CoerceUndefined( metaKey ) + ", Meta Value=" + CoerceUndefined( event.data[metaKey] ) );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
clientData.permissionLevel = event.data["level"];
|
||||
clientData.clientId = event.data["clientId"];
|
||||
clientData.lastConnection = event.data["lastConnection"];
|
||||
@ -674,15 +574,13 @@ OnClientDataReceived( event )
|
||||
clientData.performance = event.data["performance"];
|
||||
clientData.state = "complete";
|
||||
self.persistentClientId = event.data["clientId"];
|
||||
|
||||
self thread DisplayWelcomeData();
|
||||
}
|
||||
|
||||
OnExecuteCommand( event )
|
||||
{
|
||||
data = ParseDataString( event.data );
|
||||
data = event.data;
|
||||
response = "";
|
||||
|
||||
|
||||
command = level.clientCommandCallbacks[event.subtype];
|
||||
runAsTarget = level.clientCommandRusAsTarget[event.subtype];
|
||||
executionContextEntity = event.origin;
|
||||
@ -691,16 +589,23 @@ OnExecuteCommand( event )
|
||||
{
|
||||
executionContextEntity = event.target;
|
||||
}
|
||||
|
||||
|
||||
if ( IsDefined( command ) )
|
||||
{
|
||||
response = executionContextEntity [[command]]( event, data );
|
||||
if ( IsDefined( executionContextEntity ) )
|
||||
{
|
||||
response = executionContextEntity thread [[command]]( event, data );
|
||||
}
|
||||
else
|
||||
{
|
||||
thread [[command]]( event );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogDebug( "Unknown Client command->" + event.subtype );
|
||||
}
|
||||
|
||||
|
||||
// send back the response to the origin, but only if they're not the target
|
||||
if ( IsDefined( response ) && response != "" && IsPlayer( event.origin ) && event.origin != event.target )
|
||||
{
|
||||
@ -710,6 +615,15 @@ OnExecuteCommand( event )
|
||||
|
||||
OnSetClientDataCompleted( event )
|
||||
{
|
||||
// IW4MAdmin let us know it persisted (success or fail)
|
||||
LogDebug( "Set Client Data -> subtype = " + event.subType + " status = " + event.data["status"] );
|
||||
LogDebug( "Set Client Data -> subtype = " + CoerceUndefined( event.subType ) + ", status = " + CoerceUndefined( event.data["status"] ) );
|
||||
}
|
||||
|
||||
CoerceUndefined( object )
|
||||
{
|
||||
if ( !IsDefined( object ) )
|
||||
{
|
||||
return "undefined";
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
@ -2,23 +2,23 @@
|
||||
|
||||
Init()
|
||||
{
|
||||
level.eventBus.gamename = "IW4";
|
||||
|
||||
level thread Setup();
|
||||
thread Setup();
|
||||
}
|
||||
|
||||
Setup()
|
||||
{
|
||||
level endon( "game_ended" );
|
||||
waittillframeend;
|
||||
|
||||
// it's possible that the notify type has not been defined yet so we have to hard code it
|
||||
level waittill( "IntegrationBootstrapInitialized" );
|
||||
level waittill( level.notifyTypes.sharedFunctionsInitialized );
|
||||
level.eventBus.gamename = "IW4";
|
||||
|
||||
scripts\_integration_base::RegisterLogger( ::Log2Console );
|
||||
|
||||
level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired;
|
||||
level.overrideMethods[level.commonFunctions.setDvar] = ::_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.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;
|
||||
@ -27,17 +27,25 @@ Setup()
|
||||
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;
|
||||
|
||||
RegisterClientCommands();
|
||||
|
||||
level notify( level.notifyTypes.gameFunctionsInitialized );
|
||||
|
||||
scripts\_integration_base::_SetDvarIfUninitialized( level.commonKeys.busdir, GetDvar( "fs_homepath" ) + "userraw/" + "scriptdata" );
|
||||
|
||||
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
|
||||
if ( GetDvarInt( level.commonKeys.enabled ) != 1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
level thread OnPlayerConnect();
|
||||
thread OnPlayerConnect();
|
||||
}
|
||||
|
||||
OnPlayerConnect()
|
||||
@ -48,12 +56,12 @@ OnPlayerConnect()
|
||||
{
|
||||
level waittill( "connected", player );
|
||||
|
||||
if ( scripts\_integration_base::_IsBot( player ) )
|
||||
if ( player IsTestClient() )
|
||||
{
|
||||
// we don't want to track bots
|
||||
continue;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
player thread SetPersistentData();
|
||||
player thread WaitForClientEvents();
|
||||
}
|
||||
@ -84,7 +92,7 @@ WaitForClientEvents()
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
self waittill( level.eventTypes.localClientEvent, event );
|
||||
self waittill( level.eventTypes.eventAvailable, event );
|
||||
|
||||
scripts\_integration_base::LogDebug( "Received client event " + event.type );
|
||||
|
||||
@ -96,6 +104,26 @@ 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;
|
||||
@ -183,12 +211,7 @@ GetTotalShotsFired()
|
||||
return maps\mp\_utility::getPlayerStat( "mostshotsfired" );
|
||||
}
|
||||
|
||||
_SetDvarIfUninitialized( dvar, value )
|
||||
{
|
||||
SetDvarIfUninitialized( dvar, value );
|
||||
}
|
||||
|
||||
_waittill_notify_or_timeout( _notify, timeout )
|
||||
WaitillNotifyOrTimeoutWrapper( _notify, timeout )
|
||||
{
|
||||
common_scripts\utility::waittill_notify_or_timeout( _notify, timeout );
|
||||
}
|
||||
@ -198,6 +221,21 @@ Log2Console( logLevel, message )
|
||||
PrintConsole( "[" + logLevel + "] " + message + "\n" );
|
||||
}
|
||||
|
||||
SetDvarIfUninitializedWrapper( dvar, value )
|
||||
{
|
||||
SetDvarIfUninitialized( dvar, value );
|
||||
}
|
||||
|
||||
GetXuidWrapper()
|
||||
{
|
||||
return self GetXUID();
|
||||
}
|
||||
|
||||
IsBotWrapper( client )
|
||||
{
|
||||
return client IsTestClient();
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// GUID helpers
|
||||
/////////////////////////////////
|
||||
@ -441,7 +479,9 @@ NoClipImpl()
|
||||
self SetClientDvar( "sv_cheats", 0 );
|
||||
|
||||
self God();
|
||||
self Noclip();
|
||||
|
||||
self.clientflags |= 1; // IW4x specific
|
||||
|
||||
self Hide();
|
||||
|
||||
self.isNoClipped = true;
|
||||
@ -455,7 +495,9 @@ NoClipImpl()
|
||||
self SetClientDvar( "sv_cheats", 0 );
|
||||
|
||||
self God();
|
||||
self Noclip();
|
||||
|
||||
self.clientflags &= ~1; // IW4x specific
|
||||
|
||||
self Show();
|
||||
|
||||
self.isNoClipped = false;
|
||||
@ -507,11 +549,7 @@ 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 );
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1,91 +1,46 @@
|
||||
#include common_scripts\utility;
|
||||
|
||||
#inline scripts\_integration_utility;
|
||||
|
||||
Init()
|
||||
{
|
||||
level.eventBus.gamename = "IW5";
|
||||
|
||||
level thread Setup();
|
||||
thread Setup();
|
||||
}
|
||||
|
||||
Setup()
|
||||
{
|
||||
level endon( "game_ended" );
|
||||
|
||||
// it's possible that the notify type has not been defined yet so we have to hard code it
|
||||
level waittill( "IntegrationBootstrapInitialized" );
|
||||
|
||||
scripts\mp\_integration_base::RegisterLogger( ::Log2Console );
|
||||
|
||||
level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired;
|
||||
level.overrideMethods["SetDvarIfUninitialized"] = ::_SetDvarIfUninitialized;
|
||||
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
|
||||
|
||||
waittillframeend;
|
||||
|
||||
level waittill( level.notifyTypes.sharedFunctionsInitialized );
|
||||
level.eventBus.gamename = "IW5";
|
||||
|
||||
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 );
|
||||
|
||||
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\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];
|
||||
}
|
||||
}
|
||||
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()
|
||||
@ -93,12 +48,12 @@ GetTotalShotsFired()
|
||||
return maps\mp\_utility::getPlayerStat( "mostshotsfired" );
|
||||
}
|
||||
|
||||
_SetDvarIfUninitialized( dvar, value )
|
||||
SetDvarIfUninitializedWrapper( dvar, value )
|
||||
{
|
||||
SetDvarIfUninitialized( dvar, value );
|
||||
}
|
||||
|
||||
_waittill_notify_or_timeout( _notify, timeout )
|
||||
WaitillNotifyOrTimeoutWrapper( _notify, timeout )
|
||||
{
|
||||
common_scripts\utility::waittill_notify_or_timeout( _notify, timeout );
|
||||
}
|
||||
@ -108,135 +63,19 @@ Log2Console( logLevel, message )
|
||||
Print( "[" + logLevel + "] " + message + "\n" );
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// GUID helpers
|
||||
/////////////////////////////////
|
||||
|
||||
SetPersistentData()
|
||||
IsBotWrapper( client )
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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"] );
|
||||
return client IsTestClient();
|
||||
}
|
||||
|
||||
SplitGuid()
|
||||
GetXuidWrapper()
|
||||
{
|
||||
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;
|
||||
return self GetXUID();
|
||||
}
|
||||
|
||||
Pow( num, exponent )
|
||||
WaitTillAnyTimeout( timeOut, string1, string2, string3, string4, string5 )
|
||||
{
|
||||
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;
|
||||
}
|
||||
return common_scripts\utility::waittill_any_timeout( timeOut, string1, string2, string3, string4, string5 );
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
@ -245,45 +84,36 @@ GetIntForHexChar( char )
|
||||
|
||||
GiveWeaponImpl( event, data )
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
return self.name + "^7 is not alive";
|
||||
}
|
||||
|
||||
_IS_ALIVE( self );
|
||||
|
||||
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()
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
return self.name + "^7 is not alive";
|
||||
}
|
||||
|
||||
_IS_ALIVE( self );
|
||||
|
||||
self TakeAllWeapons();
|
||||
self IPrintLnBold( "All your weapons have been taken" );
|
||||
|
||||
|
||||
return "Took weapons from " + self.name;
|
||||
}
|
||||
|
||||
TeamSwitchImpl()
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
return self + "^7 is not alive";
|
||||
}
|
||||
|
||||
_IS_ALIVE( self );
|
||||
|
||||
team = level.allies;
|
||||
|
||||
|
||||
if ( self.team == "allies" )
|
||||
{
|
||||
team = level.axis;
|
||||
}
|
||||
|
||||
|
||||
self IPrintLnBold( "You are being team switched" );
|
||||
wait( 2 );
|
||||
self [[team]]();
|
||||
@ -293,10 +123,7 @@ TeamSwitchImpl()
|
||||
|
||||
LockControlsImpl()
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
return self.name + "^7 is not alive";
|
||||
}
|
||||
_IS_ALIVE( self );
|
||||
|
||||
if ( !IsDefined ( self.isControlLocked ) )
|
||||
{
|
||||
@ -312,11 +139,11 @@ LockControlsImpl()
|
||||
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
|
||||
@ -333,11 +160,13 @@ LockControlsImpl()
|
||||
|
||||
NoClipImpl()
|
||||
{
|
||||
_VERIFY_PLAYER_ENT( self );
|
||||
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
self IPrintLnBold( "You are not alive" );
|
||||
}
|
||||
|
||||
|
||||
if ( !IsDefined ( self.isNoClipped ) )
|
||||
{
|
||||
self.isNoClipped = false;
|
||||
@ -347,29 +176,29 @@ NoClipImpl()
|
||||
{
|
||||
SetDvar( "sv_cheats", 1 );
|
||||
self SetClientDvar( "cg_thirdperson", 1 );
|
||||
|
||||
|
||||
self God();
|
||||
self Noclip();
|
||||
self Hide();
|
||||
SetDvar( "sv_cheats", 0 );
|
||||
|
||||
|
||||
self.isNoClipped = true;
|
||||
|
||||
|
||||
self IPrintLnBold( "NoClip enabled" );
|
||||
}
|
||||
else
|
||||
{
|
||||
SetDvar( "sv_cheats", 1 );
|
||||
self SetClientDvar( "cg_thirdperson", 0 );
|
||||
|
||||
|
||||
self God();
|
||||
self Noclip();
|
||||
self Hide();
|
||||
|
||||
|
||||
SetDvar( "sv_cheats", 0 );
|
||||
|
||||
|
||||
self.isNoClipped = false;
|
||||
|
||||
|
||||
self IPrintLnBold( "NoClip disabled" );
|
||||
}
|
||||
|
||||
@ -378,12 +207,13 @@ NoClipImpl()
|
||||
|
||||
HideImpl()
|
||||
{
|
||||
_VERIFY_PLAYER_ENT( self );
|
||||
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
self IPrintLnBold( "You are not alive" );
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if ( !IsDefined ( self.isHidden ) )
|
||||
{
|
||||
self.isHidden = false;
|
||||
@ -393,36 +223,33 @@ HideImpl()
|
||||
{
|
||||
SetDvar( "sv_cheats", 1 );
|
||||
self SetClientDvar( "cg_thirdperson", 1 );
|
||||
|
||||
|
||||
self God();
|
||||
self Hide();
|
||||
SetDvar( "sv_cheats", 0 );
|
||||
|
||||
|
||||
self.isHidden = true;
|
||||
|
||||
|
||||
self IPrintLnBold( "Hide enabled" );
|
||||
}
|
||||
else
|
||||
{
|
||||
SetDvar( "sv_cheats", 1 );
|
||||
self SetClientDvar( "cg_thirdperson", 0 );
|
||||
|
||||
|
||||
self God();
|
||||
self Show();
|
||||
SetDvar( "sv_cheats", 0 );
|
||||
|
||||
|
||||
self.isHidden = false;
|
||||
|
||||
|
||||
self IPrintLnBold( "Hide disabled" );
|
||||
}
|
||||
}
|
||||
|
||||
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 );
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -440,6 +267,8 @@ GotoImpl( event, data )
|
||||
|
||||
GotoCoordImpl( data )
|
||||
{
|
||||
_VERIFY_PLAYER_ENT( self );
|
||||
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
self IPrintLnBold( "You are not alive" );
|
||||
@ -453,6 +282,8 @@ GotoCoordImpl( data )
|
||||
|
||||
GotoPlayerImpl( target )
|
||||
{
|
||||
_VERIFY_PLAYER_ENT( self );
|
||||
|
||||
if ( !IsAlive( target ) )
|
||||
{
|
||||
self IPrintLnBold( target.name + " is not alive" );
|
||||
@ -465,10 +296,7 @@ GotoPlayerImpl( target )
|
||||
|
||||
PlayerToMeImpl( event )
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
return self.name + " is not alive";
|
||||
}
|
||||
_IS_ALIVE( self );
|
||||
|
||||
self SetOrigin( event.origin GetOrigin() );
|
||||
return "Moved here " + self.name;
|
||||
@ -476,10 +304,7 @@ PlayerToMeImpl( event )
|
||||
|
||||
KillImpl()
|
||||
{
|
||||
if ( !IsAlive( self ) )
|
||||
{
|
||||
return self.name + " is not alive";
|
||||
}
|
||||
_IS_ALIVE( self );
|
||||
|
||||
self Suicide();
|
||||
self IPrintLnBold( "You were killed by " + self.name );
|
||||
@ -489,13 +314,15 @@ KillImpl()
|
||||
|
||||
SetSpectatorImpl()
|
||||
{
|
||||
_VERIFY_PLAYER_ENT( self );
|
||||
|
||||
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";
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
|
||||
Init()
|
||||
{
|
||||
level thread Setup();
|
||||
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";
|
||||
@ -15,7 +17,10 @@ Setup()
|
||||
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;
|
||||
@ -25,31 +30,52 @@ Setup()
|
||||
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 = [];
|
||||
|
||||
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
level notify( level.notifyTypes.sharedFunctionsInitialized );
|
||||
level waittill( level.notifyTypes.gameFunctionsInitialized );
|
||||
|
||||
if ( GetDvarInt( "sv_iw4madmin_autobalance" ) != 1 )
|
||||
scripts\_integration_base::_SetDvarIfUninitialized( level.commonKeys.autoBalance, 0 );
|
||||
|
||||
if ( GetDvarInt( level.commonKeys.enabled ) != 1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
level thread OnPlayerConnect();
|
||||
thread OnPlayerConnect();
|
||||
}
|
||||
|
||||
_IsBot( player )
|
||||
{
|
||||
return [[level.overrideMethods[level.commonFunctions.isBot]]]( player );
|
||||
}
|
||||
|
||||
OnPlayerConnect()
|
||||
@ -59,7 +85,28 @@ OnPlayerConnect()
|
||||
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;
|
||||
@ -68,13 +115,341 @@ OnPlayerConnect()
|
||||
teamToJoin = player GetTeamToJoin();
|
||||
player [[level.overrideMethods[level.commonFunctions.changeTeam]]]( teamToJoin );
|
||||
|
||||
player thread OnClientFirstSpawn();
|
||||
player thread OnClientJoinedTeam();
|
||||
player thread OnClientDisconnect();
|
||||
player thread OnPlayerFirstSpawn();
|
||||
player thread OnPlayerDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
OnClientDisconnect()
|
||||
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" );
|
||||
@ -89,7 +464,7 @@ OnClientDisconnect()
|
||||
}
|
||||
}
|
||||
|
||||
OnClientJoinedTeam()
|
||||
OnPlayerJoinedTeam()
|
||||
{
|
||||
self endon( level.eventTypes.disconnect );
|
||||
|
||||
@ -97,6 +472,14 @@ OnClientJoinedTeam()
|
||||
{
|
||||
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;
|
||||
@ -109,7 +492,7 @@ OnClientJoinedTeam()
|
||||
if ( newTeam != level.commonKeys.team1 && newTeam != level.commonKeys.team2 )
|
||||
{
|
||||
OnTeamSizeChanged();
|
||||
scripts\_integration_base::LogDebug( "not force balancing " + self.name + " because they switched to spec" );
|
||||
scripts\_integration_base::LogDebug( "not force balancing " + self.name + " because they switched to spec" );
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -124,12 +507,34 @@ OnClientJoinedTeam()
|
||||
}
|
||||
}
|
||||
|
||||
OnClientFirstSpawn()
|
||||
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 != "timeout" )
|
||||
if ( timeoutResult != level.eventBus.timeoutKey )
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -324,7 +729,7 @@ GetClosestPerformanceClientForTeam( sourceTeam, excluded )
|
||||
|
||||
else if ( candidateValue < closest )
|
||||
{
|
||||
scripts\_integration_base::LogDebug( candidateValue + " is the new best value ");
|
||||
scripts\_integration_base::LogDebug( candidateValue + " is the new best value " );
|
||||
choice = players[i];
|
||||
closest = candidateValue;
|
||||
}
|
||||
@ -449,3 +854,36 @@ GetClientPerformanceOrDefault()
|
||||
|
||||
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
|
||||
|
@ -2,90 +2,43 @@
|
||||
|
||||
Init()
|
||||
{
|
||||
level.eventBus.gamename = "T5";
|
||||
|
||||
level thread Setup();
|
||||
thread Setup();
|
||||
}
|
||||
|
||||
Setup()
|
||||
{
|
||||
level endon( "game_ended" );
|
||||
waittillframeend;
|
||||
|
||||
// it's possible that the notify type has not been defined yet so we have to hard code it
|
||||
level waittill( "IntegrationBootstrapInitialized" );
|
||||
level waittill( level.notifyTypes.sharedFunctionsInitialized );
|
||||
level.eventBus.gamename = "T5";
|
||||
|
||||
scripts\mp\_integration_base::RegisterLogger( ::Log2Console );
|
||||
scripts\_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;
|
||||
|
||||
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\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];
|
||||
}
|
||||
}
|
||||
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()
|
||||
@ -93,12 +46,12 @@ GetTotalShotsFired()
|
||||
return maps\mp\gametypes\_persistence::statGet( "total_shots" );
|
||||
}
|
||||
|
||||
_SetDvarIfUninitialized(dvar, value)
|
||||
SetDvarIfUninitializedWrapper( dvar, value )
|
||||
{
|
||||
maps\mp\_utility::set_dvar_if_unset(dvar, value);
|
||||
maps\mp\_utility::set_dvar_if_unset( dvar, value );
|
||||
}
|
||||
|
||||
_waittill_notify_or_timeout( msg, timer )
|
||||
WaitillNotifyOrTimeoutWrapper( msg, timer )
|
||||
{
|
||||
self endon( msg );
|
||||
wait( timer );
|
||||
@ -111,7 +64,6 @@ Log2Console( logLevel, message )
|
||||
|
||||
God()
|
||||
{
|
||||
|
||||
if ( !IsDefined( self.godmode ) )
|
||||
{
|
||||
self.godmode = false;
|
||||
@ -129,137 +81,16 @@ God()
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// GUID helpers
|
||||
/////////////////////////////////
|
||||
|
||||
/*SetPersistentData()
|
||||
IsBotWrapper( client )
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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"] );
|
||||
return client maps\mp\_utility::is_bot();
|
||||
}
|
||||
|
||||
SplitGuid()
|
||||
GetXuidWrapper()
|
||||
{
|
||||
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;
|
||||
return self GetGuid();
|
||||
}
|
||||
|
||||
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
|
||||
/////////////////////////////////
|
||||
@ -395,7 +226,7 @@ NoClipImpl( event, data )
|
||||
|
||||
self IPrintLnBold( "NoClip enabled" );*/
|
||||
|
||||
scripts\mp\_integration_base::LogWarning( "NoClip is not supported on T5!" );
|
||||
scripts\_integration_base::LogWarning( "NoClip is not supported on T5!" );
|
||||
|
||||
}
|
||||
|
||||
|
384
GameFiles/GameInterface/_integration_t5zm.gsc
Normal file
384
GameFiles/GameInterface/_integration_t5zm.gsc
Normal file
@ -0,0 +1,384 @@
|
||||
#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";
|
||||
}
|
395
GameFiles/GameInterface/_integration_t6.gsc
Normal file
395
GameFiles/GameInterface/_integration_t6.gsc
Normal file
@ -0,0 +1,395 @@
|
||||
#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" );
|
||||
}
|
73
GameFiles/GameInterface/_integration_t6_file_bus.gsc
Normal file
73
GameFiles/GameInterface/_integration_t6_file_bus.gsc
Normal file
@ -0,0 +1,73 @@
|
||||
/*********************************************************************************
|
||||
* 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 );
|
||||
}
|
93
GameFiles/GameInterface/_integration_t6zm_helper.gsc
Normal file
93
GameFiles/GameInterface/_integration_t6zm_helper.gsc
Normal file
@ -0,0 +1,93 @@
|
||||
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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
GameFiles/GameInterface/_integration_utility.gsh
Normal file
40
GameFiles/GameInterface/_integration_utility.gsh
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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" )
|
88
GameFiles/GameInterface/example_module.gsc
Normal file
88
GameFiles/GameInterface/example_module.gsc
Normal file
@ -0,0 +1,88 @@
|
||||
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] );
|
||||
}
|
||||
}
|
@ -3,14 +3,7 @@
|
||||
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 Plutonium IW5
|
||||
## Installation Guide
|
||||
|
||||
|
||||
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.
|
||||
The documentation can be found here: [GameInterface](https://github.com/RaidMax/IW4M-Admin/wiki/GameInterface)
|
||||
|
@ -1,14 +1,21 @@
|
||||
@echo off
|
||||
|
||||
ECHO "Pluto IW5"
|
||||
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 .\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 .\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\mp"
|
||||
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_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"
|
||||
|
@ -40,8 +40,6 @@ 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
|
||||
@ -53,6 +51,9 @@ 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}"
|
||||
@ -72,6 +73,9 @@ 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
|
||||
@ -80,6 +84,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameInterface", "GameInterf
|
||||
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}"
|
||||
|
@ -8,6 +8,7 @@ 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;
|
||||
@ -24,6 +25,7 @@ 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; }
|
||||
|
||||
@ -125,7 +127,7 @@ namespace Integrations.Cod
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_log.LogDebug("Waiting for flood protect did not complete before timeout timeout {Count}",
|
||||
_log.LogDebug("Waiting for flood protect did not complete before timeout {Count}",
|
||||
connectionState.OnComplete.CurrentCount);
|
||||
throw new RConException("Timed out waiting for flood protect to expire", true);
|
||||
}
|
||||
@ -152,32 +154,28 @@ namespace Integrations.Cod
|
||||
{
|
||||
case StaticHelpers.QueryType.GET_DVAR:
|
||||
waitForResponse = true;
|
||||
payload = string
|
||||
.Format(_config.CommandPrefixes.RConGetDvar, convertedRConPassword,
|
||||
convertedParameters + '\0').Select(Convert.ToByte).ToArray();
|
||||
payload = BuildPayload(_config.CommandPrefixes.RConGetDvar, convertedRConPassword,
|
||||
convertedParameters);
|
||||
break;
|
||||
case StaticHelpers.QueryType.SET_DVAR:
|
||||
payload = string
|
||||
.Format(_config.CommandPrefixes.RConSetDvar, convertedRConPassword,
|
||||
convertedParameters + '\0').Select(Convert.ToByte).ToArray();
|
||||
payload = BuildPayload(_config.CommandPrefixes.RConSetDvar, convertedRConPassword,
|
||||
convertedParameters);
|
||||
break;
|
||||
case StaticHelpers.QueryType.COMMAND:
|
||||
payload = string
|
||||
.Format(_config.CommandPrefixes.RConCommand, convertedRConPassword,
|
||||
convertedParameters + '\0').Select(Convert.ToByte).ToArray();
|
||||
payload = BuildPayload(_config.CommandPrefixes.RConCommand, convertedRConPassword,
|
||||
convertedParameters);
|
||||
break;
|
||||
case StaticHelpers.QueryType.GET_STATUS:
|
||||
waitForResponse = true;
|
||||
payload = (_config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray();
|
||||
payload = (_config.CommandPrefixes.RConGetStatus + '\0').Select(Helpers.SafeConversion).ToArray();
|
||||
break;
|
||||
case StaticHelpers.QueryType.GET_INFO:
|
||||
waitForResponse = true;
|
||||
payload = (_config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray();
|
||||
payload = (_config.CommandPrefixes.RConGetInfo + '\0').Select(Helpers.SafeConversion).ToArray();
|
||||
break;
|
||||
case StaticHelpers.QueryType.COMMAND_STATUS:
|
||||
waitForResponse = true;
|
||||
payload = string.Format(_config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0")
|
||||
.Select(Convert.ToByte).ToArray();
|
||||
payload = BuildPayload(_config.CommandPrefixes.RConCommand, convertedRConPassword, "status");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -322,6 +320,27 @@ namespace Integrations.Cod
|
||||
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)
|
||||
{
|
||||
@ -358,7 +377,7 @@ namespace Integrations.Cod
|
||||
|
||||
await ReceiveAndStoreSocketData(rconSocket, token, connectionState);
|
||||
|
||||
if (_parser.GameName == Server.Game.IW3)
|
||||
if (_parser.GameName is Server.Game.IW3 or Server.Game.T4)
|
||||
{
|
||||
await Task.Delay(100, token); // CoD4x shenanigans
|
||||
}
|
||||
@ -375,8 +394,18 @@ namespace Integrations.Cod
|
||||
private async Task ReceiveAndStoreSocketData(Socket rconSocket, CancellationToken token,
|
||||
ConnectionState connectionState)
|
||||
{
|
||||
var result = await rconSocket.ReceiveFromAsync(connectionState.ReceiveBuffer,
|
||||
SocketFlags.None, Endpoint, token);
|
||||
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)
|
||||
{
|
||||
@ -458,7 +487,7 @@ namespace Integrations.Cod
|
||||
|
||||
return string.Join("", splitStatusStrings);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Recombines multiple game messages into one
|
||||
/// </summary>
|
||||
@ -491,7 +520,7 @@ namespace Integrations.Cod
|
||||
{
|
||||
return connectionState.ReceivedBytes.ToArray();
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -16,4 +16,8 @@
|
||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="protobuf-net" Version="3.2.26" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
57
Integrations/Cod/SecureRcon/Helpers.cs
Normal file
57
Integrations/Cod/SecureRcon/Helpers.cs
Normal file
@ -0,0 +1,57 @@
|
||||
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();
|
||||
}
|
||||
}
|
13
Integrations/Cod/SecureRcon/SecureCommand.cs
Normal file
13
Integrations/Cod/SecureRcon/SecureCommand.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using ProtoBuf;
|
||||
|
||||
namespace Integrations.Cod.SecureRcon;
|
||||
|
||||
[ProtoContract]
|
||||
public class SecureCommand
|
||||
{
|
||||
[ProtoMember(1)]
|
||||
public byte[] SecMessage { get; set; }
|
||||
|
||||
[ProtoMember(2)]
|
||||
public byte[] Signature { get; set; }
|
||||
}
|
@ -104,7 +104,7 @@ namespace Integrations.Source
|
||||
}
|
||||
|
||||
var split = response.TrimEnd('\n').Split('\n');
|
||||
return split.Take(split.Length - 1).ToArray();
|
||||
return split.Take(Math.Max(1, split.Length - 1)).ToArray();
|
||||
}
|
||||
|
||||
catch (TaskCanceledException)
|
||||
|
17
Plugins/AutomessageFeed/AutoMessageFeedConfiguration.cs
Normal file
17
Plugins/AutomessageFeed/AutoMessageFeedConfiguration.cs
Normal file
@ -0,0 +1,17 @@
|
||||
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";
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2023.4.5.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -1,18 +0,0 @@
|
||||
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";
|
||||
}
|
||||
}
|
@ -1,88 +1,77 @@
|
||||
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 AutomessageFeed
|
||||
namespace IW4MAdmin.Plugins.AutoMessageFeed;
|
||||
|
||||
public class Plugin : IPluginV2
|
||||
{
|
||||
public class Plugin : IPlugin
|
||||
public string Name => "Automessage Feed";
|
||||
public string Version => Utilities.GetVersionAsString();
|
||||
public string Author => "RaidMax";
|
||||
|
||||
private int _currentFeedItem;
|
||||
private readonly AutoMessageFeedConfiguration _configuration;
|
||||
|
||||
public static void RegisterDependencies(IServiceCollection serviceCollection)
|
||||
{
|
||||
public string Name => "Automessage Feed";
|
||||
serviceCollection.AddConfiguration<AutoMessageFeedConfiguration>("AutomessageFeedPluginSettings");
|
||||
}
|
||||
|
||||
public float Version => (float)Utilities.GetVersionAsDouble();
|
||||
public Plugin(AutoMessageFeedConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
|
||||
public string Author => "RaidMax";
|
||||
|
||||
private int _currentFeedItem;
|
||||
private readonly IConfigurationHandler<Configuration> _configurationHandler;
|
||||
|
||||
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory)
|
||||
if (configuration.EnableFeed)
|
||||
{
|
||||
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<Configuration>("AutomessageFeedPluginSettings");
|
||||
}
|
||||
|
||||
private async Task<string> GetNextFeedItem(Server server)
|
||||
{
|
||||
var items = new List<string>();
|
||||
|
||||
using (var reader = XmlReader.Create(_configurationHandler.Configuration().FeedUrl, new XmlReaderSettings() { Async = true }))
|
||||
IManagementEventSubscriptions.Load += (manager, _) =>
|
||||
{
|
||||
var feedReader = new RssFeedReader(reader);
|
||||
|
||||
while (await feedReader.Read())
|
||||
{
|
||||
switch (feedReader.ElementType)
|
||||
{
|
||||
case SyndicationElementType.Item:
|
||||
var item = await feedReader.ReadItem();
|
||||
items.Add(Regex.Replace(item.Title, @"\<.+\>.*\</.+\>", ""));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentFeedItem < items.Count && (_configurationHandler.Configuration().MaxFeedItems == 0 || _currentFeedItem < _configurationHandler.Configuration().MaxFeedItems))
|
||||
{
|
||||
_currentFeedItem++;
|
||||
return items[_currentFeedItem - 1];
|
||||
}
|
||||
|
||||
_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;
|
||||
manager.GetMessageTokens().Add(new MessageToken("FEED", GetNextFeedItem));
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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 }))
|
||||
{
|
||||
var feedReader = new RssFeedReader(reader);
|
||||
|
||||
while (await feedReader.Read())
|
||||
{
|
||||
if (feedReader.ElementType != SyndicationElementType.Item)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var item = await feedReader.ReadItem();
|
||||
items.Add(Regex.Replace(item.Title, @"\<.+\>.*\</.+\>", ""));
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentFeedItem < items.Count && (_configuration.MaxFeedItems == 0 || _currentFeedItem < _configuration.MaxFeedItems))
|
||||
{
|
||||
_currentFeedItem++;
|
||||
return items[_currentFeedItem - 1];
|
||||
}
|
||||
|
||||
_currentFeedItem = 0;
|
||||
return Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_NO_ITEMS"];
|
||||
}
|
||||
}
|
||||
|
@ -1,391 +1,390 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LiveRadar.Configuration
|
||||
namespace IW4MAdmin.Plugins.LiveRadar.Configuration;
|
||||
|
||||
public class LiveRadarConfiguration : IBaseConfiguration
|
||||
{
|
||||
class LiveRadarConfiguration : IBaseConfiguration
|
||||
public List<MapInfo> Maps { get; set; }
|
||||
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
public List<MapInfo> Maps { get; set; }
|
||||
|
||||
public IBaseConfiguration Generate()
|
||||
Maps = new List<MapInfo>()
|
||||
{
|
||||
Maps = new List<MapInfo>()
|
||||
new MapInfo()
|
||||
{
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_afghan",
|
||||
MaxLeft = 4600, // ymax
|
||||
MaxRight = -1100, // ymin
|
||||
MaxBottom = -1400, // xmin
|
||||
MaxTop = 4600, // xmax
|
||||
Left = 52, // pxmin
|
||||
Right = 898, // pxmax
|
||||
Bottom = 930, // pymax
|
||||
Top = 44 // pymin
|
||||
},
|
||||
Name = "mp_afghan",
|
||||
MaxLeft = 4600, // ymax
|
||||
MaxRight = -1100, // ymin
|
||||
MaxBottom = -1400, // xmin
|
||||
MaxTop = 4600, // xmax
|
||||
Left = 52, // pxmin
|
||||
Right = 898, // pxmax
|
||||
Bottom = 930, // pymax
|
||||
Top = 44 // pymin
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_rust",
|
||||
Top = 212,
|
||||
Bottom = 812,
|
||||
Left = 314,
|
||||
Right = 856,
|
||||
MaxRight = -225,
|
||||
MaxLeft = 1809,
|
||||
MaxTop = 1773,
|
||||
MaxBottom = -469
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_rust",
|
||||
Top = 212,
|
||||
Bottom = 812,
|
||||
Left = 314,
|
||||
Right = 856,
|
||||
MaxRight = -225,
|
||||
MaxLeft = 1809,
|
||||
MaxTop = 1773,
|
||||
MaxBottom = -469
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_terminal",
|
||||
Top = 174,
|
||||
Bottom = 846,
|
||||
Left = 18,
|
||||
Right = 1011,
|
||||
MaxTop = 2929,
|
||||
MaxBottom = -513,
|
||||
MaxLeft = 7521,
|
||||
MaxRight = 2447
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_terminal",
|
||||
Top = 174,
|
||||
Bottom = 846,
|
||||
Left = 18,
|
||||
Right = 1011,
|
||||
MaxTop = 2929,
|
||||
MaxBottom = -513,
|
||||
MaxLeft = 7521,
|
||||
MaxRight = 2447
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_subbase",
|
||||
MaxLeft = 1841,
|
||||
MaxRight = -3817,
|
||||
MaxBottom = -1585,
|
||||
MaxTop = 2593,
|
||||
Left = 18,
|
||||
Right = 968,
|
||||
Bottom = 864,
|
||||
Top = 160,
|
||||
ViewPositionRotation = 180,
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_subbase",
|
||||
MaxLeft = 1841,
|
||||
MaxRight = -3817,
|
||||
MaxBottom = -1585,
|
||||
MaxTop = 2593,
|
||||
Left = 18,
|
||||
Right = 968,
|
||||
Bottom = 864,
|
||||
Top = 160,
|
||||
ViewPositionRotation = 180,
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_estate",
|
||||
Top = 52,
|
||||
Bottom = 999,
|
||||
Left = 173,
|
||||
Right = 942,
|
||||
MaxTop = 2103,
|
||||
MaxBottom = -5077,
|
||||
MaxLeft = 4437,
|
||||
MaxRight = -1240,
|
||||
Rotation = 143,
|
||||
CenterX = -1440,
|
||||
CenterY = 1920,
|
||||
Scaler = 0.85f,
|
||||
ViewPositionRotation = 180
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_estate",
|
||||
Top = 52,
|
||||
Bottom = 999,
|
||||
Left = 173,
|
||||
Right = 942,
|
||||
MaxTop = 2103,
|
||||
MaxBottom = -5077,
|
||||
MaxLeft = 4437,
|
||||
MaxRight = -1240,
|
||||
Rotation = 143,
|
||||
CenterX = -1440,
|
||||
CenterY = 1920,
|
||||
Scaler = 0.85f,
|
||||
ViewPositionRotation = 180
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_highrise",
|
||||
MaxBottom = -3909,
|
||||
MaxTop = 1649,
|
||||
MaxRight = 5111,
|
||||
MaxLeft = 8906,
|
||||
Left = 108,
|
||||
Right = 722,
|
||||
Top = 66,
|
||||
Bottom = 974,
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_highrise",
|
||||
MaxBottom = -3909,
|
||||
MaxTop = 1649,
|
||||
MaxRight = 5111,
|
||||
MaxLeft = 8906,
|
||||
Left = 108,
|
||||
Right = 722,
|
||||
Top = 66,
|
||||
Bottom = 974,
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_quarry",
|
||||
MaxBottom = -5905,
|
||||
MaxTop = -1423,
|
||||
MaxRight = -2095,
|
||||
MaxLeft = 3217,
|
||||
Left = 126,
|
||||
Right = 968,
|
||||
Top = 114,
|
||||
Bottom = 824
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_quarry",
|
||||
MaxBottom = -5905,
|
||||
MaxTop = -1423,
|
||||
MaxRight = -2095,
|
||||
MaxLeft = 3217,
|
||||
Left = 126,
|
||||
Right = 968,
|
||||
Top = 114,
|
||||
Bottom = 824
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_boneyard",
|
||||
MaxBottom = -1756,
|
||||
MaxTop = 2345,
|
||||
MaxRight = -715,
|
||||
MaxLeft = 1664,
|
||||
Left = 248,
|
||||
Right = 728,
|
||||
Top = 68,
|
||||
Bottom = 897
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_boneyard",
|
||||
MaxBottom = -1756,
|
||||
MaxTop = 2345,
|
||||
MaxRight = -715,
|
||||
MaxLeft = 1664,
|
||||
Left = 248,
|
||||
Right = 728,
|
||||
Top = 68,
|
||||
Bottom = 897
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_brecourt",
|
||||
MaxBottom = -3797,
|
||||
MaxTop = 4240,
|
||||
MaxRight = -3876,
|
||||
MaxLeft = 2575,
|
||||
Left = 240,
|
||||
Right = 846,
|
||||
Top = 180,
|
||||
Bottom = 934
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_brecourt",
|
||||
MaxBottom = -3797,
|
||||
MaxTop = 4240,
|
||||
MaxRight = -3876,
|
||||
MaxLeft = 2575,
|
||||
Left = 240,
|
||||
Right = 846,
|
||||
Top = 180,
|
||||
Bottom = 934
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_checkpoint",
|
||||
MaxBottom = -2273,
|
||||
MaxTop = 2153,
|
||||
MaxRight = -3457,
|
||||
MaxLeft = 2329,
|
||||
Left = 30,
|
||||
Right = 1010,
|
||||
Top = 136,
|
||||
Bottom = 890
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_checkpoint",
|
||||
MaxBottom = -2273,
|
||||
MaxTop = 2153,
|
||||
MaxRight = -3457,
|
||||
MaxLeft = 2329,
|
||||
Left = 30,
|
||||
Right = 1010,
|
||||
Top = 136,
|
||||
Bottom = 890
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_derail",
|
||||
MaxBottom = -2775,
|
||||
MaxTop = 3886,
|
||||
MaxRight = -3807,
|
||||
MaxLeft = 4490,
|
||||
Left = 130,
|
||||
Right = 892,
|
||||
Top = 210,
|
||||
Bottom = 829
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_derail",
|
||||
MaxBottom = -2775,
|
||||
MaxTop = 3886,
|
||||
MaxRight = -3807,
|
||||
MaxLeft = 4490,
|
||||
Left = 130,
|
||||
Right = 892,
|
||||
Top = 210,
|
||||
Bottom = 829
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_favela",
|
||||
MaxBottom = -2017,
|
||||
MaxTop = 1769,
|
||||
MaxRight = -1239,
|
||||
MaxLeft = 2998,
|
||||
Left = 120,
|
||||
Right = 912,
|
||||
Top = 174,
|
||||
Bottom = 878
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_favela",
|
||||
MaxBottom = -2017,
|
||||
MaxTop = 1769,
|
||||
MaxRight = -1239,
|
||||
MaxLeft = 2998,
|
||||
Left = 120,
|
||||
Right = 912,
|
||||
Top = 174,
|
||||
Bottom = 878
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_invasion",
|
||||
MaxBottom = -3673,
|
||||
MaxTop = 2540,
|
||||
MaxRight = -3835,
|
||||
MaxLeft = 980,
|
||||
Left = 20,
|
||||
Right = 808,
|
||||
Top = 0,
|
||||
Bottom = 1006
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_invasion",
|
||||
MaxBottom = -3673,
|
||||
MaxTop = 2540,
|
||||
MaxRight = -3835,
|
||||
MaxLeft = 980,
|
||||
Left = 20,
|
||||
Right = 808,
|
||||
Top = 0,
|
||||
Bottom = 1006
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_nightshift",
|
||||
MaxBottom = -2497,
|
||||
MaxTop = 1977,
|
||||
MaxRight = -2265,
|
||||
MaxLeft = 945,
|
||||
Left = 246,
|
||||
Right = 826,
|
||||
Top = 104,
|
||||
Bottom = 916
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_nightshift",
|
||||
MaxBottom = -2497,
|
||||
MaxTop = 1977,
|
||||
MaxRight = -2265,
|
||||
MaxLeft = 945,
|
||||
Left = 246,
|
||||
Right = 826,
|
||||
Top = 104,
|
||||
Bottom = 916
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_rundown",
|
||||
MaxBottom = -2304,
|
||||
MaxTop = 3194,
|
||||
MaxRight = -3558,
|
||||
MaxLeft = 3361,
|
||||
Left = 32,
|
||||
Right = 1030,
|
||||
Top = 96,
|
||||
Bottom = 892
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_rundown",
|
||||
MaxBottom = -2304,
|
||||
MaxTop = 3194,
|
||||
MaxRight = -3558,
|
||||
MaxLeft = 3361,
|
||||
Left = 32,
|
||||
Right = 1030,
|
||||
Top = 96,
|
||||
Bottom = 892
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_underpass",
|
||||
MaxBottom = -601,
|
||||
MaxTop = 3761,
|
||||
MaxRight = -1569,
|
||||
MaxLeft = 3615,
|
||||
Left = 42,
|
||||
Right = 978,
|
||||
Top = 157,
|
||||
Bottom = 944
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_underpass",
|
||||
MaxBottom = -601,
|
||||
MaxTop = 3761,
|
||||
MaxRight = -1569,
|
||||
MaxLeft = 3615,
|
||||
Left = 42,
|
||||
Right = 978,
|
||||
Top = 157,
|
||||
Bottom = 944
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_abandon",
|
||||
MaxBottom = -1290,
|
||||
MaxTop = 3855,
|
||||
MaxRight = -2907,
|
||||
MaxLeft = 2723,
|
||||
Left = 6,
|
||||
Right = 1016,
|
||||
Top = 32,
|
||||
Bottom = 945
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_abandon",
|
||||
MaxBottom = -1290,
|
||||
MaxTop = 3855,
|
||||
MaxRight = -2907,
|
||||
MaxLeft = 2723,
|
||||
Left = 6,
|
||||
Right = 1016,
|
||||
Top = 32,
|
||||
Bottom = 945
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_compact",
|
||||
MaxBottom = 0,
|
||||
MaxTop = 4264,
|
||||
MaxRight = -1552,
|
||||
MaxLeft = 3344,
|
||||
Left = 35,
|
||||
Right = 1003,
|
||||
Top = 94,
|
||||
Bottom = 935
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_compact",
|
||||
MaxBottom = 0,
|
||||
MaxTop = 4264,
|
||||
MaxRight = -1552,
|
||||
MaxLeft = 3344,
|
||||
Left = 35,
|
||||
Right = 1003,
|
||||
Top = 94,
|
||||
Bottom = 935
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_complex",
|
||||
MaxBottom = -2869,
|
||||
MaxTop = 2867,
|
||||
MaxRight = -4204,
|
||||
MaxLeft = -1218,
|
||||
Left = 282,
|
||||
Right = 749,
|
||||
Top = 48,
|
||||
Bottom = 991
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_complex",
|
||||
MaxBottom = -2869,
|
||||
MaxTop = 2867,
|
||||
MaxRight = -4204,
|
||||
MaxLeft = -1218,
|
||||
Left = 282,
|
||||
Right = 749,
|
||||
Top = 48,
|
||||
Bottom = 991
|
||||
},
|
||||
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_crash",
|
||||
MaxBottom = -953,
|
||||
MaxTop = 1811,
|
||||
MaxRight = -2129,
|
||||
MaxLeft = 2277,
|
||||
Left = 52,
|
||||
Right = 1017,
|
||||
Top = 201,
|
||||
Bottom = 807
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_crash",
|
||||
MaxBottom = -953,
|
||||
MaxTop = 1811,
|
||||
MaxRight = -2129,
|
||||
MaxLeft = 2277,
|
||||
Left = 52,
|
||||
Right = 1017,
|
||||
Top = 201,
|
||||
Bottom = 807
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_fuel2",
|
||||
MaxBottom = -2218,
|
||||
MaxTop = 4324,
|
||||
MaxRight = -3115,
|
||||
MaxLeft = 3193,
|
||||
Left = 39,
|
||||
Right = 888,
|
||||
Top = 24,
|
||||
Bottom = 906
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_fuel2",
|
||||
MaxBottom = -2218,
|
||||
MaxTop = 4324,
|
||||
MaxRight = -3115,
|
||||
MaxLeft = 3193,
|
||||
Left = 39,
|
||||
Right = 888,
|
||||
Top = 24,
|
||||
Bottom = 906
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_overgrown",
|
||||
MaxBottom = -2052,
|
||||
MaxTop = 3236,
|
||||
MaxRight = -5393,
|
||||
MaxLeft = 808,
|
||||
Left = 17,
|
||||
Right = 1024,
|
||||
Top = 0,
|
||||
Bottom = 847
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_overgrown",
|
||||
MaxBottom = -2052,
|
||||
MaxTop = 3236,
|
||||
MaxRight = -5393,
|
||||
MaxLeft = 808,
|
||||
Left = 17,
|
||||
Right = 1024,
|
||||
Top = 0,
|
||||
Bottom = 847
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_storm",
|
||||
MaxBottom = -2317,
|
||||
MaxTop = 2537,
|
||||
MaxRight = -2223,
|
||||
MaxLeft = 2097,
|
||||
Left = 79,
|
||||
Right = 932,
|
||||
Top = 20,
|
||||
Bottom = 995
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_storm",
|
||||
MaxBottom = -2317,
|
||||
MaxTop = 2537,
|
||||
MaxRight = -2223,
|
||||
MaxLeft = 2097,
|
||||
Left = 79,
|
||||
Right = 932,
|
||||
Top = 20,
|
||||
Bottom = 995
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_strike",
|
||||
MaxBottom = -2504,
|
||||
MaxTop = 3359,
|
||||
MaxRight = -3105,
|
||||
MaxLeft = 2822,
|
||||
Left = 40,
|
||||
Right = 969,
|
||||
Top = 36,
|
||||
Bottom = 955
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_strike",
|
||||
MaxBottom = -2504,
|
||||
MaxTop = 3359,
|
||||
MaxRight = -3105,
|
||||
MaxLeft = 2822,
|
||||
Left = 40,
|
||||
Right = 969,
|
||||
Top = 36,
|
||||
Bottom = 955
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_trailerpark",
|
||||
MaxBottom = -2709,
|
||||
MaxTop = 2027,
|
||||
MaxRight = -1719,
|
||||
MaxLeft = 1666,
|
||||
Left = 152,
|
||||
Right = 785,
|
||||
Top = 50,
|
||||
Bottom = 931
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_trailerpark",
|
||||
MaxBottom = -2709,
|
||||
MaxTop = 2027,
|
||||
MaxRight = -1719,
|
||||
MaxLeft = 1666,
|
||||
Left = 152,
|
||||
Right = 785,
|
||||
Top = 50,
|
||||
Bottom = 931
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_vacant",
|
||||
MaxBottom = -2089,
|
||||
MaxTop = 1652,
|
||||
MaxRight = -1393,
|
||||
MaxLeft = 1789,
|
||||
Left = 122,
|
||||
Right = 909,
|
||||
Top = 16,
|
||||
Bottom = 951
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_vacant",
|
||||
MaxBottom = -2089,
|
||||
MaxTop = 1652,
|
||||
MaxRight = -1393,
|
||||
MaxLeft = 1789,
|
||||
Left = 122,
|
||||
Right = 909,
|
||||
Top = 16,
|
||||
Bottom = 951
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_nuked",
|
||||
MaxLeft = 1211,
|
||||
MaxRight = -557,
|
||||
MaxBottom = -2110,
|
||||
MaxTop = 2092,
|
||||
Left = 340,
|
||||
Right = 698,
|
||||
Bottom = 930,
|
||||
Top = 92
|
||||
},
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_nuked",
|
||||
MaxLeft = 1211,
|
||||
MaxRight = -557,
|
||||
MaxBottom = -2110,
|
||||
MaxTop = 2092,
|
||||
Left = 340,
|
||||
Right = 698,
|
||||
Bottom = 930,
|
||||
Top = 92
|
||||
},
|
||||
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_killhouse",
|
||||
MaxLeft = 4276,
|
||||
MaxRight = 2973,
|
||||
MaxBottom = -1164,
|
||||
MaxTop = 1392,
|
||||
Left = 319,
|
||||
Right = 758,
|
||||
Bottom = 937,
|
||||
Top = 87
|
||||
}
|
||||
};
|
||||
new MapInfo()
|
||||
{
|
||||
Name = "mp_killhouse",
|
||||
MaxLeft = 4276,
|
||||
MaxRight = 2973,
|
||||
MaxBottom = -1164,
|
||||
MaxTop = 1392,
|
||||
Left = 319,
|
||||
Right = 758,
|
||||
Bottom = 937,
|
||||
Top = 87
|
||||
}
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public string Name() => "LiveRadar";
|
||||
return this;
|
||||
}
|
||||
|
||||
public string Name() => "LiveRadar";
|
||||
}
|
||||
|
@ -1,26 +1,24 @@
|
||||
using LiveRadar.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using IW4MAdmin.Plugins.LiveRadar.Configuration;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace LiveRadar.Web.Controllers
|
||||
namespace IW4MAdmin.Plugins.LiveRadar.Web.Controllers
|
||||
{
|
||||
public class RadarController : BaseController
|
||||
{
|
||||
private readonly IManager _manager;
|
||||
private static LiveRadarConfiguration _config;
|
||||
private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler;
|
||||
private readonly LiveRadarConfiguration _config;
|
||||
|
||||
public RadarController(IManager manager, IConfigurationHandlerFactory configurationHandlerFactory) :
|
||||
public RadarController(IManager manager, LiveRadarConfiguration config) :
|
||||
base(manager)
|
||||
{
|
||||
_manager = manager;
|
||||
_configurationHandler =
|
||||
configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration");
|
||||
_config = config;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -32,8 +30,9 @@ namespace LiveRadar.Web.Controllers
|
||||
.Select(server => new ServerInfo
|
||||
{
|
||||
Name = server.Hostname,
|
||||
IPAddress = server.IP,
|
||||
Port = server.Port
|
||||
IPAddress = server.ListenAddress,
|
||||
Port = server.ListenPort,
|
||||
Game = server.GameCode
|
||||
});
|
||||
|
||||
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"];
|
||||
@ -45,7 +44,7 @@ namespace LiveRadar.Web.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("Radar/{serverId}/Map")]
|
||||
public async Task<IActionResult> Map(string serverId = null)
|
||||
public IActionResult Map(string serverId = null)
|
||||
{
|
||||
var server = serverId == null
|
||||
? _manager.GetServers().FirstOrDefault()
|
||||
@ -56,12 +55,6 @@ namespace LiveRadar.Web.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (_config == null)
|
||||
{
|
||||
await _configurationHandler.BuildAsync();
|
||||
_config = _configurationHandler.Configuration() ?? new LiveRadarConfiguration();
|
||||
}
|
||||
|
||||
var map = _config.Maps.FirstOrDefault(map => map.Name == server.CurrentMap.Name);
|
||||
|
||||
if (map == null)
|
||||
|
38
Plugins/LiveRadar/Events/LiveRadarEvent.cs
Normal file
38
Plugins/LiveRadar/Events/LiveRadarEvent.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using SharedLibraryCore.Events.Game;
|
||||
using EventGeneratorCallback = System.ValueTuple<string, string,
|
||||
System.Func<string, SharedLibraryCore.Interfaces.IEventParserConfiguration,
|
||||
SharedLibraryCore.GameEvent,
|
||||
SharedLibraryCore.GameEvent>>;
|
||||
|
||||
namespace IW4MAdmin.Plugins.LiveRadar.Events;
|
||||
|
||||
public class Script : IRegisterEvent
|
||||
{
|
||||
private const string EventLiveRadar = "LiveRadar";
|
||||
|
||||
private EventGeneratorCallback LiveRadar()
|
||||
{
|
||||
return (EventLiveRadar, EventLiveRadar, (eventLine, _, _) =>
|
||||
{
|
||||
var radarEvent = new LiveRadarEvent
|
||||
{
|
||||
Type = GameEvent.EventType.Other,
|
||||
Subtype = EventLiveRadar,
|
||||
Origin = new EFClient { NetworkId = 0 },
|
||||
ScriptData = eventLine
|
||||
};
|
||||
return radarEvent;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public IEnumerable<EventGeneratorCallback> Events => new[] { LiveRadar() };
|
||||
}
|
||||
|
||||
public class LiveRadarEvent : GameScriptEvent
|
||||
{
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using EventGeneratorCallback = System.ValueTuple<string, string,
|
||||
System.Func<string, SharedLibraryCore.Interfaces.IEventParserConfiguration,
|
||||
SharedLibraryCore.GameEvent,
|
||||
SharedLibraryCore.GameEvent>>;
|
||||
|
||||
namespace LiveRadar.Events
|
||||
{
|
||||
public class Script : IRegisterEvent
|
||||
{
|
||||
private const string EVENT_LIVERADAR = "LiveRadar";
|
||||
private EventGeneratorCallback LiveRadar()
|
||||
{
|
||||
return (EVENT_LIVERADAR, EVENT_LIVERADAR, (string eventLine, IEventParserConfiguration config, GameEvent autoEvent) =>
|
||||
{
|
||||
string[] lineSplit = eventLine.Split(";");
|
||||
|
||||
autoEvent.Type = GameEvent.EventType.Other;
|
||||
autoEvent.Subtype = EVENT_LIVERADAR;
|
||||
autoEvent.Origin = new EFClient() { NetworkId = 0 };
|
||||
autoEvent.Extra = lineSplit[1]; // guid
|
||||
|
||||
return autoEvent;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public IEnumerable<EventGeneratorCallback> Events => new[] { LiveRadar() };
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2023.4.5.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -1,31 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
namespace IW4MAdmin.Plugins.LiveRadar;
|
||||
|
||||
namespace LiveRadar
|
||||
public class MapInfo
|
||||
{
|
||||
public class MapInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Alias { get; set; }
|
||||
// distance from the edge of the minimap image
|
||||
// to the "playable" area
|
||||
public int Top { get; set; }
|
||||
public int Bottom { get; set; }
|
||||
public int Left { get; set; }
|
||||
public int Right { get; set; }
|
||||
// maximum coordinate values for the map
|
||||
public int MaxTop { get; set; }
|
||||
public int MaxBottom { get; set; }
|
||||
public int MaxLeft { get; set; }
|
||||
public int MaxRight { get; set; }
|
||||
public float Rotation { get; set; }
|
||||
public float ViewPositionRotation { get; set; }
|
||||
public float CenterX { get; set; }
|
||||
public float CenterY { get; set; }
|
||||
public float Scaler { get; set; } = 1.0f;
|
||||
public string Name { get; set; }
|
||||
public string Alias { get; set; }
|
||||
// distance from the edge of the minimap image
|
||||
// to the "playable" area
|
||||
public int Top { get; set; }
|
||||
public int Bottom { get; set; }
|
||||
public int Left { get; set; }
|
||||
public int Right { get; set; }
|
||||
// maximum coordinate values for the map
|
||||
public int MaxTop { get; set; }
|
||||
public int MaxBottom { get; set; }
|
||||
public int MaxLeft { get; set; }
|
||||
public int MaxRight { get; set; }
|
||||
public float Rotation { get; set; }
|
||||
public float ViewPositionRotation { get; set; }
|
||||
public float CenterX { get; set; }
|
||||
public float CenterY { get; set; }
|
||||
public float Scaler { get; set; } = 1.0f;
|
||||
|
||||
public int Width => MaxLeft - MaxRight;
|
||||
public int Height => MaxTop - MaxBottom;
|
||||
}
|
||||
public int Width => MaxLeft - MaxRight;
|
||||
public int Height => MaxTop - MaxBottom;
|
||||
}
|
||||
|
@ -1,132 +1,145 @@
|
||||
using LiveRadar.Configuration;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using IW4MAdmin.Plugins.LiveRadar.Configuration;
|
||||
using IW4MAdmin.Plugins.LiveRadar.Events;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Events.Game;
|
||||
using SharedLibraryCore.Events.Server;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Interfaces.Events;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace LiveRadar
|
||||
namespace IW4MAdmin.Plugins.LiveRadar;
|
||||
|
||||
public class Plugin : IPluginV2
|
||||
{
|
||||
public class Plugin : IPlugin
|
||||
public string Name => "Live Radar";
|
||||
|
||||
public string Version => Utilities.GetVersionAsString();
|
||||
|
||||
public string Author => "RaidMax";
|
||||
|
||||
private bool _addedPage;
|
||||
private readonly Dictionary<string, long> _botGuidLookups;
|
||||
private readonly object _lockObject = new();
|
||||
private readonly ILogger _logger;
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
|
||||
public static void RegisterDependencies(IServiceCollection serviceCollection)
|
||||
{
|
||||
public string Name => "Live Radar";
|
||||
serviceCollection.AddConfiguration<LiveRadarConfiguration>();
|
||||
}
|
||||
|
||||
public float Version => (float)Utilities.GetVersionAsDouble();
|
||||
public Plugin(ILogger<Plugin> logger, ApplicationConfiguration appConfig)
|
||||
{
|
||||
_botGuidLookups = new Dictionary<string, long>();
|
||||
_logger = logger;
|
||||
_appConfig = appConfig;
|
||||
|
||||
IGameServerEventSubscriptions.MonitoringStarted += OnMonitoringStarted;
|
||||
IGameEventSubscriptions.ClientEnteredMatch += OnClientEnteredMatch;
|
||||
IGameEventSubscriptions.ScriptEventTriggered += OnScriptEvent;
|
||||
}
|
||||
|
||||
public string Author => "RaidMax";
|
||||
|
||||
private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler;
|
||||
private readonly Dictionary<string, long> _botGuidLookups;
|
||||
private bool addedPage;
|
||||
private readonly object lockObject = new object();
|
||||
private readonly ILogger _logger;
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
|
||||
public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, ApplicationConfiguration appConfig)
|
||||
private Task OnScriptEvent(GameScriptEvent scriptEvent, CancellationToken token)
|
||||
{
|
||||
if (scriptEvent is not LiveRadarEvent radarEvent)
|
||||
{
|
||||
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration");
|
||||
_botGuidLookups = new Dictionary<string, long>();
|
||||
_logger = logger;
|
||||
_appConfig = appConfig;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var originalBotGuid = radarEvent.ScriptData.Split(";")[1];
|
||||
|
||||
if (originalBotGuid.IsBotGuid() && _appConfig.IgnoreBots)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var botKey = $"BotGuid_{originalBotGuid}";
|
||||
long generatedBotGuid;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
var hasBotKey = _botGuidLookups.ContainsKey(botKey);
|
||||
|
||||
if (!hasBotKey && originalBotGuid.IsBotGuid())
|
||||
{
|
||||
// edge case where the bot guid has not been registered yet
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
generatedBotGuid = hasBotKey
|
||||
? _botGuidLookups[botKey]
|
||||
: (originalBotGuid ?? "0").ConvertGuidToLong(NumberStyles.HexNumber);
|
||||
}
|
||||
|
||||
var radarUpdate = RadarEvent.Parse(scriptEvent.ScriptData, generatedBotGuid);
|
||||
var client =
|
||||
radarEvent.Owner.ConnectedClients.FirstOrDefault(client => client.NetworkId == radarUpdate.Guid);
|
||||
|
||||
if (client != null)
|
||||
{
|
||||
radarUpdate.Name = client.Name.StripColors();
|
||||
client.SetAdditionalProperty("LiveRadar", radarUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
public Task OnEventAsync(GameEvent E, Server S)
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Could not parse live radar output: {Data}", e.Data);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnClientEnteredMatch(ClientEnterMatchEvent clientEvent, CancellationToken token)
|
||||
{
|
||||
if (!clientEvent.Client.IsBot)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var botKey = $"BotGuid_{clientEvent.ClientNetworkId}";
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (!_botGuidLookups.ContainsKey(botKey))
|
||||
{
|
||||
_botGuidLookups.Add(botKey, clientEvent.Client.NetworkId);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnMonitoringStarted(MonitorStartEvent monitorEvent, CancellationToken token)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
// if it's an IW4 game, with custom callbacks, we want to
|
||||
// enable the live radar page
|
||||
lock (lockObject)
|
||||
var shouldRegisterPage = monitorEvent.Server.GameCode != Reference.Game.IW4 ||
|
||||
!monitorEvent.Server.IsLegacyGameIntegrationEnabled ||
|
||||
_addedPage;
|
||||
if (shouldRegisterPage)
|
||||
{
|
||||
if (E.Type == GameEvent.EventType.Start &&
|
||||
S.GameName == Server.Game.IW4 &&
|
||||
S.CustomCallback &&
|
||||
!addedPage)
|
||||
{
|
||||
E.Owner.Manager.GetPageList().Pages.Add(Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"], "/Radar");
|
||||
addedPage = true;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.PreConnect && E.Origin.IsBot)
|
||||
{
|
||||
string botKey = $"BotGuid_{E.Extra}";
|
||||
lock (lockObject)
|
||||
{
|
||||
if (!_botGuidLookups.ContainsKey(botKey))
|
||||
{
|
||||
_botGuidLookups.Add(botKey, E.Origin.NetworkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.Other && E.Subtype == "LiveRadar")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (((string) E.Extra).IsBotGuid() && _appConfig.IgnoreBots)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
string botKey = $"BotGuid_{E.Extra}";
|
||||
long generatedBotGuid;
|
||||
|
||||
lock (lockObject)
|
||||
{
|
||||
var hasBotKey = _botGuidLookups.ContainsKey(botKey);
|
||||
|
||||
if (!hasBotKey && ((string)E.Extra).IsBotGuid())
|
||||
{
|
||||
// edge case where the bot guid has not been registered yet
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
generatedBotGuid = hasBotKey
|
||||
? _botGuidLookups[botKey]
|
||||
: (E.Extra.ToString() ?? "0").ConvertGuidToLong(NumberStyles.HexNumber);
|
||||
}
|
||||
|
||||
var radarUpdate = RadarEvent.Parse(E.Data, generatedBotGuid);
|
||||
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
|
||||
|
||||
if (client != null)
|
||||
{
|
||||
radarUpdate.Name = client.Name.StripColors();
|
||||
client.SetAdditionalProperty("LiveRadar", radarUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Could not parse live radar output: {data}", e.Data);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
(monitorEvent.Source as IManager)?.GetPageList().Pages
|
||||
.Add(Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"], "/Radar");
|
||||
_addedPage = true;
|
||||
}
|
||||
|
||||
public async Task OnLoadAsync(IManager manager)
|
||||
{
|
||||
await _configurationHandler.BuildAsync();
|
||||
if (_configurationHandler.Configuration() == null)
|
||||
{
|
||||
_configurationHandler.Set((LiveRadarConfiguration)new LiveRadarConfiguration().Generate());
|
||||
await _configurationHandler.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public Task OnTickAsync(Server S)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnUnloadAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
@ -5,60 +5,59 @@ using System.Linq;
|
||||
// ReSharper disable CompareOfFloatsByEqualityOperator
|
||||
#pragma warning disable CS0659
|
||||
|
||||
namespace LiveRadar
|
||||
namespace IW4MAdmin.Plugins.LiveRadar;
|
||||
|
||||
public class RadarEvent
|
||||
{
|
||||
public class RadarEvent
|
||||
public string Name { get; set; }
|
||||
public long Guid { get; set; }
|
||||
public Vector3 Location { get; set; }
|
||||
public Vector3 ViewAngles { get; set; }
|
||||
public string Team { get; set; }
|
||||
public int Kills { get; set; }
|
||||
public int Deaths { get; set; }
|
||||
public int Score { get; set; }
|
||||
public int PlayTime { get; set; }
|
||||
public string Weapon { get; set; }
|
||||
public int Health { get; set; }
|
||||
public bool IsAlive { get; set; }
|
||||
public Vector3 RadianAngles => new Vector3(ViewAngles.X.ToRadians(), ViewAngles.Y.ToRadians(), ViewAngles.Z.ToRadians());
|
||||
public int Id => GetHashCode();
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public long Guid { get; set; }
|
||||
public Vector3 Location { get; set; }
|
||||
public Vector3 ViewAngles { get; set; }
|
||||
public string Team { get; set; }
|
||||
public int Kills { get; set; }
|
||||
public int Deaths { get; set; }
|
||||
public int Score { get; set; }
|
||||
public int PlayTime { get; set; }
|
||||
public string Weapon { get; set; }
|
||||
public int Health { get; set; }
|
||||
public bool IsAlive { get; set; }
|
||||
public Vector3 RadianAngles => new Vector3(ViewAngles.X.ToRadians(), ViewAngles.Y.ToRadians(), ViewAngles.Z.ToRadians());
|
||||
public int Id => GetHashCode();
|
||||
|
||||
public override bool Equals(object obj)
|
||||
if (obj is RadarEvent re)
|
||||
{
|
||||
if (obj is RadarEvent re)
|
||||
{
|
||||
return re.ViewAngles.X == ViewAngles.X &&
|
||||
re.ViewAngles.Y == ViewAngles.Y &&
|
||||
re.ViewAngles.Z == ViewAngles.Z &&
|
||||
re.Location.X == Location.X &&
|
||||
re.Location.Y == Location.Y &&
|
||||
re.Location.Z == Location.Z;
|
||||
}
|
||||
|
||||
return false;
|
||||
return re.ViewAngles.X == ViewAngles.X &&
|
||||
re.ViewAngles.Y == ViewAngles.Y &&
|
||||
re.ViewAngles.Z == ViewAngles.Z &&
|
||||
re.Location.X == Location.X &&
|
||||
re.Location.Y == Location.Y &&
|
||||
re.Location.Z == Location.Z;
|
||||
}
|
||||
|
||||
public static RadarEvent Parse(string input, long generatedBotGuid)
|
||||
return false;
|
||||
}
|
||||
|
||||
public static RadarEvent Parse(string input, long generatedBotGuid)
|
||||
{
|
||||
var items = input.Split(';').Skip(1).ToList();
|
||||
|
||||
var parsedEvent = new RadarEvent()
|
||||
{
|
||||
var items = input.Split(';').Skip(1).ToList();
|
||||
Guid = generatedBotGuid,
|
||||
Location = Vector3.Parse(items[1]),
|
||||
ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(),
|
||||
Team = items[3],
|
||||
Kills = int.Parse(items[4]),
|
||||
Deaths = int.Parse(items[5]),
|
||||
Score = int.Parse(items[6]),
|
||||
Weapon = items[7],
|
||||
Health = int.Parse(items[8]),
|
||||
IsAlive = items[9] == "1",
|
||||
PlayTime = Convert.ToInt32(items[10])
|
||||
};
|
||||
|
||||
var parsedEvent = new RadarEvent()
|
||||
{
|
||||
Guid = generatedBotGuid,
|
||||
Location = Vector3.Parse(items[1]),
|
||||
ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(),
|
||||
Team = items[3],
|
||||
Kills = int.Parse(items[4]),
|
||||
Deaths = int.Parse(items[5]),
|
||||
Score = int.Parse(items[6]),
|
||||
Weapon = items[7],
|
||||
Health = int.Parse(items[8]),
|
||||
IsAlive = items[9] == "1",
|
||||
PlayTime = Convert.ToInt32(items[10])
|
||||
};
|
||||
|
||||
return parsedEvent;
|
||||
}
|
||||
return parsedEvent;
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,13 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
||||
{
|
||||
public class LoginCommand : Command
|
||||
{
|
||||
public LoginCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
|
||||
private readonly LoginConfiguration _loginConfig;
|
||||
private readonly LoginStates _loginStates;
|
||||
|
||||
public LoginCommand(CommandConfiguration config, ITranslationLookup translationLookup, LoginConfiguration loginConfig, LoginStates loginStates) : base(config, translationLookup)
|
||||
{
|
||||
_loginConfig = loginConfig;
|
||||
_loginStates = loginStates;
|
||||
Name = "login";
|
||||
Description = _translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_DESC"];
|
||||
Alias = "li";
|
||||
@ -29,6 +34,12 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||
{
|
||||
if (!_loginConfig.RequirePrivilegedClientLogin)
|
||||
{
|
||||
gameEvent.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_DISABLED"]);
|
||||
return;
|
||||
}
|
||||
|
||||
var success = gameEvent.Owner.Manager.TokenAuthenticator.AuthorizeToken(new TokenIdentifier
|
||||
{
|
||||
ClientId = gameEvent.Origin.ClientId,
|
||||
@ -43,7 +54,7 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
||||
|
||||
if (success)
|
||||
{
|
||||
Plugin.AuthorizedClients[gameEvent.Origin.ClientId] = true;
|
||||
_loginStates.AuthorizedClients[gameEvent.Origin.ClientId] = true;
|
||||
}
|
||||
|
||||
_ = success ?
|
||||
|
@ -19,7 +19,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2023.4.5.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -1,9 +1,8 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Login
|
||||
{
|
||||
class Configuration : IBaseConfiguration
|
||||
public class LoginConfiguration : IBaseConfiguration
|
||||
{
|
||||
public bool RequirePrivilegedClientLogin { get; set; }
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user