Compare commits

..

43 Commits

Author SHA1 Message Date
23a33ba489 implement more robust command api and login
improve web console command response reliability and consistency
2021-01-17 21:58:18 -06:00
dd3ebf6b34 increase buffer size for rcon connection 2021-01-17 20:04:32 -06:00
28373b9325 implement admin "privacy" for issue #185 2021-01-09 12:37:20 -06:00
843c01061d update 'uptime' output
use translations for certain webfront page meta that was neglected
update plutonium parsers to not use new line in notices as it is not supported
2021-01-08 19:21:23 -06:00
5cb2d05f33 add preset rules, configurable time spans, and separate rule shortcut for issue #180 2020-12-31 18:48:58 -06:00
5a288dafc1 update shared library core version and plugins 2020-12-20 19:23:14 -06:00
4afc478076 fix issue with view stats and reset stats failing
fix issue with set level returning wrong error message if setting a client to the same level they're currently at
update CoD4x parser version
update nuget packages
2020-12-16 13:11:30 -06:00
928cbef845 resolve bot guid issue with T5
remove unneeded check for CNCT state
2020-12-14 21:10:50 -06:00
02b910234a add official T4/WaW support for issue #178
CoD4x parser tweak to parse full guid as decimal
2020-12-13 20:33:37 -06:00
f03626c3ae Another tweak for CoD4x rcon parsing. 2020-12-12 21:43:27 -06:00
6648b75255 update CoD4x parser
tweak handling segmented status response
actually support more than 18 clients LOL
2020-12-02 14:29:49 -06:00
bd3f0caf60 fix memory leak issue related to AddDbContext not working as expected 2020-11-29 16:01:52 -06:00
b2d282d412 include ; for timeout string 2020-11-27 22:08:13 -06:00
36a02b3d7b update for database provider specific migrations
fix issues with live radar
2020-11-27 21:52:52 -06:00
8ef2959f63 make notice line separator configurable for different parsers
(updated tekno's as it doesn't support \n)
2020-11-19 20:48:25 -06:00
d58b24b5b2 add shortcut for rules in penalty reasons for issue #159 2020-11-18 18:48:51 -06:00
09f37d7941 clean up some logic related to tracking stats on player join 2020-11-18 16:28:14 -06:00
103d2726c2 persist say command messages with webfront denotation to chat log
per issue #159
2020-11-18 09:08:24 -06:00
941d9cea73 more consistent/enhanced game penalty messages per issue #171 2020-11-17 18:24:54 -06:00
a574fb0d4b update index for ratings/prune old entries
small stat tweaks to add players on first kill/damage event
(instead of on connect which causes issues with slow writes)
2020-11-14 18:24:51 -06:00
664eb32587 fix small logging issue with loading plugins
add minigun turret to list of ignored ac weapons
2020-11-14 10:53:01 -06:00
6619ce714a modify iw6x parser to default game log vars temporarily, small amount of code cleanup to git rid of warnings 2020-11-12 20:39:56 -06:00
e997b94b3b update unit tests 2020-11-12 19:46:17 -06:00
5d9c8f5369 fix introduced issue with map/map_rotate commands 2020-11-11 18:53:23 -06:00
570a228c92 refactor logging in pretty big overhaul 2020-11-11 17:35:55 -06:00
fd7bd7e0da partial support of IW6x until the game log is implemented 2020-11-07 10:40:58 -06:00
e76976799b fix issue with partial matches for map load command 2020-11-03 20:04:11 -06:00
84189cf136 fix issue with T5 status response parsing 2020-10-31 09:18:37 -05:00
98ee997bf3 update pipeline versioning 2020-10-25 10:03:15 -05:00
3f7372e780 add pre release pipeline to master 2020-10-24 21:45:30 -05:00
08676f1d1e implement remote assembly loading 2020-10-24 15:02:38 -05:00
2bbafbd8f0 fix issue with delay on map command 2020-10-17 10:55:49 -05:00
40cb2a9df6 add say all (broadcast) command 2020-10-17 10:55:42 -05:00
59f1699228 fix issue with button detection 2020-10-17 10:55:29 -05:00
1484d63b97 hide flag status for non logged in users
remove erroneous anticheat detection reason on kick
2020-10-17 10:55:19 -05:00
04217e96ee fix anticheat detection type logic 2020-10-17 10:54:54 -05:00
c41fc27a1a fix introduced bug :) 2020-09-30 21:00:40 -05:00
1f1f4de67a anticheat tweaks
- reset recoil state on map change
- refactor config
- remove m21 from chest detection
- allow ignored client ids
2020-09-30 17:15:47 -05:00
7f11921757 enhance script plugin features
(support service resolver with generic args)
(support requiresTarget for command)
2020-09-28 20:32:53 -05:00
70cae976a0 implement service resolver for script plugins 2020-09-26 18:13:56 -05:00
2ab0cfa9be implement pm admins command for issue #170 2020-09-26 17:17:21 -05:00
7e3c74e63c add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x 2020-09-21 15:32:49 -05:00
a4a65a486a update GenerateGuidFromString to resolve to a stable hash code.
fix bots not showing up on live radar
2020-09-21 15:30:42 -05:00
421 changed files with 82299 additions and 3457 deletions

2
.gitignore vendored
View File

@ -244,3 +244,5 @@ launchSettings.json
/Tests/ApplicationTests/Files/GameEvents.json /Tests/ApplicationTests/Files/GameEvents.json
/Tests/ApplicationTests/Files/replay.json /Tests/ApplicationTests/Files/replay.json
/GameLogServer/game_log_server_env /GameLogServer/game_log_server_env
.idea/*
*.db

View File

@ -1,5 +1,7 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using IW4MAdmin.Application.Misc;
using Newtonsoft.Json; using Newtonsoft.Json;
using RestEase; using RestEase;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
@ -35,6 +37,13 @@ namespace IW4MAdmin.Application.API.Master
public string Message { get; set; } public string Message { get; set; }
} }
public class PluginSubscriptionContent
{
public string Content { get; set; }
public PluginType Type { get; set; }
}
/// <summary> /// <summary>
/// Defines the capabilities of the master API /// Defines the capabilities of the master API
/// </summary> /// </summary>
@ -63,5 +72,8 @@ namespace IW4MAdmin.Application.API.Master
[Get("localization/{languageTag}")] [Get("localization/{languageTag}")]
Task<SharedLibraryCore.Localization.Layout> GetLocalization([Path("languageTag")] string languageTag); Task<SharedLibraryCore.Localization.Layout> GetLocalization([Path("languageTag")] string languageTag);
[Get("plugin_subscriptions")]
Task<IEnumerable<PluginSubscriptionContent>> GetPluginSubscription([Query("instance_id")] Guid instanceId, [Query("subscription_id")] string subscription_id);
} }
} }

View File

@ -5,7 +5,7 @@
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish> <MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId> <PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2.3.2.0</Version> <Version>2020.0.0.0</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Product>IW4MAdmin</Product> <Product>IW4MAdmin</Product>
@ -25,13 +25,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-1632" /> <PackageReference Include="Jint" Version="3.0.0-beta-1632" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.10">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.10" />
<PackageReference Include="RestEase" Version="1.5.0" /> <PackageReference Include="RestEase" Version="1.5.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@ -39,7 +39,6 @@
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection> <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation> <TieredCompilation>true</TieredCompilation>
<LangVersion>Latest</LangVersion> <LangVersion>Latest</LangVersion>
<StartupObject></StartupObject>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
@ -59,6 +58,9 @@
<None Update="DefaultSettings.json"> <None Update="DefaultSettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="Configuration\LoggingConfiguration.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent"> <Target Name="PreBuild" BeforeTargets="PreBuildEvent">

View File

@ -1,5 +1,4 @@
using IW4MAdmin.Application.API.Master; using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RconParsers; using IW4MAdmin.Application.RconParsers;
@ -9,11 +8,9 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Configuration.Validation; using SharedLibraryCore.Configuration.Validation;
using SharedLibraryCore.Database; using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Services; using SharedLibraryCore.Services;
using System; using System;
using System.Collections; using System.Collections;
@ -24,7 +21,13 @@ using System.Reflection;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using IW4MAdmin.Application.Migration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using static SharedLibraryCore.GameEvent; using static SharedLibraryCore.GameEvent;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
@ -32,7 +35,7 @@ namespace IW4MAdmin.Application
{ {
private readonly ConcurrentBag<Server> _servers; private readonly ConcurrentBag<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList(); public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
public ILogger Logger => GetLogger(0); [Obsolete] public ObsoleteLogger Logger => _serviceProvider.GetRequiredService<ObsoleteLogger>();
public bool IsRunning { get; private set; } public bool IsRunning { get; private set; }
public bool IsInitialized { get; private set; } public bool IsInitialized { get; private set; }
public DateTime StartTime { get; private set; } public DateTime StartTime { get; private set; }
@ -50,11 +53,9 @@ namespace IW4MAdmin.Application
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly List<MessageToken> MessageTokens; private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc; private readonly ClientService ClientSvc;
readonly AliasService AliasSvc;
readonly PenaltyService PenaltySvc; readonly PenaltyService PenaltySvc;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList; readonly IPageList PageList;
private readonly Dictionary<long, ILogger> _loggers = new Dictionary<long, ILogger>();
private readonly IMetaService _metaService; private readonly IMetaService _metaService;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource; private readonly CancellationTokenSource _tokenSource;
@ -68,30 +69,33 @@ namespace IW4MAdmin.Application
private readonly IScriptCommandFactory _scriptCommandFactory; private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IMetaRegistration _metaRegistration; private readonly IMetaRegistration _metaRegistration;
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver; 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 ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands, public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration, ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory, IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver) IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>(); MessageTokens = new List<MessageToken>();
ClientSvc = new ClientService(contextFactory); ClientSvc = clientService;
AliasSvc = new AliasService(); PenaltySvc = penaltyService;
PenaltySvc = new PenaltyService();
ConfigHandler = appConfigHandler; ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow; StartTime = DateTime.UtcNow;
PageList = new PageList(); PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, appConfigHandler.Configuration()) }; AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(parserRegexFactory) }; AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication(); TokenAuthenticator = new TokenAuthentication();
_logger = logger; _logger = logger;
_metaService = metaService; _metaService = metaService;
_tokenSource = new CancellationTokenSource(); _tokenSource = new CancellationTokenSource();
_loggers.Add(0, logger);
_commands = commands.ToList(); _commands = commands.ToList();
_translationLookup = translationLookup; _translationLookup = translationLookup;
_commandConfiguration = commandConfiguration; _commandConfiguration = commandConfiguration;
@ -102,6 +106,9 @@ namespace IW4MAdmin.Application
_scriptCommandFactory = scriptCommandFactory; _scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration; _metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver; _scriptPluginServiceResolver = scriptPluginServiceResolver;
_serviceProvider = serviceProvider;
_changeHistoryService = changeHistoryService;
_appConfig = appConfig;
Plugins = plugins; Plugins = plugins;
} }
@ -109,9 +116,7 @@ namespace IW4MAdmin.Application
public async Task ExecuteEvent(GameEvent newEvent) public async Task ExecuteEvent(GameEvent newEvent)
{ {
#if DEBUG == true ProcessingEvents.TryAdd(newEvent.Id, newEvent);
Logger.WriteDebug($"Entering event process for {newEvent.Id}");
#endif
// the event has failed already // the event has failed already
if (newEvent.Failed) if (newEvent.Failed)
@ -124,22 +129,17 @@ namespace IW4MAdmin.Application
await newEvent.Owner.ExecuteEvent(newEvent); await newEvent.Owner.ExecuteEvent(newEvent);
// save the event info to the database // save the event info to the database
var changeHistorySvc = new ChangeHistoryService(); await _changeHistoryService.Add(newEvent);
await changeHistorySvc.Add(newEvent);
#if DEBUG
Logger.WriteDebug($"Processed event with id {newEvent.Id}");
#endif
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early"); _logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early"); _logger.LogDebug("Received quit signal for event id {eventId}, so we are aborting early", newEvent.Id);
} }
// this happens if a plugin requires login // this happens if a plugin requires login
@ -152,31 +152,58 @@ namespace IW4MAdmin.Application
catch (NetworkException ex) catch (NetworkException ex)
{ {
newEvent.FailReason = EventFailReason.Exception; newEvent.FailReason = EventFailReason.Exception;
Logger.WriteError(ex.Message); using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
Logger.WriteDebug(ex.GetExceptionInfo()); {
_logger.LogError(ex, ex.Message);
}
} }
catch (ServerException ex) catch (ServerException ex)
{ {
newEvent.FailReason = EventFailReason.Exception; newEvent.FailReason = EventFailReason.Exception;
Logger.WriteWarning(ex.Message); using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
{
_logger.LogError(ex, ex.Message);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
newEvent.FailReason = EventFailReason.Exception; newEvent.FailReason = EventFailReason.Exception;
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner)); Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner));
Logger.WriteDebug(ex.GetExceptionInfo()); using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
{
_logger.LogError(ex, "Unexpected exception");
}
} }
skip: skip:
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null)
{
var correlatedEvents =
ProcessingEvents.Values.Where(ev =>
ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id)
.ToList();
await Task.WhenAll(correlatedEvents.Select(ev =>
ev.WaitAsync(Utilities.DefaultCommandTimeout, CancellationToken)));
newEvent.Output.AddRange(correlatedEvents.SelectMany(ev => ev.Output));
foreach (var correlatedEvent in correlatedEvents)
{
ProcessingEvents.Remove(correlatedEvent.Id, out _);
}
}
// we don't want to remove events that are correlated to command
if (ProcessingEvents.Values.Count(gameEvent => gameEvent.CorrelationId == newEvent.CorrelationId) == 1)
{
ProcessingEvents.Remove(newEvent.Id, out _);
}
// tell anyone waiting for the output that we're done // tell anyone waiting for the output that we're done
newEvent.Complete(); newEvent.Complete();
OnGameEventExecuted?.Invoke(this, newEvent); OnGameEventExecuted?.Invoke(this, newEvent);
#if DEBUG == true
Logger.WriteDebug($"Exiting event process for {newEvent.Id}");
#endif
} }
public IList<Server> GetServers() public IList<Server> GetServers()
@ -226,17 +253,14 @@ namespace IW4MAdmin.Application
try try
{ {
await server.ProcessUpdatesAsync(_tokenSource.Token); await server.ProcessUpdatesAsync(_tokenSource.Token);
if (server.Throttled)
{
await Task.Delay((int)_throttleTimeout.TotalMilliseconds, _tokenSource.Token);
}
} }
catch (Exception e) catch (Exception e)
{ {
Logger.WriteWarning($"Failed to update status for {server}"); using (LogContext.PushProperty("Server", server.ToString()))
Logger.WriteDebug(e.GetExceptionInfo()); {
_logger.LogError(e, "Failed to update status");
}
} }
finally finally
@ -245,12 +269,7 @@ namespace IW4MAdmin.Application
} }
})); }));
} }
#if DEBUG
Logger.WriteDebug($"{runningUpdateTasks.Count} servers queued for stats updates");
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
try try
{ {
await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token); await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
@ -289,8 +308,8 @@ namespace IW4MAdmin.Application
catch (Exception ex) catch (Exception ex)
{ {
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"].FormatExt(scriptPlugin.Name)); Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"].FormatExt(scriptPlugin.Name));
Logger.WriteDebug(ex.Message); _logger.LogError(ex, "Could not properly load plugin {plugin}", scriptPlugin.Name);
} }
}; };
} }
@ -303,32 +322,29 @@ namespace IW4MAdmin.Application
catch (Exception ex) catch (Exception ex)
{ {
Logger.WriteError($"{_translationLookup["SERVER_ERROR_PLUGIN"]} {plugin.Name}"); _logger.LogError(ex, $"{_translationLookup["SERVER_ERROR_PLUGIN"]} {plugin.Name}");
Logger.WriteDebug(ex.GetExceptionInfo());
} }
} }
#endregion #endregion
#region CONFIG #region CONFIG
var config = ConfigHandler.Configuration();
// copy over default config if it doesn't exist // copy over default config if it doesn't exist
if (config == null) if (!_appConfig.Servers?.Any() ?? true)
{ {
var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration(); var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration();
ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate()); //ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
var newConfig = ConfigHandler.Configuration(); //var newConfig = ConfigHandler.Configuration();
newConfig.AutoMessages = defaultConfig.AutoMessages; _appConfig.AutoMessages = defaultConfig.AutoMessages;
newConfig.GlobalRules = defaultConfig.GlobalRules; _appConfig.GlobalRules = defaultConfig.GlobalRules;
newConfig.Maps = defaultConfig.Maps; _appConfig.Maps = defaultConfig.Maps;
newConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames; _appConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
newConfig.QuickMessages = defaultConfig.QuickMessages; _appConfig.QuickMessages = defaultConfig.QuickMessages;
if (newConfig.Servers == null) //if (newConfig.Servers == null)
{ {
ConfigHandler.Set(newConfig); ConfigHandler.Set(_appConfig);
newConfig.Servers = new ServerConfiguration[1]; _appConfig.Servers = new ServerConfiguration[1];
do do
{ {
@ -343,30 +359,29 @@ namespace IW4MAdmin.Application
serverConfig.AddEventParser(parser); serverConfig.AddEventParser(parser);
} }
newConfig.Servers = newConfig.Servers.Where(_servers => _servers != null).Append((ServerConfiguration)serverConfig.Generate()).ToArray(); _appConfig.Servers = _appConfig.Servers.Where(_servers => _servers != null).Append((ServerConfiguration)serverConfig.Generate()).ToArray();
} while (Utilities.PromptBool(_translationLookup["SETUP_SERVER_SAVE"])); } while (Utilities.PromptBool(_translationLookup["SETUP_SERVER_SAVE"]));
config = newConfig;
await ConfigHandler.Save(); await ConfigHandler.Save();
} }
} }
else else
{ {
if (string.IsNullOrEmpty(config.Id)) if (string.IsNullOrEmpty(_appConfig.Id))
{ {
config.Id = Guid.NewGuid().ToString(); _appConfig.Id = Guid.NewGuid().ToString();
await ConfigHandler.Save(); await ConfigHandler.Save();
} }
if (string.IsNullOrEmpty(config.WebfrontBindUrl)) if (string.IsNullOrEmpty(_appConfig.WebfrontBindUrl))
{ {
config.WebfrontBindUrl = "http://0.0.0.0:1624"; _appConfig.WebfrontBindUrl = "http://0.0.0.0:1624";
await ConfigHandler.Save(); await ConfigHandler.Save();
} }
var validator = new ApplicationConfigurationValidator(); var validator = new ApplicationConfigurationValidator();
var validationResult = validator.Validate(config); var validationResult = validator.Validate(_appConfig);
if (!validationResult.IsValid) if (!validationResult.IsValid)
{ {
@ -377,7 +392,7 @@ namespace IW4MAdmin.Application
}; };
} }
foreach (var serverConfig in config.Servers) foreach (var serverConfig in _appConfig.Servers)
{ {
Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig); Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
@ -399,26 +414,27 @@ namespace IW4MAdmin.Application
} }
} }
if (config.Servers.Length == 0) if (_appConfig.Servers.Length == 0)
{ {
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid"); throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
} }
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252"); Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(_appConfig.CustomParserEncoding) ? _appConfig.CustomParserEncoding : "windows-1252");
#endregion #endregion
#region DATABASE #region DATABASE
using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString, _logger.LogInformation("Beginning database migration sync");
GetApplicationSettings().Configuration()?.DatabaseProvider)) Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]);
{ await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
await new ContextSeed(db).Seed(); await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
} _logger.LogInformation("Finished database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
#endregion #endregion
#region COMMANDS #region COMMANDS
if (ClientSvc.GetOwners().Result.Count > 0) if (await ClientSvc.HasOwnerAsync(_tokenSource.Token))
{ {
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand)); _commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
} }
@ -440,8 +456,8 @@ namespace IW4MAdmin.Application
// this is because I want to store the command prefix in IW4MAdminSettings, but can't easily // this is because I want to store the command prefix in IW4MAdminSettings, but can't easily
// inject it to all the places that need it // inject it to all the places that need it
cmdConfig.CommandPrefix = config.CommandPrefix; cmdConfig.CommandPrefix = _appConfig.CommandPrefix;
cmdConfig.BroadcastCommandPrefix = config.BroadcastCommandPrefix; cmdConfig.BroadcastCommandPrefix = _appConfig.BroadcastCommandPrefix;
foreach (var cmd in commandsToAddToConfig) foreach (var cmd in commandsToAddToConfig)
{ {
@ -472,6 +488,7 @@ namespace IW4MAdmin.Application
} }
#endregion #endregion
Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
await InitializeServers(); await InitializeServers();
} }
@ -487,13 +504,17 @@ namespace IW4MAdmin.Application
{ {
// todo: this might not always be an IW4MServer // todo: this might not always be an IW4MServer
var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer; 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); _servers.Add(ServerInstance);
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname.StripColors()));
_logger.LogInformation("Finishing initialization and now monitoring [{server}]", ServerInstance.Hostname, ServerInstance.ToString());
}
Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname));
// add the start event for this server // add the start event for this server
var e = new GameEvent() var e = new GameEvent()
{ {
Type = GameEvent.EventType.Start, Type = GameEvent.EventType.Start,
@ -507,13 +528,11 @@ namespace IW4MAdmin.Application
catch (ServerException e) catch (ServerException e)
{ {
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"].FormatExt($"[{Conf.IPAddress}:{Conf.Port}]")); Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"].FormatExt($"[{Conf.IPAddress}:{Conf.Port}]"));
using (LogContext.PushProperty("Server", $"{Conf.IPAddress}:{Conf.Port}"))
if (e.GetType() == typeof(DvarException))
{ {
Logger.WriteDebug($"{e.Message} {(e.GetType() == typeof(DvarException) ? $"({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})" : "")}"); _logger.LogError(e, "Unexpected exception occurred during initialization");
} }
lastException = e; lastException = e;
} }
} }
@ -548,20 +567,10 @@ namespace IW4MAdmin.Application
Stop(); Stop();
} }
public ILogger GetLogger(long serverId) [Obsolete]
public ObsoleteLogger GetLogger(long serverId)
{ {
if (_loggers.ContainsKey(serverId)) return _serviceProvider.GetRequiredService<ObsoleteLogger>();
{
return _loggers[serverId];
}
else
{
var newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
_loggers.Add(serverId, newLogger);
return newLogger;
}
} }
public IList<MessageToken> GetMessageTokens() public IList<MessageToken> GetMessageTokens()
@ -580,11 +589,6 @@ namespace IW4MAdmin.Application
return ClientSvc; return ClientSvc;
} }
public AliasService GetAliasService()
{
return AliasSvc;
}
public PenaltyService GetPenaltyService() public PenaltyService GetPenaltyService()
{ {
return PenaltySvc; return PenaltySvc;
@ -607,7 +611,7 @@ namespace IW4MAdmin.Application
public IRConParser GenerateDynamicRConParser(string name) public IRConParser GenerateDynamicRConParser(string name)
{ {
return new DynamicRConParser(_parserRegexFactory) return new DynamicRConParser(_serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), _parserRegexFactory)
{ {
Name = name Name = name
}; };

View File

@ -4,11 +4,9 @@ set TargetDir=%3
set OutDir=%4 set OutDir=%4
set Version=%5 set Version=%5
echo %Version% > "%SolutionDir%DEPLOY\version.txt"
echo Copying dependency configs echo Copying dependency configs
copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%" copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%"
copy "%SolutionDir%SharedLibaryCore\%OutDir%*.deps.json" "%TargetDir%" copy "%SolutionDir%SharedLibraryCore\%OutDir%*.deps.json" "%TargetDir%"
if not exist "%TargetDir%Plugins" ( if not exist "%TargetDir%Plugins" (
echo "Making plugin dir" echo "Making plugin dir"
@ -16,12 +14,3 @@ if not exist "%TargetDir%Plugins" (
) )
xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\" xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
echo Copying plugins for publish
del %SolutionDir%BUILD\Plugins\Tests.dll
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\Windows\Plugins\"
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
echo Copying script plugins for publish
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\Windows\Plugins\"
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"

View File

@ -0,0 +1,43 @@
{
"Serilog": {
"Using": [
"Serilog.Sinks.File"
],
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "Log/IW4MAdmin-Application.log",
"rollingInterval": "Day",
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
],
"Enrich": [
"FromLogContext",
"WithMachineName",
"WithThreadId"
],
"Destructure": [
{
"Name": "ToMaximumDepth",
"Args": {
"maximumDestructuringDepth": 4
}
},
{
"Name": "ToMaximumStringLength",
"Args": {
"maximumStringLength": 1000
}
},
{
"Name": "ToMaximumCollectionCount",
"Args": {
"maximumCollectionCount": 24
}
}
]
}
}

View File

@ -5,7 +5,9 @@ using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.EventParsers namespace IW4MAdmin.Application.EventParsers
{ {
@ -91,16 +93,25 @@ namespace IW4MAdmin.Application.EventParsers
public virtual GameEvent GenerateGameEvent(string logLine) public virtual GameEvent GenerateGameEvent(string logLine)
{ {
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine); var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
int gameTime = 0; var gameTime = 0L;
if (timeMatch.Success) if (timeMatch.Success)
{
if (timeMatch.Values[0].Contains(":"))
{ {
gameTime = timeMatch gameTime = timeMatch
.Values .Values
.Skip(2) .Skip(2)
// this converts the timestamp into seconds passed // this converts the timestamp into seconds passed
.Select((_value, index) => int.Parse(_value.ToString()) * (index == 0 ? 60 : 1)) .Select((_value, index) => long.Parse(_value.ToString()) * (index == 0 ? 60 : 1))
.Sum(); .Sum();
}
else
{
gameTime = long.Parse(timeMatch.Values[0]);
}
// we want to strip the time from the log line // we want to strip the time from the log line
logLine = logLine.Substring(timeMatch.Values.First().Length); logLine = logLine.Substring(timeMatch.Values.First().Length);
} }
@ -348,7 +359,7 @@ namespace IW4MAdmin.Application.EventParsers
catch (Exception e) catch (Exception e)
{ {
_logger.WriteWarning($"Could not handle custom event generation - {e.GetExceptionInfo()}"); _logger.LogError(e, $"Could not handle custom event generation");
} }
} }

View File

@ -1,5 +1,6 @@
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.EventParsers namespace IW4MAdmin.Application.EventParsers
{ {

View File

@ -0,0 +1,101 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Events;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.MigrationContext;
using ILogger = Serilog.ILogger;
namespace IW4MAdmin.Application.Extensions
{
public static class StartupExtensions
{
private static ILogger _defaultLogger = null;
public static IServiceCollection AddBaseLogger(this IServiceCollection services,
ApplicationConfiguration appConfig)
{
if (_defaultLogger == null)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile(Path.Join(Utilities.OperatingDirectory, "Configuration", "LoggingConfiguration.json"))
.Build();
var loggerConfig = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning);
if (Utilities.IsDevelopment)
{
loggerConfig = loggerConfig.WriteTo.Console(
outputTemplate:
"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Debug();
}
_defaultLogger = loggerConfig.CreateLogger();
}
services.AddLogging(builder => builder.AddSerilog(_defaultLogger, dispose: true));
services.AddSingleton(new LoggerFactory()
.AddSerilog(_defaultLogger, true));
return services;
}
public static IServiceCollection AddDatabaseContextOptions(this IServiceCollection services,
ApplicationConfiguration appConfig)
{
var activeProvider = appConfig.DatabaseProvider?.ToLower();
if (string.IsNullOrEmpty(appConfig.ConnectionString) || activeProvider == "sqlite")
{
var currentPath = Utilities.OperatingDirectory;
currentPath = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? $"{Path.DirectorySeparatorChar}{currentPath}"
: currentPath;
var connectionStringBuilder = new SqliteConnectionStringBuilder
{DataSource = Path.Join(currentPath, "Database", "Database.db")};
var connectionString = connectionStringBuilder.ToString();
var builder = new DbContextOptionsBuilder<SqliteDatabaseContext>()
.UseSqlite(connectionString);
services.AddSingleton((DbContextOptions) builder.Options);
return services;
}
switch (activeProvider)
{
case "mysql":
var appendTimeout = !appConfig.ConnectionString.Contains("default command timeout",
StringComparison.InvariantCultureIgnoreCase);
services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder<MySqlDatabaseContext>()
.UseMySql(appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : ""),
mysqlOptions => mysqlOptions.EnableRetryOnFailure())
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options);
return services;
case "postgresql":
appendTimeout = !appConfig.ConnectionString.Contains("Command Timeout",
StringComparison.InvariantCultureIgnoreCase);
services.AddSingleton(sp =>
(DbContextOptions) new DbContextOptionsBuilder<PostgresqlDatabaseContext>()
.UseNpgsql(appConfig.ConnectionString + (appendTimeout ? ";Command Timeout=0" : ""),
postgresqlOptions => postgresqlOptions.EnableRetryOnFailure())
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options);
return services;
default:
throw new ArgumentException($"No context available for {appConfig.DatabaseProvider}");
}
}
}
}

View File

@ -1,4 +1,8 @@
using SharedLibraryCore.Database; using System;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.MigrationContext;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
@ -8,6 +12,15 @@ namespace IW4MAdmin.Application.Factories
/// </summary> /// </summary>
public class DatabaseContextFactory : IDatabaseContextFactory public class DatabaseContextFactory : IDatabaseContextFactory
{ {
private readonly DbContextOptions _contextOptions;
private readonly string _activeProvider;
public DatabaseContextFactory(ApplicationConfiguration appConfig, DbContextOptions contextOptions)
{
_contextOptions = contextOptions;
_activeProvider = appConfig.DatabaseProvider?.ToLower();
}
/// <summary> /// <summary>
/// creates a new database context /// creates a new database context
/// </summary> /// </summary>
@ -15,7 +28,35 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns> /// <returns></returns>
public DatabaseContext CreateContext(bool? enableTracking = true) public DatabaseContext CreateContext(bool? enableTracking = true)
{ {
return enableTracking.HasValue ? new DatabaseContext(disableTracking: !enableTracking.Value) : new DatabaseContext(); var context = BuildContext();
enableTracking ??= true;
if (enableTracking.Value)
{
context.ChangeTracker.AutoDetectChangesEnabled = true;
context.ChangeTracker.LazyLoadingEnabled = true;
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
}
else
{
context.ChangeTracker.AutoDetectChangesEnabled = false;
context.ChangeTracker.LazyLoadingEnabled = false;
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
return context;
}
private DatabaseContext BuildContext()
{
return _activeProvider switch
{
"sqlite" => new SqliteDatabaseContext(_contextOptions),
"mysql" => new MySqlDatabaseContext(_contextOptions),
"postgresql" => new PostgresqlDatabaseContext(_contextOptions),
_ => throw new ArgumentException($"No context found for {_activeProvider}")
};
} }
} }
} }

View File

@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using Microsoft.Extensions.Logging;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -19,12 +20,12 @@ namespace IW4MAdmin.Application.Factories
var baseUri = logUris[0]; var baseUri = logUris[0];
if (baseUri.Scheme == Uri.UriSchemeHttp) if (baseUri.Scheme == Uri.UriSchemeHttp)
{ {
return new GameLogReaderHttp(logUris, eventParser, _serviceProvider.GetRequiredService<ILogger>()); return new GameLogReaderHttp(logUris, eventParser, _serviceProvider.GetRequiredService<ILogger<GameLogReaderHttp>>());
} }
else if (baseUri.Scheme == Uri.UriSchemeFile) else if (baseUri.Scheme == Uri.UriSchemeFile)
{ {
return new GameLogReader(baseUri.LocalPath, eventParser, _serviceProvider.GetRequiredService<ILogger>()); return new GameLogReader(baseUri.LocalPath, eventParser, _serviceProvider.GetRequiredService<ILogger<GameLogReader>>());
} }
throw new NotImplementedException($"No log reader implemented for Uri scheme \"{baseUri.Scheme}\""); throw new NotImplementedException($"No log reader implemented for Uri scheme \"{baseUri.Scheme}\"");

View File

@ -1,7 +1,8 @@
using SharedLibraryCore; using System;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Collections;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -11,21 +12,21 @@ namespace IW4MAdmin.Application.Factories
internal class GameServerInstanceFactory : IGameServerInstanceFactory internal class GameServerInstanceFactory : IGameServerInstanceFactory
{ {
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IRConConnectionFactory _rconConnectionFactory;
private readonly IGameLogReaderFactory _gameLogReaderFactory;
private readonly IMetaService _metaService; private readonly IMetaService _metaService;
private readonly IServiceProvider _serviceProvider;
/// <summary> /// <summary>
/// base constructor /// base constructor
/// </summary> /// </summary>
/// <param name="translationLookup"></param> /// <param name="translationLookup"></param>
/// <param name="rconConnectionFactory"></param> /// <param name="rconConnectionFactory"></param>
public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService) public GameServerInstanceFactory(ITranslationLookup translationLookup,
IMetaService metaService,
IServiceProvider serviceProvider)
{ {
_translationLookup = translationLookup; _translationLookup = translationLookup;
_rconConnectionFactory = rconConnectionFactory;
_gameLogReaderFactory = gameLogReaderFactory;
_metaService = metaService; _metaService = metaService;
_serviceProvider = serviceProvider;
} }
/// <summary> /// <summary>
@ -36,7 +37,7 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns> /// <returns></returns>
public Server CreateServer(ServerConfiguration config, IManager manager) public Server CreateServer(ServerConfiguration config, IManager manager)
{ {
return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory, _gameLogReaderFactory, _metaService); return new IW4MServer(config, _translationLookup, _metaService, _serviceProvider, _serviceProvider.GetRequiredService<IClientNoticeMessageFormatter>());
} }
} }
} }

View File

@ -1,6 +1,7 @@
using IW4MAdmin.Application.RCon; using IW4MAdmin.Application.RCon;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Text; using System.Text;
using Microsoft.Extensions.Logging;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -10,13 +11,13 @@ namespace IW4MAdmin.Application.Factories
internal class RConConnectionFactory : IRConConnectionFactory internal class RConConnectionFactory : IRConConnectionFactory
{ {
private static readonly Encoding gameEncoding = Encoding.GetEncoding("windows-1252"); private static readonly Encoding gameEncoding = Encoding.GetEncoding("windows-1252");
private readonly ILogger _logger; private readonly ILogger<RConConnection> _logger;
/// <summary> /// <summary>
/// Base constructor /// Base constructor
/// </summary> /// </summary>
/// <param name="logger"></param> /// <param name="logger"></param>
public RConConnectionFactory(ILogger logger) public RConConnectionFactory(ILogger<RConConnection> logger)
{ {
_logger = logger; _logger = logger;
} }

View File

@ -6,6 +6,8 @@ using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
@ -15,17 +17,20 @@ namespace IW4MAdmin.Application.Factories
/// </summary> /// </summary>
public class ScriptCommandFactory : IScriptCommandFactory public class ScriptCommandFactory : IScriptCommandFactory
{ {
private CommandConfiguration _config; private readonly CommandConfiguration _config;
private readonly ITranslationLookup _transLookup; private readonly ITranslationLookup _transLookup;
private readonly IServiceProvider _serviceProvider;
public ScriptCommandFactory(CommandConfiguration config, ITranslationLookup transLookup) public ScriptCommandFactory(CommandConfiguration config, ITranslationLookup transLookup, IServiceProvider serviceProvider)
{ {
_config = config; _config = config;
_transLookup = transLookup; _transLookup = transLookup;
_serviceProvider = serviceProvider;
} }
/// <inheritdoc/> /// <inheritdoc/>
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, bool isTargetRequired, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction) public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission,
bool isTargetRequired, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction)
{ {
var permissionEnum = Enum.Parse<Permission>(permission); var permissionEnum = Enum.Parse<Permission>(permission);
var argsArray = args.Select(_arg => new CommandArgument var argsArray = args.Select(_arg => new CommandArgument
@ -34,7 +39,8 @@ namespace IW4MAdmin.Application.Factories
Required = _arg.Item2 Required = _arg.Item2
}).ToArray(); }).ToArray();
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction, _config, _transLookup); return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction,
_config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>());
} }
} }
} }

View File

@ -1,19 +1,18 @@
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
using Newtonsoft.Json;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Events; using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
public class GameEventHandler : IEventHandler public class GameEventHandler : IEventHandler
{ {
private readonly EventLog _eventLog; private readonly EventLog _eventLog;
private readonly ILogger _logger;
private static readonly GameEvent.EventType[] overrideEvents = new[] private static readonly GameEvent.EventType[] overrideEvents = new[]
{ {
GameEvent.EventType.Connect, GameEvent.EventType.Connect,
@ -22,34 +21,23 @@ namespace IW4MAdmin.Application
GameEvent.EventType.Stop GameEvent.EventType.Stop
}; };
public GameEventHandler() public GameEventHandler(ILogger<GameEventHandler> logger)
{ {
_eventLog = new EventLog(); _eventLog = new EventLog();
_logger = logger;
} }
public void HandleEvent(IManager manager, GameEvent gameEvent) public void HandleEvent(IManager manager, GameEvent gameEvent)
{ {
#if DEBUG
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
gameEvent.Owner.Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type)) if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
{ {
#if DEBUG
gameEvent.Owner.Logger.WriteDebug($"Adding event with id {gameEvent.Id}");
#endif
EventApi.OnGameEvent(gameEvent); EventApi.OnGameEvent(gameEvent);
Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent)); Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent));
} }
#if DEBUG
else else
{ {
gameEvent.Owner.Logger.WriteDebug($"Skipping event as we're shutting down {gameEvent.Id}"); _logger.LogDebug("Skipping event as we're shutting down {eventId}", gameEvent.Id);
} }
#endif
} }
} }
} }

View File

@ -3,6 +3,9 @@ using SharedLibraryCore.Interfaces;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO namespace IW4MAdmin.Application.IO
{ {
@ -12,12 +15,14 @@ namespace IW4MAdmin.Application.IO
private readonly Server _server; private readonly Server _server;
private readonly IGameLogReader _reader; private readonly IGameLogReader _reader;
private readonly bool _ignoreBots; private readonly bool _ignoreBots;
private readonly ILogger _logger;
public GameLogEventDetection(Server server, Uri[] gameLogUris, IGameLogReaderFactory gameLogReaderFactory) public GameLogEventDetection(ILogger<GameLogEventDetection> logger, IW4MServer server, Uri[] gameLogUris, IGameLogReaderFactory gameLogReaderFactory)
{ {
_reader = gameLogReaderFactory.CreateGameLogReader(gameLogUris, server.EventParser); _reader = gameLogReaderFactory.CreateGameLogReader(gameLogUris, server.EventParser);
_server = server; _server = server;
_ignoreBots = server?.Manager.GetApplicationSettings().Configuration().IgnoreBots ?? false; _ignoreBots = server?.Manager.GetApplicationSettings().Configuration().IgnoreBots ?? false;
_logger = logger;
} }
public async Task PollForChanges() public async Task PollForChanges()
@ -33,15 +38,17 @@ namespace IW4MAdmin.Application.IO
catch (Exception e) catch (Exception e)
{ {
_server.Logger.WriteWarning($"Failed to update log event for {_server.EndPoint}"); using(LogContext.PushProperty("Server", _server.ToString()))
_server.Logger.WriteDebug(e.GetExceptionInfo()); {
_logger.LogError(e, "Failed to update log event for {endpoint}", _server.EndPoint);
}
} }
} }
await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken); await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken);
} }
_server.Logger.WriteDebug("Stopped polling for changes"); _logger.LogDebug("Stopped polling for changes");
} }
public async Task UpdateLogEvents() public async Task UpdateLogEvents()
@ -68,9 +75,6 @@ namespace IW4MAdmin.Application.IO
{ {
try try
{ {
#if DEBUG
_server.Logger.WriteVerbose(gameEvent.Data);
#endif
gameEvent.Owner = _server; gameEvent.Owner = _server;
// we don't want to add the event if ignoreBots is on and the event comes from a bot // we don't want to add the event if ignoreBots is on and the event comes from a bot
@ -102,10 +106,14 @@ namespace IW4MAdmin.Application.IO
catch (InvalidOperationException) catch (InvalidOperationException)
{ {
if (!_ignoreBots) if (_ignoreBots)
{ {
_server.Logger.WriteWarning("Could not find client in client list when parsing event line"); continue;
_server.Logger.WriteDebug(gameEvent.Data); }
using(LogContext.PushProperty("Server", _server.ToString()))
{
_logger.LogError("Could not find client in client list when parsing event line {data}", gameEvent.Data);
} }
} }
} }

View File

@ -6,6 +6,8 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO namespace IW4MAdmin.Application.IO
{ {
@ -19,7 +21,7 @@ namespace IW4MAdmin.Application.IO
public int UpdateInterval => 300; public int UpdateInterval => 300;
public GameLogReader(string logFile, IEventParser parser, ILogger logger) public GameLogReader(string logFile, IEventParser parser, ILogger<GameLogReader> logger)
{ {
_logFile = logFile; _logFile = logFile;
_parser = parser; _parser = parser;
@ -73,9 +75,7 @@ namespace IW4MAdmin.Application.IO
catch (Exception e) catch (Exception e)
{ {
_logger.WriteWarning("Could not properly parse event line"); _logger.LogError(e, "Could not properly parse event line {@eventLine}", eventLine);
_logger.WriteDebug(e.Message);
_logger.WriteDebug(eventLine);
} }
} }

View File

@ -6,6 +6,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.IO namespace IW4MAdmin.Application.IO
{ {
@ -20,7 +22,7 @@ namespace IW4MAdmin.Application.IO
private readonly string _safeLogPath; private readonly string _safeLogPath;
private string lastKey = "next"; private string lastKey = "next";
public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger logger) public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger<GameLogReaderHttp> logger)
{ {
_eventParser = parser; _eventParser = parser;
_logServerApi = RestClient.For<IGameLogServer>(gameLogServerUris[0].ToString()); _logServerApi = RestClient.For<IGameLogServer>(gameLogServerUris[0].ToString());
@ -40,7 +42,7 @@ namespace IW4MAdmin.Application.IO
if (!response.Success && string.IsNullOrEmpty(lastKey)) if (!response.Success && string.IsNullOrEmpty(lastKey))
{ {
_logger.WriteError($"Could not get log server info of {_safeLogPath}"); _logger.LogError("Could not get log server info of {logPath}", _safeLogPath);
return events; return events;
} }
@ -62,9 +64,7 @@ namespace IW4MAdmin.Application.IO
catch (Exception e) catch (Exception e)
{ {
_logger.WriteError("Could not properly parse event line from http"); _logger.LogError(e, "Could not properly parse event line from http {eventLine}", eventLine);
_logger.WriteDebug(e.Message);
_logger.WriteDebug(eventLine);
} }
} }
} }

View File

@ -12,10 +12,12 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
namespace IW4MAdmin namespace IW4MAdmin
@ -27,40 +29,51 @@ namespace IW4MAdmin
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IMetaService _metaService; private readonly IMetaService _metaService;
private const int REPORT_FLAG_COUNT = 4; private const int REPORT_FLAG_COUNT = 4;
private int lastGameTime = 0; private long lastGameTime = 0;
public int Id { get; private set; } public int Id { get; private set; }
private readonly IServiceProvider _serviceProvider;
private readonly IClientNoticeMessageFormatter _messageFormatter;
public IW4MServer(IManager mgr, ServerConfiguration cfg, ITranslationLookup lookup, public IW4MServer(
IRConConnectionFactory connectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService) : base(cfg, mgr, connectionFactory, gameLogReaderFactory) ServerConfiguration serverConfiguration,
ITranslationLookup lookup,
IMetaService metaService,
IServiceProvider serviceProvider,
IClientNoticeMessageFormatter messageFormatter) : base(serviceProvider.GetRequiredService<ILogger<Server>>(),
serviceProvider.GetRequiredService<SharedLibraryCore.Interfaces.ILogger>(),
serverConfiguration,
serviceProvider.GetRequiredService<IManager>(),
serviceProvider.GetRequiredService<IRConConnectionFactory>(),
serviceProvider.GetRequiredService<IGameLogReaderFactory>())
{ {
_translationLookup = lookup; _translationLookup = lookup;
_metaService = metaService; _metaService = metaService;
_serviceProvider = serviceProvider;
_messageFormatter = messageFormatter;
} }
override public async Task<EFClient> OnClientConnected(EFClient clientFromLog) public override async Task<EFClient> OnClientConnected(EFClient clientFromLog)
{ {
Logger.WriteDebug($"Client slot #{clientFromLog.ClientNumber} now reserved"); ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber);
EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId); EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId);
// first time client is connecting to server // first time client is connecting to server
if (client == null) if (client == null)
{ {
Logger.WriteDebug($"Client {clientFromLog} first time connecting"); ServerLogger.LogDebug("Client {client} first time connecting", clientFromLog.ToString());
clientFromLog.CurrentServer = this; clientFromLog.CurrentServer = this;
client = await Manager.GetClientService().Create(clientFromLog); client = await Manager.GetClientService().Create(clientFromLog);
} }
/// this is only a temporary version until the IPAddress is transmitted // this is only a temporary version until the IPAddress is transmitted
client.CurrentAlias = new EFAlias() client.CurrentAlias = new EFAlias()
{ {
Name = clientFromLog.Name, Name = clientFromLog.Name,
IPAddress = clientFromLog.IPAddress IPAddress = clientFromLog.IPAddress
}; };
Logger.WriteInfo($"Client {client} connected...");
// Do the player specific stuff // Do the player specific stuff
client.ClientNumber = clientFromLog.ClientNumber; client.ClientNumber = clientFromLog.ClientNumber;
client.Score = clientFromLog.Score; client.Score = clientFromLog.Score;
@ -69,9 +82,7 @@ namespace IW4MAdmin
client.State = ClientState.Connecting; client.State = ClientState.Connecting;
Clients[client.ClientNumber] = client; Clients[client.ClientNumber] = client;
#if DEBUG == true ServerLogger.LogDebug("End PreConnect for {client}", client.ToString());
Logger.WriteDebug($"End PreConnect for {client}");
#endif
var e = new GameEvent() var e = new GameEvent()
{ {
Origin = client, Origin = client,
@ -83,11 +94,14 @@ namespace IW4MAdmin
return client; return client;
} }
override public async Task OnClientDisconnected(EFClient client) public override async Task OnClientDisconnected(EFClient client)
{ {
if (!GetClientsAsList().Any(_client => _client.NetworkId == client.NetworkId)) if (!GetClientsAsList().Any(_client => _client.NetworkId == client.NetworkId))
{ {
Logger.WriteInfo($"{client} disconnecting, but they are not connected"); using (LogContext.PushProperty("Server", ToString()))
{
ServerLogger.LogWarning("{client} disconnecting, but they are not connected", client.ToString());
}
return; return;
} }
@ -95,7 +109,7 @@ namespace IW4MAdmin
if (client.ClientNumber >= 0) if (client.ClientNumber >= 0)
{ {
#endif #endif
Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting..."); ServerLogger.LogDebug("Client {@client} disconnecting...", new { client=client.ToString(), client.State });
Clients[client.ClientNumber] = null; Clients[client.ClientNumber] = null;
await client.OnDisconnect(); await client.OnDisconnect();
@ -114,19 +128,14 @@ namespace IW4MAdmin
public override async Task ExecuteEvent(GameEvent E) public override async Task ExecuteEvent(GameEvent E)
{ {
if (E == null) using (LogContext.PushProperty("Server", ToString()))
{ {
Logger.WriteError("Received NULL event");
return;
}
if (E.IsBlocking) if (E.IsBlocking)
{ {
await E.Origin?.Lock(); await E.Origin?.Lock();
} }
bool canExecuteCommand = true; bool canExecuteCommand = true;
Exception lastException = null;
try try
{ {
@ -145,7 +154,8 @@ namespace IW4MAdmin
catch (CommandException e) catch (CommandException e)
{ {
Logger.WriteInfo(e.Message); ServerLogger.LogWarning(e, "Error validating command from event {@event}",
new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId });
E.FailReason = GameEvent.EventFailReason.Invalid; E.FailReason = GameEvent.EventFailReason.Invalid;
} }
@ -175,42 +185,20 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.Command && E.Extra is Command command && if (E.Type == GameEvent.EventType.Command && E.Extra is Command command &&
(canExecuteCommand || E.Origin?.Level == Permission.Console)) (canExecuteCommand || E.Origin?.Level == Permission.Console))
{ {
ServerLogger.LogInformation("Executing command {comamnd} for {client}", command.Name, E.Origin.ToString());
await command.ExecuteAsync(E); await command.ExecuteAsync(E);
} }
var pluginTasks = Manager.Plugins.Where(_plugin => _plugin.Name != "Login").Select(async _plugin => var pluginTasks = Manager.Plugins
{ .Where(_plugin => _plugin.Name != "Login")
try .Select(async plugin => await CreatePluginTask(plugin, E));
{
// we don't want to run the events on parser plugins
if (_plugin is ScriptPlugin scriptPlugin && scriptPlugin.IsParser)
{
return;
}
using (var tokenSource = new CancellationTokenSource()) await Task.WhenAll(pluginTasks);
{
tokenSource.CancelAfter(Utilities.DefaultCommandTimeout);
await (_plugin.OnEventAsync(E, this)).WithWaitCancellation(tokenSource.Token);
}
}
catch (Exception Except)
{
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{_plugin.Name}]");
Logger.WriteDebug(Except.GetExceptionInfo());
}
}).ToArray();
if (pluginTasks.Any())
{
await Task.WhenAny(pluginTasks);
}
} }
catch (Exception e) catch (Exception e)
{ {
lastException = e; ServerLogger.LogError(e, "Unexpected exception occurred processing event");
if (E.Origin != null && E.Type == GameEvent.EventType.Command) if (E.Origin != null && E.Type == GameEvent.EventType.Command)
{ {
E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]); E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
@ -223,15 +211,30 @@ namespace IW4MAdmin
{ {
E.Origin?.Unlock(); E.Origin?.Unlock();
} }
}
}
}
if (lastException != null) private async Task CreatePluginTask(IPlugin plugin, GameEvent gameEvent)
{ {
bool notifyDisconnects = !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost; // we don't want to run the events on parser plugins
if (notifyDisconnects || (!notifyDisconnects && lastException as NetworkException == null)) if (plugin is ScriptPlugin scriptPlugin && scriptPlugin.IsParser)
{ {
throw lastException; return;
} }
using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(Utilities.DefaultCommandTimeout);
try
{
await (plugin.OnEventAsync(gameEvent, this)).WithWaitCancellation(tokenSource.Token);
} }
catch (Exception ex)
{
Console.WriteLine(loc["SERVER_PLUGIN_ERROR"]);
ServerLogger.LogError(ex, "Could not execute {methodName} for plugin {plugin}",
nameof(plugin.OnEventAsync), plugin.Name);
} }
} }
@ -240,41 +243,45 @@ namespace IW4MAdmin
/// </summary> /// </summary>
/// <param name="E"></param> /// <param name="E"></param>
/// <returns></returns> /// <returns></returns>
override protected async Task<bool> ProcessEvent(GameEvent E) protected override async Task<bool> ProcessEvent(GameEvent E)
{ {
#if DEBUG using (LogContext.PushProperty("Server", ToString()))
Logger.WriteDebug($"processing event of type {E.Type}"); using (LogContext.PushProperty("EventType", E.Type))
#endif {
ServerLogger.LogDebug("processing event of type {type}", E.Type);
if (E.Type == GameEvent.EventType.ConnectionLost) if (E.Type == GameEvent.EventType.ConnectionLost)
{ {
var exception = E.Extra as Exception; var exception = E.Extra as Exception;
ServerLogger.LogError(exception,
"Connection lost with {server}", ToString());
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{ {
Logger.WriteError(exception.Message); Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
if (exception.Data["internal_exception"] != null)
{
Logger.WriteDebug($"Internal Exception: {exception.Data["internal_exception"]}");
} }
}
Logger.WriteInfo("Connection lost to server, so we are throttling the poll rate");
Throttled = true; Throttled = true;
} }
if (E.Type == GameEvent.EventType.ConnectionRestored) if (E.Type == GameEvent.EventType.ConnectionRestored)
{ {
if (Throttled && !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) ServerLogger.LogInformation(
"Connection restored with {server}", ToString());
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{ {
Logger.WriteVerbose(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]")); Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
} }
Logger.WriteInfo("Connection restored to server, so we are no longer throttling the poll rate");
Throttled = false; Throttled = false;
} }
if (E.Type == GameEvent.EventType.ChangePermission) if (E.Type == GameEvent.EventType.ChangePermission)
{ {
var newPermission = (Permission)E.Extra; var newPermission = (Permission) E.Extra;
Logger.WriteInfo($"{E.Origin} is setting {E.Target} to permission level {newPermission}"); 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); await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin);
} }
@ -299,6 +306,7 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.PreConnect) else if (E.Type == GameEvent.EventType.PreConnect)
{ {
ServerLogger.LogInformation("Detected PreConnect for {client} from {source}", E.Origin.ToString(), E.Source);
// we don't want to track bots in the database at all if ignore bots is requested // we don't want to track bots in the database at all if ignore bots is requested
if (E.Origin.IsBot && Manager.GetApplicationSettings().Configuration().IgnoreBots) if (E.Origin.IsBot && Manager.GetApplicationSettings().Configuration().IgnoreBots)
{ {
@ -307,16 +315,19 @@ namespace IW4MAdmin
if (E.Origin.CurrentServer == null) if (E.Origin.CurrentServer == null)
{ {
Logger.WriteWarning($"preconnecting client {E.Origin} did not have a current server specified"); ServerLogger.LogWarning("Preconnecting client {client} did not have a current server specified",
E.Origin.ToString());
E.Origin.CurrentServer = this; E.Origin.CurrentServer = this;
} }
var existingClient = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin)); var existingClient = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
// they're already connected // they're already connected
if (existingClient != null && existingClient.ClientNumber == E.Origin.ClientNumber && !E.Origin.IsBot) if (existingClient != null && existingClient.ClientNumber == E.Origin.ClientNumber &&
!E.Origin.IsBot)
{ {
Logger.WriteWarning($"detected preconnect for {E.Origin}, but they are already connected"); ServerLogger.LogInformation("{client} is already connected, so we are ignoring their PreConnect",
E.Origin.ToString());
return false; return false;
} }
@ -324,18 +335,19 @@ namespace IW4MAdmin
// possible a connect/reconnect game event before we get to process it here // possible a connect/reconnect game event before we get to process it here
// it appears that new games decide to switch client slots between maps (even if the clients aren't disconnecting) // it appears that new games decide to switch client slots between maps (even if the clients aren't disconnecting)
// bots can have duplicate names which causes conflicting GUIDs // bots can have duplicate names which causes conflicting GUIDs
else if (existingClient != null && existingClient.ClientNumber != E.Origin.ClientNumber && !E.Origin.IsBot) if (existingClient != null && existingClient.ClientNumber != E.Origin.ClientNumber &&
!E.Origin.IsBot)
{ {
Logger.WriteWarning($"client {E.Origin} is trying to connect in client slot {E.Origin.ClientNumber}, but they are already registed in client slot {existingClient.ClientNumber}, swapping..."); ServerLogger.LogWarning(
"client {client} is trying to connect in client slot {newClientSlot}, but they are already registered in client slot {oldClientSlot}, swapping...",
E.Origin.ToString(), E.Origin.ClientNumber, existingClient.ClientNumber);
// we need to remove them so the client spots can swap // we need to remove them so the client spots can swap
await OnClientDisconnected(Clients[existingClient.ClientNumber]); await OnClientDisconnected(Clients[existingClient.ClientNumber]);
} }
if (Clients[E.Origin.ClientNumber] == null) if (Clients[E.Origin.ClientNumber] == null)
{ {
#if DEBUG == true ServerLogger.LogDebug("Begin PreConnect for {origin}", E.Origin.ToString());
Logger.WriteDebug($"Begin PreConnect for {E.Origin}");
#endif
// we can go ahead and put them in so that they don't get re added // we can go ahead and put them in so that they don't get re added
Clients[E.Origin.ClientNumber] = E.Origin; Clients[E.Origin.ClientNumber] = E.Origin;
try try
@ -346,9 +358,8 @@ namespace IW4MAdmin
catch (Exception ex) catch (Exception ex)
{ {
Logger.WriteError($"{loc["SERVER_ERROR_ADDPLAYER"]} {E.Origin}"); Console.WriteLine($"{loc["SERVER_ERROR_ADDPLAYER"]} {E.Origin}");
Logger.WriteDebug(ex.GetExceptionInfo()); ServerLogger.LogError(ex, "Could not add player {player}", E.Origin.ToString());
Clients[E.Origin.ClientNumber] = null; Clients[E.Origin.ClientNumber] = null;
return false; return false;
} }
@ -362,7 +373,9 @@ namespace IW4MAdmin
// for some reason there's still a client in the spot // for some reason there's still a client in the spot
else else
{ {
Logger.WriteWarning($"{E.Origin} is connecting but {Clients[E.Origin.ClientNumber]} is currently in that client slot"); ServerLogger.LogWarning(
"{origin} is connecting but {existingClient} is currently in that client slot",
E.Origin.ToString(), Clients[E.Origin.ClientNumber].ToString());
} }
} }
@ -437,18 +450,20 @@ namespace IW4MAdmin
if (!E.Target.IsPrivileged() && reportNum >= REPORT_FLAG_COUNT && !isAutoFlagged) if (!E.Target.IsPrivileged() && reportNum >= REPORT_FLAG_COUNT && !isAutoFlagged)
{ {
E.Target.Flag(Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"].FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner)); E.Target.Flag(
Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"]
.FormatExt(reportNum), Utilities.IW4MAdminClient(E.Owner));
} }
} }
else if (E.Type == GameEvent.EventType.TempBan) else if (E.Type == GameEvent.EventType.TempBan)
{ {
await TempBan(E.Data, (TimeSpan)E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin); ; await TempBan(E.Data, (TimeSpan) E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin);
} }
else if (E.Type == GameEvent.EventType.Ban) else if (E.Type == GameEvent.EventType.Ban)
{ {
bool isEvade = E.Extra != null ? (bool)E.Extra : false; bool isEvade = E.Extra != null ? (bool) E.Extra : false;
await Ban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin, isEvade); await Ban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin, isEvade);
} }
@ -459,7 +474,7 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.Kick) else if (E.Type == GameEvent.EventType.Kick)
{ {
await Kick(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin); await Kick(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin, E.Extra as EFPenalty);
} }
else if (E.Type == GameEvent.EventType.Warn) else if (E.Type == GameEvent.EventType.Warn)
@ -482,11 +497,15 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.PreDisconnect) else if (E.Type == GameEvent.EventType.PreDisconnect)
{ {
ServerLogger.LogInformation("Detected PreDisconnect for {client} from {source}",
E.Origin.ToString(), E.Source);
bool isPotentialFalseQuit = E.GameTime.HasValue && E.GameTime.Value == lastGameTime; bool isPotentialFalseQuit = E.GameTime.HasValue && E.GameTime.Value == lastGameTime;
if (isPotentialFalseQuit) if (isPotentialFalseQuit)
{ {
Logger.WriteInfo($"Receive predisconnect event for {E.Origin}, but it occured at game time {E.GameTime.Value}, which is the same last map change, so we're ignoring"); ServerLogger.LogDebug(
"Received PreDisconnect event for {origin}, but it occured at game time {gameTime}, which is the same last map change, so we're ignoring",
E.Origin.ToString(), E.GameTime);
return false; return false;
} }
@ -496,34 +515,31 @@ namespace IW4MAdmin
if (client == null) if (client == null)
{ {
Logger.WriteWarning($"Client {E.Origin} detected as disconnecting, but could not find them in the player list"); // this can happen when the status picks up the connect before the log does
ServerLogger.LogInformation(
"Ignoring PreDisconnect for {origin} because they are no longer on the client list",
E.Origin.ToString());
return false; return false;
} }
else if (client.State != ClientState.Unknown) else if (client.State != ClientState.Unknown)
{ {
#if DEBUG == true
Logger.WriteDebug($"Begin PreDisconnect for {client}");
#endif
await OnClientDisconnected(client); await OnClientDisconnected(client);
#if DEBUG == true
Logger.WriteDebug($"End PreDisconnect for {client}");
#endif
return true; return true;
} }
else else
{ {
Logger.WriteWarning($"Expected disconnecting client {client} to be in state {ClientState.Connected.ToString()}, but is in state {client.State}"); ServerLogger.LogWarning(
"Expected disconnecting client {client} to be in state {state}, but is in state {clientState}",
client.ToString(), ClientState.Connected.ToString(), client.State);
return false; return false;
} }
} }
else if (E.Type == GameEvent.EventType.Update) else if (E.Type == GameEvent.EventType.Update)
{ {
#if DEBUG == true ServerLogger.LogDebug("Begin Update for {origin}", E.Origin.ToString());
Logger.WriteDebug($"Begin Update for {E.Origin}");
#endif
await OnClientUpdate(E.Origin); await OnClientUpdate(E.Origin);
} }
@ -559,7 +575,7 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.MapChange) if (E.Type == GameEvent.EventType.MapChange)
{ {
Logger.WriteInfo($"New map loaded - {ClientNum} active players"); ServerLogger.LogInformation("New map loaded - {clientCount} active players", ClientNum);
// iw4 doesn't log the game info // iw4 doesn't log the game info
if (E.Extra == null) if (E.Extra == null)
@ -568,7 +584,7 @@ namespace IW4MAdmin
if (dict == null) if (dict == null)
{ {
Logger.WriteWarning("Map change event response doesn't have any data"); ServerLogger.LogWarning("Map change event response doesn't have any data");
} }
else else
@ -583,7 +599,7 @@ namespace IW4MAdmin
else else
{ {
var dict = (Dictionary<string, string>)E.Extra; var dict = (Dictionary<string, string>) E.Extra;
Gametype = dict["g_gametype"]; Gametype = dict["g_gametype"];
Hostname = dict["sv_hostname"]; Hostname = dict["sv_hostname"];
MaxClients = int.Parse(dict["sv_maxclients"]); MaxClients = int.Parse(dict["sv_maxclients"]);
@ -600,7 +616,7 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.MapEnd) if (E.Type == GameEvent.EventType.MapEnd)
{ {
Logger.WriteInfo("Game ending..."); ServerLogger.LogInformation("Game ending...");
if (E.GameTime.HasValue) if (E.GameTime.HasValue)
{ {
@ -638,6 +654,7 @@ namespace IW4MAdmin
return true; return true;
} }
}
private async Task OnClientUpdate(EFClient origin) private async Task OnClientUpdate(EFClient origin)
{ {
@ -645,7 +662,7 @@ namespace IW4MAdmin
if (client == null) if (client == null)
{ {
Logger.WriteWarning($"{origin} expected to exist in client list for update, but they do not"); ServerLogger.LogWarning("{origin} expected to exist in client list for update, but they do not", origin.ToString());
return; return;
} }
@ -664,15 +681,17 @@ namespace IW4MAdmin
catch (Exception e) catch (Exception e)
{ {
Logger.WriteWarning($"Could not execute on join for {origin}"); using(LogContext.PushProperty("Server", ToString()))
Logger.WriteDebug(e.GetExceptionInfo()); {
ServerLogger.LogError(e, "Could not execute on join for {origin}", origin.ToString());
}
} }
} }
else if ((client.IPAddress != null && client.State == ClientState.Disconnecting) || else if ((client.IPAddress != null && client.State == ClientState.Disconnecting) ||
client.Level == Permission.Banned) client.Level == Permission.Banned)
{ {
Logger.WriteWarning($"{client} state is Unknown (probably kicked), but they are still connected. trying to kick again..."); ServerLogger.LogWarning("{client} state is Unknown (probably kicked), but they are still connected. trying to kick again...", origin.ToString());
await client.CanConnect(client.IPAddress); await client.CanConnect(client.IPAddress);
} }
} }
@ -686,9 +705,6 @@ namespace IW4MAdmin
/// <returns></returns> /// <returns></returns>
async Task<IList<EFClient>[]> PollPlayersAsync() async Task<IList<EFClient>[]> PollPlayersAsync()
{ {
#if DEBUG
var now = DateTime.Now;
#endif
var currentClients = GetClientsAsList(); var currentClients = GetClientsAsList();
var statusResponse = (await this.GetStatusAsync()); var statusResponse = (await this.GetStatusAsync());
var polledClients = statusResponse.Item1.AsEnumerable(); var polledClients = statusResponse.Item1.AsEnumerable();
@ -697,9 +713,6 @@ namespace IW4MAdmin
{ {
polledClients = polledClients.Where(c => !c.IsBot); polledClients = polledClients.Where(c => !c.IsBot);
} }
#if DEBUG
Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms");
#endif
var disconnectingClients = currentClients.Except(polledClients); var disconnectingClients = currentClients.Except(polledClients);
var connectingClients = polledClients.Except(currentClients); var connectingClients = polledClients.Except(currentClients);
var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients); var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients);
@ -763,9 +776,8 @@ namespace IW4MAdmin
DateTime playerCountStart = DateTime.Now; DateTime playerCountStart = DateTime.Now;
DateTime lastCount = DateTime.Now; DateTime lastCount = DateTime.Now;
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts) public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
{ {
bool notifyDisconnects = !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost;
try try
{ {
if (cts.IsCancellationRequested) if (cts.IsCancellationRequested)
@ -776,12 +788,10 @@ namespace IW4MAdmin
try try
{ {
#if DEBUG if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue && Utilities.IsDevelopment)
if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue)
{ {
return true; return true;
} }
#endif
var polledClients = await PollPlayersAsync(); var polledClients = await PollPlayersAsync();
@ -838,7 +848,7 @@ namespace IW4MAdmin
Manager.AddEvent(e); Manager.AddEvent(e);
} }
if (ConnectionErrors > 0) if (Throttled)
{ {
var _event = new GameEvent() var _event = new GameEvent()
{ {
@ -851,14 +861,12 @@ namespace IW4MAdmin
Manager.AddEvent(_event); Manager.AddEvent(_event);
} }
ConnectionErrors = 0;
LastPoll = DateTime.Now; LastPoll = DateTime.Now;
} }
catch (NetworkException e) catch (NetworkException e)
{ {
ConnectionErrors++; if (!Throttled)
if (ConnectionErrors == 3)
{ {
var _event = new GameEvent() var _event = new GameEvent()
{ {
@ -872,6 +880,7 @@ namespace IW4MAdmin
Manager.AddEvent(_event); Manager.AddEvent(_event);
} }
return true; return true;
} }
@ -916,24 +925,22 @@ namespace IW4MAdmin
} }
// this one is ok // this one is ok
catch (ServerException e) catch (Exception e) when(e is ServerException || e is RConException)
{ {
if (e is NetworkException && !Throttled && notifyDisconnects) using(LogContext.PushProperty("Server", ToString()))
{ {
Logger.WriteError(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}")); ServerLogger.LogWarning(e, "Undesirable exception occured during processing updates");
Logger.WriteDebug(e.GetExceptionInfo());
}
else
{
Logger.WriteError(e.Message);
} }
return false; return false;
} }
catch (Exception E) catch (Exception e)
{ {
Logger.WriteError(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]")); using(LogContext.PushProperty("Server", ToString()))
Logger.WriteDebug(E.GetExceptionInfo()); {
ServerLogger.LogError(e, "Unexpected exception occured during processing updates");
}
Console.WriteLine(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]"));
return false; return false;
} }
} }
@ -949,7 +956,7 @@ namespace IW4MAdmin
RconParser = RconParser ?? Manager.AdditionalRConParsers[0]; RconParser = RconParser ?? Manager.AdditionalRConParsers[0];
EventParser = EventParser ?? Manager.AdditionalEventParsers[0]; EventParser = EventParser ?? Manager.AdditionalEventParsers[0];
RemoteConnection.SetConfiguration(RconParser.Configuration); RemoteConnection.SetConfiguration(RconParser);
var version = await this.GetMappedDvarValueOrDefaultAsync<string>("version"); var version = await this.GetMappedDvarValueOrDefaultAsync<string>("version");
Version = version.Value; Version = version.Value;
@ -972,7 +979,7 @@ namespace IW4MAdmin
if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1") if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1")
{ {
throw new ServerException(loc["SERVER_ERROR_NOT_RUNNING"]); throw new ServerException(loc["SERVER_ERROR_NOT_RUNNING"].FormatExt(this.ToString()));
} }
var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null; var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null;
@ -1013,6 +1020,12 @@ namespace IW4MAdmin
Website = loc["SERVER_WEBSITE_GENERIC"]; Website = loc["SERVER_WEBSITE_GENERIC"];
} }
// todo: remove this once _website is weaned off
if (string.IsNullOrEmpty(Manager.GetApplicationSettings().Configuration().ContactUri))
{
Manager.GetApplicationSettings().Configuration().ContactUri = Website;
}
InitializeMaps(); InitializeMaps();
WorkingDirectory = basepath.Value; WorkingDirectory = basepath.Value;
@ -1043,8 +1056,9 @@ namespace IW4MAdmin
if (needsRestart) if (needsRestart)
{ {
Logger.WriteWarning("Game log file not properly initialized, restarting map..."); // disabling this for the time being
await this.ExecuteCommandAsync("map_restart"); /*Logger.WriteWarning("Game log file not properly initialized, restarting map...");
await this.ExecuteCommandAsync("map_restart");*/
} }
// this DVAR isn't set until the a map is loaded // this DVAR isn't set until the a map is loaded
@ -1076,16 +1090,21 @@ namespace IW4MAdmin
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
}; };
LogPath = GenerateLogPath(logInfo); LogPath = GenerateLogPath(logInfo);
ServerLogger.LogInformation("Game log information {@logInfo}", logInfo);
if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null) if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null)
{ {
Logger.WriteError(loc["SERVER_ERROR_DNE"].FormatExt(LogPath)); Console.WriteLine(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
ServerLogger.LogCritical("Game log path does not exist {logPath}", LogPath);
throw new ServerException(loc["SERVER_ERROR_DNE"].FormatExt(LogPath)); throw new ServerException(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
} }
} }
LogEvent = new GameLogEventDetection(this, GenerateUriForLog(LogPath, ServerConfig.GameLogServerUrl?.AbsoluteUri), gameLogReaderFactory); ServerLogger.LogInformation("Generated game log path is {logPath}", LogPath);
Logger.WriteInfo($"Log file is {LogPath}"); LogEvent = new GameLogEventDetection( _serviceProvider.GetRequiredService<ILogger<GameLogEventDetection>>(),
this,
GenerateUriForLog(LogPath, ServerConfig.GameLogServerUrl?.AbsoluteUri), gameLogReaderFactory);
_ = Task.Run(() => LogEvent.PollForChanges()); _ = Task.Run(() => LogEvent.PollForChanges());
@ -1164,8 +1183,8 @@ namespace IW4MAdmin
Link = targetClient.AliasLink Link = targetClient.AliasLink
}; };
Logger.WriteDebug($"Creating warn penalty for {targetClient}"); ServerLogger.LogDebug("Creating warn penalty for {targetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0)); await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame) if (targetClient.IsIngame)
{ {
@ -1175,12 +1194,13 @@ namespace IW4MAdmin
return; return;
} }
// todo: move to translation sheet
string message = $"^1{loc["SERVER_WARNING"]} ^7[^3{targetClient.Warnings}^7]: ^3{targetClient.Name}^7, {reason}"; string message = $"^1{loc["SERVER_WARNING"]} ^7[^3{targetClient.Warnings}^7]: ^3{targetClient.Name}^7, {reason}";
targetClient.CurrentServer.Broadcast(message); targetClient.CurrentServer.Broadcast(message);
} }
} }
public override async Task Kick(string Reason, EFClient targetClient, EFClient originClient) public override async Task Kick(string reason, EFClient targetClient, EFClient originClient, EFPenalty previousPenalty)
{ {
targetClient = targetClient.ClientNumber < 0 ? targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients() Manager.GetActiveClients()
@ -1192,13 +1212,13 @@ namespace IW4MAdmin
Type = EFPenalty.PenaltyType.Kick, Type = EFPenalty.PenaltyType.Kick,
Expires = DateTime.UtcNow, Expires = DateTime.UtcNow,
Offender = targetClient, Offender = targetClient,
Offense = Reason, Offense = reason,
Punisher = originClient, Punisher = originClient,
Link = targetClient.AliasLink Link = targetClient.AliasLink
}; };
Logger.WriteDebug($"Creating kick penalty for {targetClient}"); ServerLogger.LogDebug("Creating kick penalty for {targetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0)); await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame) if (targetClient.IsIngame)
{ {
@ -1211,7 +1231,11 @@ namespace IW4MAdmin
Manager.AddEvent(e); Manager.AddEvent(e);
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7"); var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
targetClient.ClientNumber,
_messageFormatter.BuildFormattedMessage(RconParser.Configuration,
newPenalty,
previousPenalty));
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick); await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
} }
} }
@ -1234,18 +1258,20 @@ namespace IW4MAdmin
Link = targetClient.AliasLink Link = targetClient.AliasLink
}; };
Logger.WriteDebug($"Creating tempban penalty for {targetClient}"); ServerLogger.LogDebug("Creating tempban penalty for {targetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0)); await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame) if (targetClient.IsIngame)
{ {
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}"); var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
Logger.WriteDebug($"Executing tempban kick command for {targetClient}"); targetClient.ClientNumber,
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
ServerLogger.LogDebug("Executing tempban kick command for {targetClient}", targetClient.ToString());
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick); await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
} }
} }
override public async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false) public override async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
{ {
// ensure player gets kicked if command not performed on them in the same server // ensure player gets kicked if command not performed on them in the same server
targetClient = targetClient.ClientNumber < 0 ? targetClient = targetClient.ClientNumber < 0 ?
@ -1264,14 +1290,16 @@ namespace IW4MAdmin
IsEvadedOffense = isEvade IsEvadedOffense = isEvade
}; };
Logger.WriteDebug($"Creating ban penalty for {targetClient}"); ServerLogger.LogDebug("Creating ban penalty for {targetClient}", targetClient.ToString());
targetClient.SetLevel(Permission.Banned, originClient); targetClient.SetLevel(Permission.Banned, originClient);
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0)); await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame) if (targetClient.IsIngame)
{ {
Logger.WriteDebug($"Attempting to kicking newly banned client {targetClient}"); ServerLogger.LogDebug("Attempting to kicking newly banned client {targetClient}", targetClient.ToString());
string formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7"); var formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
targetClient.ClientNumber,
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString); await targetClient.CurrentServer.ExecuteCommandAsync(formattedString);
} }
} }
@ -1290,6 +1318,7 @@ namespace IW4MAdmin
Link = Target.AliasLink Link = Target.AliasLink
}; };
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", Target.ToString());
Target.SetLevel(Permission.User, Origin); Target.SetLevel(Permission.User, Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId); await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId);
await Manager.GetPenaltyService().Create(unbanPenalty); await Manager.GetPenaltyService().Create(unbanPenalty);

View File

@ -6,15 +6,22 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Localization namespace IW4MAdmin.Application.Localization
{ {
public class Configure public static class Configure
{ {
public static ITranslationLookup Initialize(bool useLocalTranslation, IMasterApi apiInstance, string customLocale = null) public static ITranslationLookup Initialize(ILogger logger, IMasterApi apiInstance, ApplicationConfiguration applicationConfiguration)
{ {
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale; var useLocalTranslation = applicationConfiguration?.UseLocalTranslations ?? true;
string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json"); var customLocale = applicationConfiguration?.EnableCustomLocale ?? false
? (applicationConfiguration.CustomLocale ?? "en-US")
: "en-US";
var currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
var localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
if (!useLocalTranslation) if (!useLocalTranslation)
{ {
@ -25,9 +32,10 @@ namespace IW4MAdmin.Application.Localization
return localization.LocalizationIndex; return localization.LocalizationIndex;
} }
catch (Exception) catch (Exception ex)
{ {
// the online localization failed so will default to local files // the online localization failed so will default to local files
logger.LogWarning(ex, "Could not download latest translations");
} }
} }
@ -55,18 +63,20 @@ namespace IW4MAdmin.Application.Localization
{ {
var localizationContents = File.ReadAllText(filePath, Encoding.UTF8); var localizationContents = File.ReadAllText(filePath, Encoding.UTF8);
var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents); var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents);
if (eachLocalizationFile == null)
{
continue;
}
foreach (var item in eachLocalizationFile.LocalizationIndex.Set) foreach (var item in eachLocalizationFile.LocalizationIndex.Set)
{ {
if (!localizationDict.TryAdd(item.Key, item.Value)) if (!localizationDict.TryAdd(item.Key, item.Value))
{ {
Program.ServerManager.GetLogger(0).WriteError($"Could not add locale string {item.Key} to localization"); logger.LogError("Could not add locale string {key} to localization", item.Key);
} }
} }
} }
string localizationFile = $"{Path.Join(Utilities.OperatingDirectory, "Localization")}{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocale}-{currentLocale.ToUpper()}.json";
Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict) Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict)
{ {
LocalizationName = currentLocale, LocalizationName = currentLocale,

View File

@ -1,7 +1,6 @@
using IW4MAdmin.Application.API.Master; using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Factories; using IW4MAdmin.Application.Factories;
using IW4MAdmin.Application.Helpers;
using IW4MAdmin.Application.Meta; using IW4MAdmin.Application.Meta;
using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
@ -24,6 +23,10 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Localization;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
@ -65,8 +68,11 @@ namespace IW4MAdmin.Application
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e) private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
{ {
ServerManager?.Stop(); ServerManager?.Stop();
if (ApplicationTask != null)
{
await ApplicationTask; await ApplicationTask;
} }
}
/// <summary> /// <summary>
/// task that initializes application and starts the application monitoring and runtime tasks /// task that initializes application and starts the application monitoring and runtime tasks
@ -76,29 +82,33 @@ namespace IW4MAdmin.Application
{ {
restart: restart:
ITranslationLookup translationLookup = null; ITranslationLookup translationLookup = null;
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
Utilities.DefaultLogger = logger;
IServiceCollection services = null;
logger.LogInformation("Begin IW4MAdmin startup. Version is {version} {@args}", Version, args);
try try
{ {
// do any needed housekeeping file/folder migrations // do any needed housekeeping file/folder migrations
ConfigurationMigration.MoveConfigFolder10518(null); ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories(); ConfigurationMigration.CheckDirectories();
logger.LogDebug("Configuring services...");
var services = ConfigureServices(args); services = ConfigureServices(args);
serviceProvider = services.BuildServiceProvider(); serviceProvider = services.BuildServiceProvider();
var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>(); var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>();
ServerManager = (ApplicationManager)serviceProvider.GetRequiredService<IManager>(); ServerManager = (ApplicationManager)serviceProvider.GetRequiredService<IManager>();
translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>(); translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>();
ServerManager.Logger.WriteInfo(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_VERSION"].FormatExt(Version));
await versionChecker.CheckVersion(); await versionChecker.CheckVersion();
await ServerManager.Init(); await ServerManager.Init();
} }
catch (Exception e) catch (Exception e)
{ {
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"]; string failMessage = translationLookup == null ? "Failed to initialize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
string exitMessage = translationLookup == null ? "Press enter to exit..." : translationLookup["MANAGER_EXIT"]; string exitMessage = translationLookup == null ? "Press enter to exit..." : translationLookup["MANAGER_EXIT"];
logger.LogCritical(e, "Failed to initialize IW4MAdmin");
Console.WriteLine(failMessage); Console.WriteLine(failMessage);
while (e.InnerException != null) while (e.InnerException != null)
@ -131,13 +141,14 @@ namespace IW4MAdmin.Application
try try
{ {
ApplicationTask = RunApplicationTasksAsync(); ApplicationTask = RunApplicationTasksAsync(logger, services);
await ApplicationTask; await ApplicationTask;
} }
catch (Exception e) catch (Exception e)
{ {
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"]; logger.LogCritical(e, "Failed to launch IW4MAdmin");
string failMessage = translationLookup == null ? "Failed to launch IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}"); Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}");
} }
@ -153,15 +164,15 @@ namespace IW4MAdmin.Application
/// runs the core application tasks /// runs the core application tasks
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private static async Task RunApplicationTasksAsync() private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
{ {
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ? var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ?
WebfrontCore.Program.Init(ServerManager, serviceProvider, ServerManager.CancellationToken) : WebfrontCore.Program.Init(ServerManager, serviceProvider, services, ServerManager.CancellationToken) :
Task.CompletedTask; Task.CompletedTask;
// we want to run this one on a manual thread instead of letting the thread pool handle it, // 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 // because we can't exit early from waiting on console input, and it prevents us from restarting
var inputThread = new Thread(async () => await ReadConsoleInput()); var inputThread = new Thread(async () => await ReadConsoleInput(logger));
inputThread.Start(); inputThread.Start();
var tasks = new[] var tasks = new[]
@ -171,9 +182,11 @@ namespace IW4MAdmin.Application
serviceProvider.GetRequiredService<IMasterCommunication>().RunUploadStatus(ServerManager.CancellationToken) serviceProvider.GetRequiredService<IMasterCommunication>().RunUploadStatus(ServerManager.CancellationToken)
}; };
logger.LogDebug("Starting webfront and input tasks");
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
ServerManager.Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]); logger.LogInformation("Shutdown completed successfully");
Console.Write(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
} }
@ -181,11 +194,11 @@ namespace IW4MAdmin.Application
/// reads input from the console and executes entered commands on the default server /// reads input from the console and executes entered commands on the default server
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private static async Task ReadConsoleInput() private static async Task ReadConsoleInput(ILogger logger)
{ {
if (Console.IsInputRedirected) if (Console.IsInputRedirected)
{ {
ServerManager.Logger.WriteInfo("Disabling console input as it has been redirected"); logger.LogInformation("Disabling console input as it has been redirected");
return; return;
} }
@ -221,68 +234,29 @@ namespace IW4MAdmin.Application
{ } { }
} }
/// <summary> private static IServiceCollection HandlePluginRegistration(ApplicationConfiguration appConfig,
/// Configures the dependency injection services IServiceCollection serviceCollection,
/// </summary> IMasterApi masterApi)
private static IServiceCollection ConfigureServices(string[] args)
{ {
var defaultLogger = new Logger("IW4MAdmin-Manager"); var defaultLogger = BuildDefaultLogger<Program>(appConfig);
var pluginImporter = new PluginImporter(defaultLogger); var pluginServiceProvider = new ServiceCollection()
.AddBaseLogger(appConfig)
var serviceCollection = new ServiceCollection(); .AddSingleton(appConfig)
serviceCollection.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection) .AddSingleton(masterApi)
.AddSingleton(new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings") as IConfigurationHandler<ApplicationConfiguration>) .AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
.AddSingleton(new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as IConfigurationHandler<CommandConfiguration>)
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration() ?? new ApplicationConfiguration())
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>().Configuration() ?? new CommandConfiguration())
.AddSingleton<ILogger>(_serviceProvider => defaultLogger)
.AddSingleton<IPluginImporter, PluginImporter>() .AddSingleton<IPluginImporter, PluginImporter>()
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>() .BuildServiceProvider();
.AddSingleton<IRConConnectionFactory, RConConnectionFactory>()
.AddSingleton<IGameServerInstanceFactory, GameServerInstanceFactory>()
.AddSingleton<IConfigurationHandlerFactory, ConfigurationHandlerFactory>()
.AddSingleton<IParserRegexFactory, ParserRegexFactory>()
.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>()
.AddSingleton<IGameLogReaderFactory, GameLogReaderFactory>()
.AddSingleton<IScriptCommandFactory, ScriptCommandFactory>()
.AddSingleton<IAuditInformationRepository, AuditInformationRepository>()
.AddSingleton<IEntityService<EFClient>, ClientService>()
.AddSingleton<IMetaService, MetaService>()
.AddSingleton<IMetaRegistration, MetaRegistration>()
.AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>, ReceivedPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>, AdministeredPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>, UpdatedAliasResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton(_serviceProvider =>
{
var config = _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration();
return Localization.Configure.Initialize(useLocalTranslation: config?.UseLocalTranslations ?? false,
apiInstance: _serviceProvider.GetRequiredService<IMasterApi>(),
customLocale: config?.EnableCustomLocale ?? false ? (config.CustomLocale ?? "en-US") : "en-US");
})
.AddSingleton<IManager, ApplicationManager>()
.AddSingleton(_serviceProvider => RestClient
.For<IMasterApi>(Utilities.IsDevelopment ? new Uri("http://127.0.0.1:8080") : _serviceProvider
.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration()?.MasterUrl ??
new ApplicationConfiguration().MasterUrl))
.AddSingleton<IMasterCommunication, MasterCommunication>();
if (args.Contains("serialevents")) var pluginImporter = pluginServiceProvider.GetRequiredService<IPluginImporter>();
{
serviceCollection.AddSingleton<IEventHandler, SerialGameEventHandler>(); // we need to register the rest client with regular collection
} serviceCollection.AddSingleton(masterApi);
else
{
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
}
// register the native commands // register the native commands
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes() foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
.Where(_command => _command.BaseType == typeof(Command))) .Where(_command => _command.BaseType == typeof(Command)))
{ {
defaultLogger.WriteInfo($"Registered native command type {commandType.Name}"); defaultLogger.LogDebug("Registered native command type {name}", commandType.Name);
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType); serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
} }
@ -290,14 +264,14 @@ namespace IW4MAdmin.Application
var pluginImplementations = pluginImporter.DiscoverAssemblyPluginImplementations(); var pluginImplementations = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in pluginImplementations.Item1) foreach (var pluginType in pluginImplementations.Item1)
{ {
defaultLogger.WriteInfo($"Registered plugin type {pluginType.FullName}"); defaultLogger.LogDebug("Registered plugin type {name}", pluginType.FullName);
serviceCollection.AddSingleton(typeof(IPlugin), pluginType); serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
} }
// register the plugin commands // register the plugin commands
foreach (var commandType in pluginImplementations.Item2) foreach (var commandType in pluginImplementations.Item2)
{ {
defaultLogger.WriteInfo($"Registered plugin command type {commandType.FullName}"); defaultLogger.LogDebug("Registered plugin command type {name}", commandType.FullName);
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType); serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
} }
@ -321,5 +295,96 @@ namespace IW4MAdmin.Application
return serviceCollection; return serviceCollection;
} }
/// <summary>
/// Configures the dependency injection services
/// </summary>
private static IServiceCollection ConfigureServices(string[] args)
{
// setup the static resources (config/master api/translations)
var serviceCollection = new ServiceCollection();
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
var appConfig = appConfigHandler.Configuration();
var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080")
: appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
var masterRestClient = RestClient.For<IMasterApi>(masterUri);
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
if (appConfig == null)
{
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
appConfigHandler.Set(appConfig);
appConfigHandler.Save();
}
// build the dependency list
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
serviceCollection
.AddBaseLogger(appConfig)
.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection)
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
.AddSingleton(
new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as
IConfigurationHandler<CommandConfiguration>)
.AddSingleton(appConfig)
.AddSingleton(_serviceProvider =>
_serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
.Configuration() ?? new CommandConfiguration())
.AddSingleton<IPluginImporter, PluginImporter>()
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
.AddSingleton<IRConConnectionFactory, RConConnectionFactory>()
.AddSingleton<IGameServerInstanceFactory, GameServerInstanceFactory>()
.AddSingleton<IConfigurationHandlerFactory, ConfigurationHandlerFactory>()
.AddSingleton<IParserRegexFactory, ParserRegexFactory>()
.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>()
.AddSingleton<IGameLogReaderFactory, GameLogReaderFactory>()
.AddSingleton<IScriptCommandFactory, ScriptCommandFactory>()
.AddSingleton<IAuditInformationRepository, AuditInformationRepository>()
.AddSingleton<IEntityService<EFClient>, ClientService>()
.AddSingleton<IMetaService, MetaService>()
.AddSingleton<ClientService>()
.AddSingleton<PenaltyService>()
.AddSingleton<ChangeHistoryService>()
.AddSingleton<IMetaRegistration, MetaRegistration>()
.AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>,
ReceivedPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>,
AdministeredPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>,
UpdatedAliasResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
.AddSingleton<IMasterCommunication, MasterCommunication>()
.AddSingleton<IManager, ApplicationManager>()
.AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>()
.AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);
if (args.Contains("serialevents"))
{
serviceCollection.AddSingleton<IEventHandler, SerialGameEventHandler>();
}
else
{
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
}
return serviceCollection;
}
private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig)
{
var collection = new ServiceCollection()
.AddBaseLogger(appConfig)
.BuildServiceProvider();
return collection.GetRequiredService<ILogger<T>>();
}
} }
} }

View File

@ -1,11 +1,13 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses; using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper; using SharedLibraryCore.QueryHelper;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta namespace IW4MAdmin.Application.Meta
{ {
@ -18,7 +20,7 @@ namespace IW4MAdmin.Application.Meta
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
public AdministeredPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory) public AdministeredPenaltyResourceQueryHelper(ILogger<AdministeredPenaltyResourceQueryHelper> logger, IDatabaseContextFactory contextFactory)
{ {
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
@ -26,7 +28,7 @@ namespace IW4MAdmin.Application.Meta
public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query) public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{ {
using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var iqPenalties = ctx.Penalties.AsNoTracking() var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => query.ClientId == _penalty.PunisherId) .Where(_penalty => query.ClientId == _penalty.PunisherId)

View File

@ -6,6 +6,8 @@ using SharedLibraryCore.QueryHelper;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta namespace IW4MAdmin.Application.Meta
{ {
@ -19,7 +21,7 @@ namespace IW4MAdmin.Application.Meta
private readonly IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> _administeredPenaltyHelper; private readonly IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> _administeredPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> _updatedAliasHelper; private readonly IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> _updatedAliasHelper;
public MetaRegistration(ILogger logger, IMetaService metaService, ITranslationLookup transLookup, IEntityService<EFClient> clientEntityService, public MetaRegistration(ILogger<MetaRegistration> logger, IMetaService metaService, ITranslationLookup transLookup, IEntityService<EFClient> clientEntityService,
IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> receivedPenaltyHelper, IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> receivedPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> administeredPenaltyHelper, IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> administeredPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper) IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper)
@ -82,7 +84,7 @@ namespace IW4MAdmin.Application.Meta
if (client == null) if (client == null)
{ {
_logger.WriteWarning($"No client found with id {request.ClientId} when generating profile meta"); _logger.LogWarning("No client found with id {clientId} when generating profile meta", request.ClientId);
return metaList; return metaList;
} }

View File

@ -2,12 +2,14 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses; using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper; using SharedLibraryCore.QueryHelper;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta namespace IW4MAdmin.Application.Meta
{ {
@ -20,7 +22,7 @@ namespace IW4MAdmin.Application.Meta
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
public ReceivedPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory) public ReceivedPenaltyResourceQueryHelper(ILogger<ReceivedPenaltyResourceQueryHelper> logger, IDatabaseContextFactory contextFactory)
{ {
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
@ -29,7 +31,7 @@ namespace IW4MAdmin.Application.Meta
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query) public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{ {
var linkedPenaltyType = Utilities.LinkedPenaltyTypes(); var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var linkId = await ctx.Clients.AsNoTracking() var linkId = await ctx.Clients.AsNoTracking()
.Where(_client => _client.ClientId == query.ClientId) .Where(_client => _client.ClientId == query.ClientId)

View File

@ -6,6 +6,8 @@ using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper; using SharedLibraryCore.QueryHelper;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta namespace IW4MAdmin.Application.Meta
{ {
@ -18,7 +20,7 @@ namespace IW4MAdmin.Application.Meta
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
public UpdatedAliasResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory) public UpdatedAliasResourceQueryHelper(ILogger<UpdatedAliasResourceQueryHelper> logger, IDatabaseContextFactory contextFactory)
{ {
_logger = logger; _logger = logger;
_contextFactory = contextFactory; _contextFactory = contextFactory;
@ -26,7 +28,7 @@ namespace IW4MAdmin.Application.Meta
public async Task<ResourceQueryHelperResult<UpdatedAliasResponse>> QueryResource(ClientPaginationRequest query) public async Task<ResourceQueryHelperResult<UpdatedAliasResponse>> QueryResource(ClientPaginationRequest query)
{ {
using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId; int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId;
var iqAliasUpdates = ctx.Aliases var iqAliasUpdates = ctx.Aliases

View File

@ -1,11 +1,8 @@
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using ILogger = Microsoft.Extensions.Logging.ILogger;
using System.Text.RegularExpressions;
namespace IW4MAdmin.Application.Migration namespace IW4MAdmin.Application.Migration
{ {
@ -56,7 +53,6 @@ namespace IW4MAdmin.Application.Migration
if (!Directory.Exists(configDirectory)) if (!Directory.Exists(configDirectory))
{ {
log?.WriteDebug($"Creating directory for configs {configDirectory}");
Directory.CreateDirectory(configDirectory); Directory.CreateDirectory(configDirectory);
} }
@ -66,7 +62,6 @@ namespace IW4MAdmin.Application.Migration
foreach (var configFile in configurationFiles) foreach (var configFile in configurationFiles)
{ {
log?.WriteDebug($"Moving config file {configFile}");
string destinationPath = Path.Join("Configuration", configFile); string destinationPath = Path.Join("Configuration", configFile);
if (!File.Exists(destinationPath)) if (!File.Exists(destinationPath))
{ {
@ -77,7 +72,6 @@ namespace IW4MAdmin.Application.Migration
if (!File.Exists(Path.Join("Database", "Database.db")) && if (!File.Exists(Path.Join("Database", "Database.db")) &&
File.Exists("Database.db")) File.Exists("Database.db"))
{ {
log?.WriteDebug("Moving database file");
File.Move("Database.db", Path.Join("Database", "Database.db")); File.Move("Database.db", Path.Join("Database", "Database.db"));
} }
} }

View File

@ -0,0 +1,24 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore.Database;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Migration
{
public static class DatabaseHousekeeping
{
private static readonly DateTime CutoffDate = DateTime.UtcNow.AddMonths(-6);
public static async Task RemoveOldRatings(IDatabaseContextFactory contextFactory, CancellationToken token)
{
await using var context = contextFactory.CreateContext();
var dbSet = context.Set<EFRating>();
var itemsToDelete = dbSet.Where(rating => rating.When <= CutoffDate);
dbSet.RemoveRange(itemsToDelete);
await context.SaveChangesAsync(token);
}
}
}

View File

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IClientNoticeMessageFormatter
/// </summary>
public class ClientNoticeMessageFormatter : IClientNoticeMessageFormatter
{
private readonly ITranslationLookup _transLookup;
private readonly ApplicationConfiguration _appConfig;
public ClientNoticeMessageFormatter(ITranslationLookup transLookup, ApplicationConfiguration appConfig)
{
_transLookup = transLookup;
_appConfig = appConfig;
}
public string BuildFormattedMessage(IRConParserConfiguration config, EFPenalty currentPenalty, EFPenalty originalPenalty = null)
{
var isNewLineSeparator = config.NoticeLineSeparator == Environment.NewLine;
var penalty = originalPenalty ?? currentPenalty;
var builder = new StringBuilder();
// build the top level header
var header = _transLookup[$"SERVER_{penalty.Type.ToString().ToUpper()}_TEXT"];
builder.Append(header);
builder.Append(config.NoticeLineSeparator);
// build the reason
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense);
if (isNewLineSeparator)
{
foreach (var splitReason in SplitOverMaxLength(reason, config.NoticeMaxCharactersPerLine))
{
builder.Append(splitReason);
builder.Append(config.NoticeLineSeparator);
}
}
else
{
builder.Append(reason);
builder.Append(config.NoticeLineSeparator);
}
if (penalty.Type == EFPenalty.PenaltyType.TempBan)
{
// build the time remaining if temporary
var timeRemainingValue = penalty.Expires.HasValue
? (penalty.Expires - DateTime.UtcNow).Value.HumanizeForCurrentCulture()
: "--";
var timeRemaining = _transLookup["GAME_MESSAGE_PENALTY_TIME_REMAINING"].FormatExt(timeRemainingValue);
if (isNewLineSeparator)
{
foreach (var splitReason in SplitOverMaxLength(timeRemaining, config.NoticeMaxCharactersPerLine))
{
builder.Append(splitReason);
builder.Append(config.NoticeLineSeparator);
}
}
else
{
builder.Append(timeRemaining);
}
}
if (penalty.Type == EFPenalty.PenaltyType.Ban)
{
// provide a place to appeal the ban (should always be specified but including a placeholder just incase)
builder.Append(_transLookup["GAME_MESSAGE_PENALTY_APPEAL"].FormatExt(_appConfig.ContactUri ?? "--"));
}
// final format looks something like:
/*
* You are permanently banned
* Reason - toxic behavior
* Visit example.com to appeal
*/
return builder.ToString();
}
private static IEnumerable<string> SplitOverMaxLength(string source, int maxCharactersPerLine)
{
if (source.Length <= maxCharactersPerLine)
{
return new[] {source};
}
var segments = new List<string>();
var currentLocation = 0;
while (currentLocation < source.Length)
{
var nextLocation = currentLocation + maxCharactersPerLine;
// there's probably a more efficient way to do this but this is readable
segments.Add(string.Concat(
source
.Skip(currentLocation)
.Take(Math.Min(maxCharactersPerLine, source.Length - currentLocation))));
currentLocation = nextLocation;
}
if (currentLocation < source.Length)
{
segments.Add(source.Substring(currentLocation, source.Length - currentLocation));
}
return segments;
}
}
}

View File

@ -1,63 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
namespace IW4MAdmin.Application.Misc
{
internal class EventPerformance
{
public long ExecutionTime { get; set; }
public GameEvent Event { get; set; }
public string EventInfo => $"{Event.Type}, {Event.FailReason}, {Event.IsBlocking}, {Event.Data}, {Event.Message}, {Event.Extra}";
}
public class DuplicateKeyComparer<TKey> : IComparer<TKey> where TKey : IComparable
{
public int Compare(TKey x, TKey y)
{
int result = x.CompareTo(y);
if (result == 0)
return 1;
else
return result;
}
}
internal class EventProfiler
{
public double AverageEventTime { get; private set; }
public double MaxEventTime => Events.Values.Last().ExecutionTime;
public double MinEventTime => Events.Values[0].ExecutionTime;
public int TotalEventCount => Events.Count;
public SortedList<long, EventPerformance> Events { get; private set; } = new SortedList<long, EventPerformance>(new DuplicateKeyComparer<long>());
private readonly ILogger _logger;
public EventProfiler(ILogger logger)
{
_logger = logger;
}
public void Profile(DateTime start, DateTime end, GameEvent gameEvent)
{
_logger.WriteDebug($"Starting profile of event {gameEvent.Id}");
long executionTime = (long)Math.Round((end - start).TotalMilliseconds);
var perf = new EventPerformance()
{
Event = gameEvent,
ExecutionTime = executionTime
};
lock (Events)
{
Events.Add(executionTime, perf);
}
AverageEventTime = (AverageEventTime * (TotalEventCount - 1) + executionTime) / TotalEventCount;
_logger.WriteDebug($"Finished profile of event {gameEvent.Id}");
}
}
}

View File

@ -1,132 +1,47 @@
using IW4MAdmin.Application.IO; using System;
using SharedLibraryCore; using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces; using ILogger = SharedLibraryCore.Interfaces.ILogger;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
[Obsolete]
public class Logger : ILogger public class Logger : ILogger
{ {
enum LogType private readonly Microsoft.Extensions.Logging.ILogger _logger;
public Logger(ILogger<Logger> logger)
{ {
Verbose, _logger = logger;
Info,
Debug,
Warning,
Error,
Assert
}
readonly string FileName;
readonly ReaderWriterLockSlim WritingLock;
static readonly short MAX_LOG_FILES = 10;
public Logger(string fn)
{
FileName = Path.Join(Utilities.OperatingDirectory, "Log", $"{fn}.log");
WritingLock = new ReaderWriterLockSlim();
RotateLogs();
}
~Logger()
{
WritingLock.Dispose();
}
/// <summary>
/// rotates logs when log is initialized
/// </summary>
private void RotateLogs()
{
string maxLog = FileName + MAX_LOG_FILES;
if (File.Exists(maxLog))
{
File.Delete(maxLog);
}
for (int i = MAX_LOG_FILES - 1; i >= 0; i--)
{
string logToMove = i == 0 ? FileName : FileName + i;
string movedLogName = FileName + (i + 1);
if (File.Exists(logToMove))
{
File.Move(logToMove, movedLogName);
}
}
}
void Write(string msg, LogType type)
{
WritingLock.EnterWriteLock();
string stringType = type.ToString();
msg = msg.StripColors();
try
{
stringType = Utilities.CurrentLocalization.LocalizationIndex[$"GLOBAL_{type.ToString().ToUpper()}"];
}
catch (Exception) { }
string LogLine = $"[{DateTime.Now.ToString("MM.dd.yyy HH:mm:ss.fff")}] - {stringType}: {msg}";
try
{
#if DEBUG
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
Console.WriteLine(msg);
#else
if (type == LogType.Error || type == LogType.Verbose)
{
Console.WriteLine(LogLine);
}
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
#endif
}
catch (Exception ex)
{
Console.WriteLine("Well.. It looks like your machine can't event write to the log file. That's something else...");
Console.WriteLine(ex.GetExceptionInfo());
}
WritingLock.ExitWriteLock();
} }
public void WriteVerbose(string msg) public void WriteVerbose(string msg)
{ {
Write(msg, LogType.Verbose); _logger.LogInformation(msg);
} }
public void WriteDebug(string msg) public void WriteDebug(string msg)
{ {
Write(msg, LogType.Debug); _logger.LogDebug(msg);
} }
public void WriteError(string msg) public void WriteError(string msg)
{ {
Write(msg, LogType.Error); _logger.LogError(msg);
} }
public void WriteInfo(string msg) public void WriteInfo(string msg)
{ {
Write(msg, LogType.Info); WriteVerbose(msg);
} }
public void WriteWarning(string msg) public void WriteWarning(string msg)
{ {
Write(msg, LogType.Warning); _logger.LogWarning(msg);
} }
public void WriteAssert(bool condition, string msg) public void WriteAssert(bool condition, string msg)
{ {
if (!condition) throw new NotImplementedException();
Write(msg, LogType.Assert);
} }
} }
} }

View File

@ -8,6 +8,8 @@ using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -24,10 +26,9 @@ namespace IW4MAdmin.Application.Misc
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
private readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99"); private readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99");
private readonly int _apiVersion = 1; private readonly int _apiVersion = 1;
private bool firstHeartBeat = true; private bool firstHeartBeat = true;
public MasterCommunication(ILogger logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager) public MasterCommunication(ILogger<MasterCommunication> logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager)
{ {
_logger = logger; _logger = logger;
_transLookup = translationLookup; _transLookup = translationLookup;
@ -55,13 +56,7 @@ namespace IW4MAdmin.Application.Misc
catch (Exception e) catch (Exception e)
{ {
_logger.WriteWarning(_transLookup["MANAGER_VERSION_FAIL"]); _logger.LogWarning(e, "Unable to retrieve IW4MAdmin version information");
while (e.InnerException != null)
{
e = e.InnerException;
}
_logger.WriteDebug(e.Message);
} }
if (version.CurrentVersionStable == _fallbackVersion) if (version.CurrentVersionStable == _fallbackVersion)
@ -110,12 +105,12 @@ namespace IW4MAdmin.Application.Misc
catch (System.Net.Http.HttpRequestException e) catch (System.Net.Http.HttpRequestException e)
{ {
_logger.WriteWarning($"Could not send heartbeat - {e.Message}"); _logger.LogWarning(e, "Could not send heartbeat");
} }
catch (AggregateException e) catch (AggregateException e)
{ {
_logger.WriteWarning($"Could not send heartbeat - {e.Message}"); _logger.LogWarning(e, "Could not send heartbeat");
var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(ApiException)); var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(ApiException));
foreach (var ex in exceptions) foreach (var ex in exceptions)
@ -129,7 +124,7 @@ namespace IW4MAdmin.Application.Misc
catch (ApiException e) catch (ApiException e)
{ {
_logger.WriteWarning($"Could not send heartbeat - {e.Message}"); _logger.LogWarning(e, "Could not send heartbeat");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized) if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{ {
connected = false; connected = false;
@ -138,7 +133,7 @@ namespace IW4MAdmin.Application.Misc
catch (Exception e) catch (Exception e)
{ {
_logger.WriteWarning($"Could not send heartbeat - {e.Message}"); _logger.LogWarning(e, "Could not send heartbeat");
} }
@ -202,7 +197,7 @@ namespace IW4MAdmin.Application.Misc
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK) if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
{ {
_logger.WriteWarning($"Response code from master is {response.ResponseMessage.StatusCode}, message is {response.StringContent}"); _logger.LogWarning("Non success response code from master is {statusCode}, message is {message}", response.ResponseMessage.StatusCode, response.StringContent);
} }
} }
} }

View File

@ -7,6 +7,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -20,7 +22,7 @@ namespace IW4MAdmin.Application.Misc
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger; private readonly ILogger _logger;
public MetaService(ILogger logger, IDatabaseContextFactory contextFactory) public MetaService(ILogger<MetaService> logger, IDatabaseContextFactory contextFactory)
{ {
_logger = logger; _logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>(); _metaActions = new Dictionary<MetaType, List<dynamic>>();
@ -35,7 +37,7 @@ namespace IW4MAdmin.Application.Misc
return; return;
} }
using var ctx = _contextFactory.CreateContext(); await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta var existingMeta = await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey) .Where(_meta => _meta.Key == metaKey)
@ -64,7 +66,7 @@ namespace IW4MAdmin.Application.Misc
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client) public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
{ {
using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
return await ctx.EFMeta return await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey) .Where(_meta => _meta.Key == metaKey)

View File

@ -1,8 +1,9 @@
using SharedLibraryCore; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -11,7 +12,7 @@ namespace IW4MAdmin.Application.Misc
private readonly IDictionary<string, IList<object>> _actions; private readonly IDictionary<string, IList<object>> _actions;
private readonly ILogger _logger; private readonly ILogger _logger;
public MiddlewareActionHandler(ILogger logger) public MiddlewareActionHandler(ILogger<MiddlewareActionHandler> logger)
{ {
_actions = new Dictionary<string, IList<object>>(); _actions = new Dictionary<string, IList<object>>();
_logger = logger; _logger = logger;
@ -38,8 +39,7 @@ namespace IW4MAdmin.Application.Misc
} }
catch (Exception e) catch (Exception e)
{ {
_logger.WriteWarning($"Failed to invoke middleware action {name}"); _logger.LogWarning(e, "Failed to invoke middleware action {name}", name);
_logger.WriteDebug(e.GetExceptionInfo());
} }
} }

View File

@ -5,9 +5,12 @@ using System.Reflection;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Linq; using System.Linq;
using SharedLibraryCore; using SharedLibraryCore;
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.API.Master;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Helpers namespace IW4MAdmin.Application.Misc
{ {
/// <summary> /// <summary>
/// implementation of IPluginImporter /// implementation of IPluginImporter
@ -15,12 +18,19 @@ namespace IW4MAdmin.Application.Helpers
/// </summary> /// </summary>
public class PluginImporter : IPluginImporter public class PluginImporter : IPluginImporter
{ {
private IEnumerable<PluginSubscriptionContent> _pluginSubscription;
private static readonly string PLUGIN_DIR = "Plugins"; private static readonly string PLUGIN_DIR = "Plugins";
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IRemoteAssemblyHandler _remoteAssemblyHandler;
private readonly IMasterApi _masterApi;
private readonly ApplicationConfiguration _appConfig;
public PluginImporter(ILogger logger) public PluginImporter(ILogger<PluginImporter> logger, ApplicationConfiguration appConfig, IMasterApi masterApi, IRemoteAssemblyHandler remoteAssemblyHandler)
{ {
_logger = logger; _logger = logger;
_masterApi = masterApi;
_remoteAssemblyHandler = remoteAssemblyHandler;
_appConfig = appConfig;
} }
/// <summary> /// <summary>
@ -33,16 +43,16 @@ namespace IW4MAdmin.Application.Helpers
if (Directory.Exists(pluginDir)) if (Directory.Exists(pluginDir))
{ {
string[] scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js"); var scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts());
_logger.WriteInfo($"Discovered {scriptPluginFiles.Length} potential script plugins"); _logger.LogDebug("Discovered {count} potential script plugins", scriptPluginFiles.Count());
if (scriptPluginFiles.Length > 0) if (scriptPluginFiles.Count() > 0)
{ {
foreach (string fileName in scriptPluginFiles) foreach (string fileName in scriptPluginFiles)
{ {
_logger.WriteInfo($"Discovered script plugin {fileName}"); _logger.LogDebug("Discovered script plugin {fileName}", fileName);
var plugin = new ScriptPlugin(fileName); var plugin = new ScriptPlugin(_logger, fileName);
yield return plugin; yield return plugin;
} }
} }
@ -62,27 +72,70 @@ namespace IW4MAdmin.Application.Helpers
if (Directory.Exists(pluginDir)) if (Directory.Exists(pluginDir))
{ {
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll"); var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
_logger.WriteInfo($"Discovered {dllFileNames.Length} potential plugin assemblies"); _logger.LogDebug("Discovered {count} potential plugin assemblies", dllFileNames.Length);
if (dllFileNames.Length > 0) if (dllFileNames.Length > 0)
{ {
var assemblies = dllFileNames.Select(_name => Assembly.LoadFrom(_name)); // 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 pluginTypes = assemblies
.SelectMany(_asm => _asm.GetTypes()) .SelectMany(_asm => _asm.GetTypes())
.Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null); .Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null);
_logger.WriteInfo($"Discovered {pluginTypes.Count()} plugin implementations"); _logger.LogDebug("Discovered {count} plugin implementations", pluginTypes.Count());
commandTypes = assemblies commandTypes = assemblies
.SelectMany(_asm => _asm.GetTypes()) .SelectMany(_asm => _asm.GetTypes())
.Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command)); .Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command));
_logger.WriteInfo($"Discovered {commandTypes.Count()} plugin commands"); _logger.LogDebug("Discovered {count} plugin commands", commandTypes.Count());
} }
} }
return (pluginTypes, commandTypes); return (pluginTypes, commandTypes);
} }
private IEnumerable<Assembly> GetRemoteAssemblies()
{
try
{
if (_pluginSubscription == null)
_pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptAssemblies(_pluginSubscription.Where(sub => sub.Type == PluginType.Binary).Select(sub => sub.Content).ToArray());
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not load remote assemblies");
return Enumerable.Empty<Assembly>();
}
}
private IEnumerable<string> GetRemoteScripts()
{
try
{
if (_pluginSubscription == null)
_pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());
}
catch (Exception ex)
{
_logger.LogWarning(ex,"Could not load remote scripts");
return Enumerable.Empty<string>();
}
}
}
public enum PluginType
{
Binary,
Script
} }
} }

View File

@ -0,0 +1,76 @@
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
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 readonly ApplicationConfiguration _appconfig;
private readonly ILogger _logger;
public RemoteAssemblyHandler(ILogger<RemoteAssemblyHandler> logger, ApplicationConfiguration appconfig)
{
_appconfig = appconfig;
_logger = logger;
}
public IEnumerable<Assembly> DecryptAssemblies(string[] encryptedAssemblies)
{
return DecryptContent(encryptedAssemblies)
.Select(decryptedAssembly => Assembly.Load(decryptedAssembly));
}
public IEnumerable<string> DecryptScripts(string[] encryptedScripts)
{
return DecryptContent(encryptedScripts).Select(decryptedScript => Encoding.UTF8.GetString(decryptedScript));
}
private byte[][] DecryptContent(string[] content)
{
if (string.IsNullOrEmpty(_appconfig.Id) || string.IsNullOrWhiteSpace(_appconfig.SubscriptionId))
{
_logger.LogWarning($"{nameof(_appconfig.Id)} and {nameof(_appconfig.SubscriptionId)} must be provided to attempt loading remote assemblies/scripts");
return new byte[0][];
}
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 keyGen = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(_appconfig.SubscriptionId), Encoding.UTF8.GetBytes(_appconfig.Id.ToString()), iterationCount, HashAlgorithmName.SHA512);
var encryption = new AesGcm(keyGen.GetBytes(keyLength));
try
{
encryption.Decrypt(nonce, encryptedContent, tag, decryptedContent);
}
catch (CryptographicException ex)
{
_logger.LogError(ex, "Could not decrypt remote plugin assemblies");
}
return decryptedContent;
});
return assemblies.ToArray();
}
}
}

View File

@ -4,7 +4,9 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -14,13 +16,15 @@ namespace IW4MAdmin.Application.Misc
public class ScriptCommand : Command public class ScriptCommand : Command
{ {
private readonly Action<GameEvent> _executeAction; private readonly Action<GameEvent> _executeAction;
private readonly ILogger _logger;
public ScriptCommand(string name, string alias, string description, bool isTargetRequired, Permission permission, public ScriptCommand(string name, string alias, string description, bool isTargetRequired, Permission permission,
CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout) CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout, ILogger<ScriptCommand> logger)
: base(config, layout) : base(config, layout)
{ {
_executeAction = executeAction; _executeAction = executeAction;
_logger = logger;
Name = name; Name = name;
Alias = alias; Alias = alias;
Description = description; Description = description;
@ -29,14 +33,21 @@ namespace IW4MAdmin.Application.Misc
Arguments = args; Arguments = args;
} }
public override Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent e)
{ {
if (_executeAction == null) if (_executeAction == null)
{ {
throw new InvalidOperationException($"No execute action defined for command \"{Name}\""); throw new InvalidOperationException($"No execute action defined for command \"{Name}\"");
} }
return Task.Run(() => _executeAction(E)); try
{
await Task.Run(() => _executeAction(e));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to execute ScriptCommand action for command {command} {@event}", Name, e);
}
} }
} }
} }

View File

@ -1,4 +1,5 @@
using Jint; using System;
using Jint;
using Jint.Native; using Jint.Native;
using Jint.Runtime; using Jint.Runtime;
using Microsoft.CSharp.RuntimeBinder; using Microsoft.CSharp.RuntimeBinder;
@ -12,6 +13,9 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -39,9 +43,11 @@ namespace IW4MAdmin.Application.Misc
private readonly SemaphoreSlim _onProcessing; private readonly SemaphoreSlim _onProcessing;
private bool successfullyLoaded; private bool successfullyLoaded;
private readonly List<string> _registeredCommandNames; private readonly List<string> _registeredCommandNames;
private readonly ILogger _logger;
public ScriptPlugin(string filename, string workingDirectory = null) public ScriptPlugin(ILogger logger, string filename, string workingDirectory = null)
{ {
_logger = logger;
_fileName = filename; _fileName = filename;
Watcher = new FileSystemWatcher() Watcher = new FileSystemWatcher()
{ {
@ -84,7 +90,7 @@ namespace IW4MAdmin.Application.Misc
foreach (string commandName in _registeredCommandNames) foreach (string commandName in _registeredCommandNames)
{ {
manager.GetLogger(0).WriteDebug($"Removing plugin registered command \"{commandName}\""); _logger.LogDebug("Removing plugin registered command {command}", commandName);
manager.RemoveCommandByName(commandName); manager.RemoveCommandByName(commandName);
} }
@ -112,7 +118,28 @@ namespace IW4MAdmin.Application.Misc
}) })
.CatchClrExceptions()); .CatchClrExceptions());
try
{
_scriptEngine.Execute(script); _scriptEngine.Execute(script);
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} at {@locationInfo}",
nameof(Initialize), _fileName, ex.Location);
throw new PluginException($"A JavaScript parsing error occured while initializing script plugin");
}
catch (Exception e)
{
_logger.LogError(e,
"Encountered unexpected error while running {methodName} for script plugin {plugin}",
nameof(Initialize), _fileName);
throw new PluginException($"An unexpected error occured while initialization script plugin");
}
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization); _scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver); _scriptEngine.SetValue("_serviceResolver", serviceResolver);
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject(); dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
@ -129,7 +156,7 @@ namespace IW4MAdmin.Application.Misc
{ {
foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory)) foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory))
{ {
manager.GetLogger(0).WriteDebug($"Adding plugin registered command \"{command.Name}\""); _logger.LogDebug("Adding plugin registered command {commandName}", command.Name);
manager.AddAdditionalCommand(command); manager.AddAdditionalCommand(command);
_registeredCommandNames.Add(command.Name); _registeredCommandNames.Add(command.Name);
} }
@ -167,12 +194,20 @@ namespace IW4MAdmin.Application.Misc
catch (JavaScriptException ex) catch (JavaScriptException ex)
{ {
throw new PluginException($"An error occured while initializing script plugin: {ex.Error} (Line: {ex.Location.Start.Line}, Character: {ex.Location.Start.Column})") { PluginFile = _fileName }; _logger.LogError(ex,
"Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} initialization {@locationInfo}",
nameof(OnLoadAsync), _fileName, ex.Location);
throw new PluginException("An error occured while initializing script plugin");
} }
catch catch (Exception ex)
{ {
throw; _logger.LogError(ex,
"Encountered unexpected error while running {methodName} for script plugin {plugin}",
nameof(OnLoadAsync), _fileName);
throw new PluginException("An unexpected error occured while initializing script plugin");
} }
finally finally
@ -198,9 +233,28 @@ namespace IW4MAdmin.Application.Misc
_scriptEngine.Execute("plugin.onEventAsync(_gameEvent, _server)").GetCompletionValue(); _scriptEngine.Execute("plugin.onEventAsync(_gameEvent, _server)").GetCompletionValue();
} }
catch catch (JavaScriptException ex)
{ {
throw; using (LogContext.PushProperty("Server", S.ToString()))
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} with event type {eventType} {@locationInfo}",
nameof(OnEventAsync), _fileName, E.Type, ex.Location);
}
throw new PluginException($"An error occured while executing action for script plugin");
}
catch (Exception e)
{
using (LogContext.PushProperty("Server", S.ToString()))
{
_logger.LogError(e,
"Encountered unexpected error while running {methodName} for script plugin {plugin} with event type {eventType}",
nameof(OnEventAsync), _fileName, E.Type);
}
throw new PluginException($"An error occured while executing action for script plugin");
} }
finally finally
@ -215,7 +269,7 @@ namespace IW4MAdmin.Application.Misc
public Task OnLoadAsync(IManager manager) public Task OnLoadAsync(IManager manager)
{ {
manager.GetLogger(0).WriteDebug($"OnLoad executing for {Name}"); _logger.LogDebug("OnLoad executing for {name}", Name);
_scriptEngine.SetValue("_manager", manager); _scriptEngine.SetValue("_manager", manager);
return Task.FromResult(_scriptEngine.Execute("plugin.onLoadAsync(_manager)").GetCompletionValue()); return Task.FromResult(_scriptEngine.Execute("plugin.onLoadAsync(_manager)").GetCompletionValue());
} }

View File

@ -18,7 +18,7 @@ namespace IW4MAdmin.Application.RCon
} }
public int ConnectionAttempts { get; set; } public int ConnectionAttempts { get; set; }
const int BufferSize = 4096; private const int BufferSize = 16384;
public readonly byte[] ReceiveBuffer = new byte[BufferSize]; public readonly byte[] ReceiveBuffer = new byte[BufferSize];
public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1); public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1);
public readonly ManualResetEventSlim OnSentData = new ManualResetEventSlim(false); public readonly ManualResetEventSlim OnSentData = new ManualResetEventSlim(false);

View File

@ -10,7 +10,11 @@ using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.RCon namespace IW4MAdmin.Application.RCon
{ {
@ -23,11 +27,12 @@ namespace IW4MAdmin.Application.RCon
public IPEndPoint Endpoint { get; private set; } public IPEndPoint Endpoint { get; private set; }
public string RConPassword { get; private set; } public string RConPassword { get; private set; }
private IRConParser parser;
private IRConParserConfiguration config; private IRConParserConfiguration config;
private readonly ILogger _log; private readonly ILogger _log;
private readonly Encoding _gameEncoding; private readonly Encoding _gameEncoding;
public RConConnection(string ipAddress, int port, string password, ILogger log, Encoding gameEncoding) public RConConnection(string ipAddress, int port, string password, ILogger<RConConnection> log, Encoding gameEncoding)
{ {
Endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), port); Endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
_gameEncoding = gameEncoding; _gameEncoding = gameEncoding;
@ -35,9 +40,10 @@ namespace IW4MAdmin.Application.RCon
_log = log; _log = log;
} }
public void SetConfiguration(IRConParserConfiguration config) public void SetConfiguration(IRConParser parser)
{ {
this.config = config; this.parser = parser;
config = parser.Configuration;
} }
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "")
@ -49,9 +55,8 @@ namespace IW4MAdmin.Application.RCon
var connectionState = ActiveQueries[this.Endpoint]; var connectionState = ActiveQueries[this.Endpoint];
#if DEBUG == true _log.LogDebug("Waiting for semaphore to be released [{endpoint}]", Endpoint);
_log.WriteDebug($"Waiting for semaphore to be released [{this.Endpoint}]");
#endif
// enter the semaphore so only one query is sent at a time per server. // enter the semaphore so only one query is sent at a time per server.
await connectionState.OnComplete.WaitAsync(); await connectionState.OnComplete.WaitAsync();
@ -64,10 +69,8 @@ namespace IW4MAdmin.Application.RCon
connectionState.LastQuery = DateTime.Now; connectionState.LastQuery = DateTime.Now;
#if DEBUG == true _log.LogDebug("Semaphore has been released [{endpoint}]", Endpoint);
_log.WriteDebug($"Semaphore has been released [{this.Endpoint}]"); _log.LogDebug("Query {@queryInfo}", new { endpoint=Endpoint.ToString(), type, parameters });
_log.WriteDebug($"Query [{this.Endpoint},{type.ToString()},{parameters}]");
#endif
byte[] payload = null; byte[] payload = null;
bool waitForResponse = config.WaitForResponse; bool waitForResponse = config.WaitForResponse;
@ -112,18 +115,34 @@ namespace IW4MAdmin.Application.RCon
// this happens when someone tries to send something that can't be converted into a 7 bit character set // this happens when someone tries to send something that can't be converted into a 7 bit character set
// e.g: emoji -> windows-1252 // e.g: emoji -> windows-1252
catch (OverflowException) catch (OverflowException ex)
{ {
connectionState.OnComplete.Release(1); connectionState.OnComplete.Release(1);
throw new NetworkException($"Invalid character encountered when converting encodings - {parameters}"); using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogError(ex, "Could not convert RCon data payload to desired encoding {encoding} {params}",
_gameEncoding.EncodingName, parameters);
}
throw new RConException($"Invalid character encountered when converting encodings");
} }
byte[][] response = null; byte[][] response = null;
retrySend: retrySend:
if (connectionState.ConnectionAttempts > 1)
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogInformation(
"Retrying RCon message ({connectionAttempts}/{allowedConnectionFailures} attempts) with parameters {payload}",
connectionState.ConnectionAttempts,
StaticHelpers.AllowedConnectionFails, parameters);
}
}
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
{ {
DontFragment = true, DontFragment = false,
Ttl = 100, Ttl = 100,
ExclusiveAddressUse = true, ExclusiveAddressUse = true,
}) })
@ -133,16 +152,19 @@ namespace IW4MAdmin.Application.RCon
connectionState.OnReceivedData.Reset(); connectionState.OnReceivedData.Reset();
connectionState.ConnectionAttempts++; connectionState.ConnectionAttempts++;
connectionState.BytesReadPerSegment.Clear(); connectionState.BytesReadPerSegment.Clear();
#if DEBUG == true bool exceptionCaught = false;
_log.WriteDebug($"Sending {payload.Length} bytes to [{this.Endpoint}] ({connectionState.ConnectionAttempts}/{StaticHelpers.AllowedConnectionFails})");
#endif _log.LogDebug("Sending {payloadLength} bytes to [{endpoint}] ({connectionAttempts}/{allowedConnectionFailures})",
payload.Length, Endpoint, connectionState.ConnectionAttempts, StaticHelpers.AllowedConnectionFails);
try try
{ {
response = await SendPayloadAsync(payload, waitForResponse);
response = await SendPayloadAsync(payload, waitForResponse, parser.OverrideTimeoutForCommand(parameters));
if ((response.Length == 0 || response[0].Length == 0) && waitForResponse) if ((response.Length == 0 || response[0].Length == 0) && waitForResponse)
{ {
throw new NetworkException("Expected response but got 0 bytes back"); throw new RConException("Expected response but got 0 bytes back");
} }
connectionState.ConnectionAttempts = 0; connectionState.ConnectionAttempts = 0;
@ -150,18 +172,28 @@ namespace IW4MAdmin.Application.RCon
catch catch
{ {
// we want to retry with a delay
if (connectionState.ConnectionAttempts < StaticHelpers.AllowedConnectionFails) if (connectionState.ConnectionAttempts < StaticHelpers.AllowedConnectionFails)
{ {
await Task.Delay(StaticHelpers.FloodProtectionInterval); exceptionCaught = true;
await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts));
goto retrySend; goto retrySend;
} }
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"].FormatExt(Endpoint)); using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning(
"Made {connectionAttempts} attempts to send RCon data to server, but received no response",
connectionState.ConnectionAttempts);
}
connectionState.ConnectionAttempts = 0;
throw new NetworkException("Reached maximum retry attempts to send RCon data to server");
} }
finally finally
{ {
if (connectionState.OnComplete.CurrentCount == 0) // we don't want to release if we're going to retry the query
if (connectionState.OnComplete.CurrentCount == 0 && !exceptionCaught)
{ {
connectionState.OnComplete.Release(1); connectionState.OnComplete.Release(1);
} }
@ -170,23 +202,22 @@ namespace IW4MAdmin.Application.RCon
if (response.Length == 0) if (response.Length == 0)
{ {
_log.WriteWarning($"Received empty response for request [{type.ToString()}, {parameters}, {Endpoint.ToString()}]"); _log.LogDebug("Received empty response for RCon request {@query}", new { endpoint=Endpoint.ToString(), type, parameters });
return new string[0]; return new string[0];
} }
string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ? string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ?
ReassembleSegmentedStatus(response) : ReassembleSegmentedStatus(response) : RecombineMessages(response);
_gameEncoding.GetString(response[0]) + '\n';
// note: not all games respond if the pasword is wrong or not set // note: not all games respond if the pasword is wrong or not set
if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword")) if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword"))
{ {
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]); throw new RConException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]);
} }
if (responseString.Contains("rcon_password")) if (responseString.Contains("rcon_password"))
{ {
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_NOTSET"]); throw new RConException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_NOTSET"]);
} }
if (responseString.Contains(config.ServerNotRunningResponse)) if (responseString.Contains(config.ServerNotRunningResponse))
@ -199,7 +230,13 @@ namespace IW4MAdmin.Application.RCon
if (headerSplit.Length != 2) if (headerSplit.Length != 2)
{ {
throw new NetworkException("Unexpected response header from server"); using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning("Invalid response header from server. Expected {expected}, but got {response}",
config.CommandPrefixes.RConResponse, headerSplit.FirstOrDefault());
}
throw new RConException("Unexpected response header from server");
} }
string[] splitResponse = headerSplit.Last().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); string[] splitResponse = headerSplit.Last().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
@ -234,7 +271,36 @@ namespace IW4MAdmin.Application.RCon
return string.Join("", splitStatusStrings); return string.Join("", splitStatusStrings);
} }
private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse) /// <summary>
/// Recombines multiple game messages into one
/// </summary>
/// <param name="payload"></param>
/// <returns></returns>
private string RecombineMessages(byte[][] payload)
{
if (payload.Length == 1)
{
return _gameEncoding.GetString(payload[0]).TrimEnd('\n') + '\n';
}
else
{
var builder = new StringBuilder();
for (int i = 0; i < payload.Length; i++)
{
string message = _gameEncoding.GetString(payload[i]).TrimEnd('\n') + '\n';
if (i > 0)
{
message = message.Replace(config.CommandPrefixes.RConResponse, "");
}
builder.Append(message);
}
builder.Append('\n');
return builder.ToString();
}
}
private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse, TimeSpan overrideTimeout)
{ {
var connectionState = ActiveQueries[this.Endpoint]; var connectionState = ActiveQueries[this.Endpoint];
var rconSocket = (Socket)connectionState.SendEventArgs.UserToken; var rconSocket = (Socket)connectionState.SendEventArgs.UserToken;
@ -258,11 +324,17 @@ namespace IW4MAdmin.Application.RCon
if (sendDataPending) if (sendDataPending)
{ {
// the send has not been completed asyncronously // the send has not been completed asynchronously
if (!await Task.Run(() => connectionState.OnSentData.Wait(StaticHelpers.SocketTimeout))) // this really shouldn't ever happen because it's UDP
if (!await Task.Run(() => connectionState.OnSentData.Wait(StaticHelpers.SocketTimeout(1))))
{ {
using(LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning("Socket timed out while sending RCon data on attempt {attempt}",
connectionState.ConnectionAttempts);
}
rconSocket.Close(); rconSocket.Close();
throw new NetworkException("Timed out sending data", rconSocket); throw new NetworkException("Timed out sending RCon data", rconSocket);
} }
} }
@ -278,15 +350,37 @@ namespace IW4MAdmin.Application.RCon
if (receiveDataPending) if (receiveDataPending)
{ {
if (!await Task.Run(() => connectionState.OnReceivedData.Wait(10000))) _log.LogDebug("Waiting to asynchronously receive data on attempt #{connectionAttempts}", connectionState.ConnectionAttempts);
if (!await Task.Run(() => connectionState.OnReceivedData.Wait(
new[]
{ {
StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts),
overrideTimeout
}.Max())))
{
if (connectionState.ConnectionAttempts > 1) // this reduces some spam for unstable connections
{
using (LogContext.PushProperty("Server", Endpoint.ToString()))
{
_log.LogWarning(
"Socket timed out while waiting for RCon response on attempt {attempt} with timeout delay of {timeout}",
connectionState.ConnectionAttempts,
StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts));
}
}
rconSocket.Close(); rconSocket.Close();
throw new NetworkException("Timed out waiting for response", rconSocket); throw new NetworkException("Timed out receiving RCon response", rconSocket);
} }
} }
rconSocket.Close(); rconSocket.Close();
return GetResponseData(connectionState);
}
private byte[][] GetResponseData(ConnectionState connectionState)
{
var responseList = new List<byte[]>(); var responseList = new List<byte[]>();
int totalBytesRead = 0; int totalBytesRead = 0;
@ -305,58 +399,71 @@ namespace IW4MAdmin.Application.RCon
private void OnDataReceived(object sender, SocketAsyncEventArgs e) private void OnDataReceived(object sender, SocketAsyncEventArgs e)
{ {
#if DEBUG == true _log.LogDebug("Read {bytesTransferred} bytes from {endpoint}", e.BytesTransferred, e.RemoteEndPoint);
_log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}");
#endif
// this occurs when we close the socket // this occurs when we close the socket
if (e.BytesTransferred == 0) if (e.BytesTransferred == 0)
{ {
_log.LogDebug("No bytes were transmitted so the connection was probably closed");
ActiveQueries[this.Endpoint].OnReceivedData.Set(); ActiveQueries[this.Endpoint].OnReceivedData.Set();
return; return;
} }
if (sender is Socket sock) if (!(sender is Socket sock))
{ {
return;
}
var state = ActiveQueries[this.Endpoint]; var state = ActiveQueries[this.Endpoint];
state.BytesReadPerSegment.Add(e.BytesTransferred); state.BytesReadPerSegment.Add(e.BytesTransferred);
// I don't even want to know why this works for getting more data from Cod4x
// but I'm leaving it in here as long as it doesn't break anything.
// it's very stupid...
Thread.Sleep(150);
try try
{ {
var totalBytesTransferred = e.BytesTransferred;
_log.LogDebug("{total} total bytes transferred with {available} bytes remaining", totalBytesTransferred, sock.Available);
// we still have available data so the payload was segmented // we still have available data so the payload was segmented
if (sock.Available > 0) while (sock.Available > 0)
{ {
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, e.BytesTransferred, state.ReceiveBuffer.Length - e.BytesTransferred); _log.LogDebug("{available} more bytes to be read", sock.Available);
if (!sock.ReceiveAsync(state.ReceiveEventArgs)) var bufferSpaceAvailable = sock.Available + totalBytesTransferred - state.ReceiveBuffer.Length;
if (bufferSpaceAvailable >= 0 )
{ {
#if DEBUG == true _log.LogWarning("Not enough buffer space to store incoming data {bytesNeeded} additional bytes required", bufferSpaceAvailable);
_log.WriteDebug($"Read {state.ReceiveEventArgs.BytesTransferred} synchronous bytes from {e.RemoteEndPoint.ToString()}"); }
#endif
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, totalBytesTransferred, sock.Available);
if (sock.ReceiveAsync(state.ReceiveEventArgs))
{
_log.LogDebug("Remaining bytes are async");
continue;
}
_log.LogDebug("Read {bytesTransferred} synchronous bytes from {endpoint}", state.ReceiveEventArgs.BytesTransferred, e.RemoteEndPoint);
// we need to increment this here because the callback isn't executed if there's no pending IO // we need to increment this here because the callback isn't executed if there's no pending IO
state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred); state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred);
ActiveQueries[this.Endpoint].OnReceivedData.Set(); totalBytesTransferred += state.ReceiveEventArgs.BytesTransferred;
}
} }
else
{
ActiveQueries[this.Endpoint].OnReceivedData.Set(); ActiveQueries[this.Endpoint].OnReceivedData.Set();
} }
}
catch (ObjectDisposedException) catch (ObjectDisposedException)
{ {
_log.LogDebug("Socket was disposed while receiving data");
ActiveQueries[this.Endpoint].OnReceivedData.Set(); ActiveQueries[this.Endpoint].OnReceivedData.Set();
} }
} }
}
private void OnDataSent(object sender, SocketAsyncEventArgs e) private void OnDataSent(object sender, SocketAsyncEventArgs e)
{ {
#if DEBUG == true _log.LogDebug("Sent {byteCount} bytes to {endpoint}", e.Buffer?.Length, e.ConnectSocket?.RemoteEndPoint);
_log.WriteDebug($"Sent {e.Buffer?.Length} bytes to {e.ConnectSocket?.RemoteEndPoint?.ToString()}");
#endif
ActiveQueries[this.Endpoint].OnSentData.Set(); ActiveQueries[this.Endpoint].OnSentData.Set();
} }
} }

View File

@ -8,14 +8,19 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.RconParsers namespace IW4MAdmin.Application.RconParsers
{ {
public class BaseRConParser : IRConParser public class BaseRConParser : IRConParser
{ {
public BaseRConParser(IParserRegexFactory parserRegexFactory) private readonly ILogger _logger;
public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory)
{ {
_logger = logger;
Configuration = new DynamicRConParserConfiguration(parserRegexFactory) Configuration = new DynamicRConParserConfiguration(parserRegexFactory)
{ {
CommandPrefixes = new CommandPrefix() CommandPrefixes = new CommandPrefix()
@ -76,7 +81,22 @@ namespace IW4MAdmin.Application.RconParsers
public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default) public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default)
{ {
string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName); string[] lineSplit;
try
{
lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName);
}
catch
{
if (fallbackValue == null)
{
throw;
}
lineSplit = new string[0];
}
string response = string.Join('\n', lineSplit).TrimEnd('\0'); string response = string.Join('\n', lineSplit).TrimEnd('\0');
var match = Regex.Match(response, Configuration.Dvar.Pattern); var match = Regex.Match(response, Configuration.Dvar.Pattern);
@ -118,12 +138,7 @@ namespace IW4MAdmin.Application.RconParsers
public virtual async Task<(List<EFClient>, string, string)> GetStatusAsync(IRConConnection connection) public virtual async Task<(List<EFClient>, string, string)> GetStatusAsync(IRConConnection connection)
{ {
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS); string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
#if DEBUG _logger.LogDebug("Status Response {response}", string.Join(Environment.NewLine, response));
foreach (var line in response)
{
Console.WriteLine(line);
}
#endif
return (ClientsFromStatus(response), MapFromStatus(response), GameTypeFromStatus(response)); return (ClientsFromStatus(response), MapFromStatus(response), GameTypeFromStatus(response));
} }
@ -190,6 +205,12 @@ namespace IW4MAdmin.Application.RconParsers
if (match.Success) if (match.Success)
{ {
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]] == "ZMBI")
{
_logger.LogDebug("Ignoring detected client {client} because they are zombie state", string.Join(",", match.Values));
continue;
}
int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]); int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]);
int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]); int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]);
@ -204,12 +225,13 @@ namespace IW4MAdmin.Application.RconParsers
long networkId; long networkId;
string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString; string networkIdString;
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
try try
{ {
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]]; networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkId = networkIdString.IsBotGuid() ? networkId = networkIdString.IsBotGuid() || (ip == null && ping == 999) ?
name.GenerateGuidFromString() : name.GenerateGuidFromString() :
networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
} }
@ -219,8 +241,6 @@ namespace IW4MAdmin.Application.RconParsers
continue; continue;
} }
int? ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
var client = new EFClient() var client = new EFClient()
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias()
@ -263,5 +283,16 @@ namespace IW4MAdmin.Application.RconParsers
public T GetDefaultDvarValue<T>(string dvarName) => Configuration.DefaultDvarValues.ContainsKey(dvarName) ? public T GetDefaultDvarValue<T>(string dvarName) => Configuration.DefaultDvarValues.ContainsKey(dvarName) ?
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) : (T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
default; default;
public TimeSpan OverrideTimeoutForCommand(string command)
{
if (command.Contains("map_rotate", StringComparison.InvariantCultureIgnoreCase) ||
command.StartsWith("map ", StringComparison.InvariantCultureIgnoreCase))
{
return TimeSpan.FromSeconds(30);
}
return TimeSpan.Zero;
}
} }
} }

View File

@ -1,4 +1,5 @@
using SharedLibraryCore.Interfaces; using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.RconParsers namespace IW4MAdmin.Application.RconParsers
{ {
@ -8,7 +9,7 @@ namespace IW4MAdmin.Application.RconParsers
/// </summary> /// </summary>
sealed internal class DynamicRConParser : BaseRConParser sealed internal class DynamicRConParser : BaseRConParser
{ {
public DynamicRConParser(IParserRegexFactory parserRegexFactory) : base(parserRegexFactory) public DynamicRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory) : base(logger, parserRegexFactory)
{ {
} }
} }

View File

@ -1,4 +1,5 @@
using SharedLibraryCore.Interfaces; using System;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon; using SharedLibraryCore.RCon;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
@ -22,6 +23,9 @@ namespace IW4MAdmin.Application.RconParsers
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber; public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>();
public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>();
public int NoticeMaximumLines { get; set; } = 8;
public int NoticeMaxCharactersPerLine { get; set; } = 50;
public string NoticeLineSeparator { get; set; } = Environment.NewLine;
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory) public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
{ {

View File

@ -1,8 +1,11 @@
name: '$(Date:yyyy.MM.dd)$(Rev:.r)'
trigger: trigger:
batch: true batch: true
branches: branches:
include: include:
- release/pre - release/pre
- master
pr: none pr: none
@ -12,110 +15,132 @@ pool:
variables: variables:
solution: 'IW4MAdmin.sln' solution: 'IW4MAdmin.sln'
buildPlatform: 'Any CPU' buildPlatform: 'Any CPU'
buildConfiguration: 'Prerelease'
outputFolder: '$(Build.ArtifactStagingDirectory)\Publish\$(buildConfiguration)' outputFolder: '$(Build.ArtifactStagingDirectory)\Publish\$(buildConfiguration)'
releaseType: verified
buildConfiguration: Stable
isPreRelease: false
steps: steps:
- 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 - task: NuGetCommand@2
displayName: 'Restore nuget packages'
inputs: inputs:
restoreSolution: '$(solution)' restoreSolution: '$(solution)'
- task: PowerShell@2 - task: PowerShell@2
displayName: 'Preload external resources'
inputs: inputs:
targetType: 'inline' targetType: 'inline'
script: | script: |
Write-Host 'Build Configuration is $(buildConfiguration), Release Type is $(releaseType)'
md -Force lib\open-iconic\font\css 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.scss wget https://raw.githubusercontent.com/iconic/open-iconic/master/font/css/open-iconic-bootstrap.scss -o lib\open-iconic\font\css\open-iconic-bootstrap.scss
failOnStderr: true failOnStderr: true
workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore\wwwroot' workingDirectory: '$(Build.Repository.LocalPath)\WebfrontCore\wwwroot'
- task: projectversionasvariable@1
inputs:
path: '$(Build.Repository.LocalPath)\Application\Application.csproj'
- task: VSBuild@1 - task: VSBuild@1
displayName: 'Build projects'
inputs: inputs:
solution: '$(solution)' solution: '$(solution)'
msbuildArgs: '/p:DeployOnBuild=false /p:PackageAsSingleFile=false /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)" /p:Version=$(Version.Major).$(Version.Minor).$(Version.Build).$(Build.BuildId)' msbuildArgs: '/p:DeployOnBuild=false /p:PackageAsSingleFile=false /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)" /p:Version=$(Build.BuildNumber)'
platform: '$(buildPlatform)' platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)' configuration: '$(buildConfiguration)'
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Publish projects'
inputs: inputs:
command: 'publish' command: 'publish'
publishWebProjects: false publishWebProjects: false
projects: | projects: |
**/WebfrontCore.csproj **/WebfrontCore.csproj
**/Application.csproj **/Application.csproj
arguments: '-c $(buildConfiguration) -o $(outputFolder) /p:Version=$(Version.Major).$(Version.Minor).$(Version.Build).$(Build.BuildId)' arguments: '-c $(buildConfiguration) -o $(outputFolder) /p:Version=$(Build.BuildNumber)'
zipAfterPublish: false zipAfterPublish: false
modifyOutputPath: false modifyOutputPath: false
- task: PowerShell@2 - task: PowerShell@2
displayName: 'Run publish script 1'
inputs: inputs:
targetType: 'inline' filePath: 'DeploymentFiles/PostPublish.ps1'
script: 'wget https://raidmax.org/downloads/dos2unix.exe'
failOnStderr: true
workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts'
- task: PowerShell@2
inputs:
filePath: 'PostPublish.ps1'
arguments: '$(outputFolder)' arguments: '$(outputFolder)'
failOnStderr: true failOnStderr: true
workingDirectory: '$(Build.Repository.LocalPath)' workingDirectory: '$(Build.Repository.LocalPath)'
- task: BatchScript@1 - task: BatchScript@1
displayName: 'Run publish script 2'
inputs: inputs:
filename: 'Application\BuildScripts\PostPublish.bat' filename: 'Application\BuildScripts\PostPublish.bat'
workingFolder: '$(Build.Repository.LocalPath)' workingFolder: '$(Build.Repository.LocalPath)'
arguments: '$(outputFolder)' arguments: '$(outputFolder)'
failOnStandardError: true 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 - task: CmdLine@2
displayName: 'Convert Linux start script line endings'
inputs: inputs:
script: | script: |
echo changing to encoding for linux start script echo changing to encoding for linux start script
dos2unix $(outputFolder)\StartIW4MAdmin.sh dos2unix $(outputFolder)\StartIW4MAdmin.sh
echo creating website version filename echo creating website version filename
@echo IW4MAdmin-$(Version.Major).$(Version.Minor)-$(buildConfiguration)$(Version.Build)b$(Build.BuildId) > $(Build.ArtifactStagingDirectory)\version_prerelease.txt @echo IW4MAdmin-$(Build.BuildNumber) > $(Build.ArtifactStagingDirectory)\version_$(releaseType).txt
workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts' workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts'
- task: CopyFiles@2 - task: CopyFiles@2
displayName: 'Move script plugins into publish directory'
inputs: inputs:
SourceFolder: '$(Build.Repository.LocalPath)\Plugins\ScriptPlugins' SourceFolder: '$(Build.Repository.LocalPath)\Plugins\ScriptPlugins'
Contents: '*.js' Contents: '*.js'
TargetFolder: '$(outputFolder)\Plugins' TargetFolder: '$(outputFolder)\Plugins'
- task: CopyFiles@2 - task: CopyFiles@2
displayName: 'Move binary plugins into publish directory'
inputs: inputs:
SourceFolder: '$(Build.Repository.LocalPath)\BUILD\Plugins\' SourceFolder: '$(Build.Repository.LocalPath)\BUILD\Plugins\'
Contents: '*.dll' Contents: '*.dll'
TargetFolder: '$(outputFolder)\Plugins' TargetFolder: '$(outputFolder)\Plugins'
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Move webfront resources into publish directory'
inputs: inputs:
script: 'xcopy /s /y /f wwwroot $(outputFolder)\wwwroot' script: 'xcopy /s /y /f wwwroot $(outputFolder)\wwwroot'
workingDirectory: '$(Build.Repository.LocalPath)\BUILD\Plugins' workingDirectory: '$(Build.Repository.LocalPath)\BUILD\Plugins'
failOnStderr: true failOnStderr: true
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Move gamescript files into publish directory'
inputs: inputs:
script: 'echo d | xcopy /s /y /f GameFiles $(outputFolder)\GameFiles' script: 'echo d | xcopy /s /y /f GameFiles $(outputFolder)\GameFiles'
workingDirectory: '$(Build.Repository.LocalPath)' workingDirectory: '$(Build.Repository.LocalPath)'
failOnStderr: true failOnStderr: true
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: 'Generate final zip file'
inputs: inputs:
rootFolderOrFile: '$(outputFolder)' rootFolderOrFile: '$(outputFolder)'
includeRootFolder: false includeRootFolder: false
archiveType: 'zip' archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Version.Major).$(Version.Minor)-$(buildConfiguration)$(Version.Build)b$(Build.BuildId).zip' archiveFile: '$(Build.ArtifactStagingDirectory)/IW4MAdmin-$(Build.BuildNumber).zip'
replaceExistingArchive: true replaceExistingArchive: true
- task: FtpUpload@2 - task: FtpUpload@2
displayName: 'Upload zip file to website'
inputs: inputs:
credentialsOption: 'inputs' credentialsOption: 'inputs'
serverUrl: '$(FTPUrl)' serverUrl: '$(FTPUrl)'
@ -130,13 +155,14 @@ steps:
trustSSL: false trustSSL: false
- task: FtpUpload@2 - task: FtpUpload@2
displayName: 'Upload version info to website'
inputs: inputs:
credentialsOption: 'inputs' credentialsOption: 'inputs'
serverUrl: '$(FTPUrl)' serverUrl: '$(FTPUrl)'
username: '$(FTPUsername)' username: '$(FTPUsername)'
password: '$(FTPPassword)' password: '$(FTPPassword)'
rootDirectory: '$(Build.ArtifactStagingDirectory)' rootDirectory: '$(Build.ArtifactStagingDirectory)'
filePatterns: 'version_prerelease.txt' filePatterns: 'version_$(releaseType).txt'
remoteDirectory: 'IW4MAdmin' remoteDirectory: 'IW4MAdmin'
clean: false clean: false
cleanContents: false cleanContents: false
@ -144,27 +170,29 @@ steps:
trustSSL: false trustSSL: false
- task: GitHubRelease@1 - task: GitHubRelease@1
displayName: 'Make GitHub release'
inputs: inputs:
gitHubConnection: 'github.com_RaidMax' gitHubConnection: 'github.com_RaidMax'
repositoryName: 'RaidMax/IW4M-Admin' repositoryName: 'RaidMax/IW4M-Admin'
action: 'create' action: 'create'
target: '$(Build.SourceVersion)' target: '$(Build.SourceVersion)'
tagSource: 'userSpecifiedTag' tagSource: 'userSpecifiedTag'
tag: '$(Version.Major).$(Version.Minor)-$(buildConfiguration)$(Version.Build)b$(Build.BuildId)' tag: '$(Build.BuildNumber)-$(releaseType)'
title: 'Version $(Version.Major).$(Version.Minor) $(buildConfiguration) Feature $(Version.Build) Build $(Build.BuildId)' title: 'IW4MAdmin $(Build.BuildNumber) ($(releaseType))'
assets: '$(Build.ArtifactStagingDirectory)/*.zip' assets: '$(Build.ArtifactStagingDirectory)/*.zip'
isPreRelease: true isPreRelease: $(isPreRelease)
releaseNotesSource: 'inline' releaseNotesSource: 'inline'
releaseNotesInline: 'todo' releaseNotesInline: 'todo'
changeLogCompareToRelease: 'lastNonDraftRelease' changeLogCompareToRelease: 'lastNonDraftRelease'
changeLogType: 'commitBased' changeLogType: 'commitBased'
- task: PowerShell@2 - task: PowerShell@2
displayName: 'Update master version'
inputs: inputs:
targetType: 'inline' targetType: 'inline'
script: | script: |
$payload = @{ $payload = @{
'current-version-prerelease' = '$(Version.Major).$(Version.Minor).$(Version.Build).$(Build.BuildId)' 'current-version-$(releaseType)' = '$(Build.BuildNumber)'
'jwt-secret' = '$(JWTSecret)' 'jwt-secret' = '$(JWTSecret)'
} | ConvertTo-Json } | ConvertTo-Json
@ -179,6 +207,7 @@ steps:
Invoke-RestMethod @params Invoke-RestMethod @params
- task: PublishPipelineArtifact@1 - task: PublishPipelineArtifact@1
displayName: 'Publish artifact for analysis'
inputs: inputs:
targetPath: '$(outputFolder)' targetPath: '$(outputFolder)'
artifact: 'IW4MAdmin.$(buildConfiguration)' artifact: 'IW4MAdmin.$(buildConfiguration)'

View File

@ -8,11 +8,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc
GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc
PostPublish.ps1 = PostPublish.ps1 DeploymentFiles\deployment-pipeline.yml = DeploymentFiles\deployment-pipeline.yml
pre-release-pipeline.yml = pre-release-pipeline.yml DeploymentFiles\PostPublish.ps1 = DeploymentFiles\PostPublish.ps1
README.md = README.md README.md = README.md
RunPublishPre.cmd = RunPublishPre.cmd
RunPublishRelease.cmd = RunPublishRelease.cmd
version.txt = version.txt version.txt = version.txt
EndProjectSection EndProjectSection
EndProject EndProject
@ -37,6 +35,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\ActionOnReport.js = Plugins\ScriptPlugins\ActionOnReport.js Plugins\ScriptPlugins\ActionOnReport.js = Plugins\ScriptPlugins\ActionOnReport.js
Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js
Plugins\ScriptPlugins\ParserIW4x.js = Plugins\ScriptPlugins\ParserIW4x.js Plugins\ScriptPlugins\ParserIW4x.js = Plugins\ScriptPlugins\ParserIW4x.js
Plugins\ScriptPlugins\ParserIW6x.js = Plugins\ScriptPlugins\ParserIW6x.js
Plugins\ScriptPlugins\ParserPIW5.js = Plugins\ScriptPlugins\ParserPIW5.js Plugins\ScriptPlugins\ParserPIW5.js = Plugins\ScriptPlugins\ParserPIW5.js
Plugins\ScriptPlugins\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js Plugins\ScriptPlugins\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js
Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js
@ -45,6 +44,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\SampleScriptPluginCommand.js = Plugins\ScriptPlugins\SampleScriptPluginCommand.js Plugins\ScriptPlugins\SampleScriptPluginCommand.js = Plugins\ScriptPlugins\SampleScriptPluginCommand.js
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
Plugins\ScriptPlugins\ParserT4.js = Plugins\ScriptPlugins\ParserT4.js
EndProjectSection EndProjectSection
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{A848FCF1-8527-4AA8-A1AA-50D29695C678}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{A848FCF1-8527-4AA8-A1AA-50D29695C678}"

View File

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

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2020.12.20.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2020.12.20.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,11 +1,13 @@
using LiveRadar.Configuration; using LiveRadar.Configuration;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace LiveRadar namespace LiveRadar
{ {
@ -21,11 +23,13 @@ namespace LiveRadar
private readonly Dictionary<string, long> _botGuidLookups; private readonly Dictionary<string, long> _botGuidLookups;
private bool addedPage; private bool addedPage;
private readonly object lockObject = new object(); private readonly object lockObject = new object();
private readonly ILogger _logger;
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory) public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory)
{ {
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration"); _configurationHandler = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration");
_botGuidLookups = new Dictionary<string, long>(); _botGuidLookups = new Dictionary<string, long>();
_logger = logger;
} }
public Task OnEventAsync(GameEvent E, Server S) public Task OnEventAsync(GameEvent E, Server S)
@ -65,7 +69,9 @@ namespace LiveRadar
lock (lockObject) lock (lockObject)
{ {
generatedBotGuid = _botGuidLookups.ContainsKey(botKey) ? _botGuidLookups[botKey] : 0; generatedBotGuid = _botGuidLookups.ContainsKey(botKey)
? _botGuidLookups[botKey]
: (E.Extra.ToString() ?? "0").ConvertGuidToLong(NumberStyles.HexNumber);
} }
var radarUpdate = RadarEvent.Parse(E.Data, generatedBotGuid); var radarUpdate = RadarEvent.Parse(E.Data, generatedBotGuid);
@ -80,8 +86,7 @@ namespace LiveRadar
catch (Exception e) catch (Exception e)
{ {
S.Logger.WriteWarning($"Could not parse live radar output: {e.Data}"); _logger.LogError(e, "Could not parse live radar output: {data}", e.Data);
S.Logger.WriteDebug(e.GetExceptionInfo());
} }
} }

View File

@ -23,7 +23,7 @@
</Target> </Target>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2020.12.20.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2020.12.20.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -15,18 +15,22 @@ var plugin = {
eventParser = manager.GenerateDynamicEventParser(this.name); eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +playerid +steamid +name +lastmsg +address +qport +rate *'; rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +playerid +steamid +name +lastmsg +address +qport +rate *';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]{16,32})|bot[0-9]+) ([0-9]+) +(.{0,32}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$'; rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]{16,32})|0) +([[0-9]+|0]) +(.{0,32}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|bot) +(-*[0-9]+) +([0-9]+) *$';
rconParser.Configuration.Status.AddMapping(104, 6); // RConName rconParser.Configuration.Status.AddMapping(104, 6); // RConName
rconParser.Configuration.Status.AddMapping(105, 8); // RConIPAddress rconParser.Configuration.Status.AddMapping(105, 8); // RConIPAddress
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.Dvar.Pattern = '^"(.+)" is: "(.+)?" default: "(.+)?" info: "(.+)?"$'; rconParser.Configuration.Dvar.Pattern = '^"(.+)" is: "(.+)?" default: "(.+)?" info: "(.+)?"$';
rconParser.Configuration.Dvar.AddMapping(109, 2); // DVAR latched value rconParser.Configuration.Dvar.AddMapping(109, 2); // DVAR latched value
rconParser.Configuration.Dvar.AddMapping(110, 4); // dvar info rconParser.Configuration.Dvar.AddMapping(110, 4); // dvar info
rconParser.Version = 'CoD4 X - win_mingw-x86 build 963 Mar 12 2019'; rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.NoticeLineSeparator = '. '; // CoD4x does not support \n in the client notice
rconParser.Version = 'CoD4 X - win_mingw-x86 build 1056 Dec 12 2020';
rconParser.GameName = 1; // IW3 rconParser.GameName = 1; // IW3
eventParser.Configuration.GameDirectory = 'main'; eventParser.Configuration.GameDirectory = 'main';
eventParser.Version = 'CoD4 X - win_mingw-x86 build 963 Mar 12 2019'; eventParser.Configuration.GuidNumberStyle = 7; // Integer
eventParser.Version = 'CoD4 X - win_mingw-x86 build 1056 Dec 12 2020';
eventParser.GameName = 1; // IW3 eventParser.GameName = 1; // IW3
eventParser.URLProtocolFormat = 'cod4://{{ip}}:{{port}}'; eventParser.URLProtocolFormat = 'cod4://{{ip}}:{{port}}';
}, },

View File

@ -0,0 +1,45 @@
var rconParser;
var eventParser;
var plugin = {
author: 'Xerxes, RaidMax',
version: 0.2,
name: 'IW6x Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
rconParser.Configuration.CommandPrefixes.Say = 'say {0}';
rconParser.Configuration.CommandPrefixes.Kick = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
rconParser.Configuration.WaitForResponse = false;
rconParser.Configuration.Status.AddMapping(102, 4);
rconParser.Configuration.Status.AddMapping(103, 5);
rconParser.Configuration.Status.AddMapping(104, 6);
rconParser.Configuration.DefaultDvarValues.Add('g_log', 'games_mp.log'); // todo: remove this once proper log support is implemented
rconParser.Configuration.DefaultDvarValues.Add('g_logsync', '1'); // todo: remove this once proper log support is implemented
rconParser.Version = 'IW6 MP 3.15 build 2 Sat Sep 14 2013 03:58:30PM win64';
rconParser.GameName = 4; // IW6
eventParser.Version = 'IW6 MP 3.15 build 2 Sat Sep 14 2013 03:58:30PM win64';
eventParser.GameName = 4; // IW6
eventParser.Configuration.GameDirectory = 'iw6x';
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = { var plugin = {
author: 'RaidMax', author: 'RaidMax',
version: 0.5, version: 0.6,
name: 'Plutonium IW5 Parser', name: 'Plutonium IW5 Parser',
isParser: true, isParser: true,
@ -26,6 +26,7 @@ var plugin = {
rconParser.Configuration.Dvar.AddMapping(107, 2); rconParser.Configuration.Dvar.AddMapping(107, 2);
rconParser.Configuration.WaitForResponse = true; rconParser.Configuration.WaitForResponse = true;
rconParser.Configuration.CanGenerateLogPath = true; rconParser.Configuration.CanGenerateLogPath = true;
rconParser.Configuration.NoticeLineSeparator = '. ';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *'; rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(?:[0-1]{1}) +([0-9]{1,4}|[A-Z]{4}) +([a-f|A-F|0-9]{16}) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$'; rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(?:[0-1]{1}) +([0-9]{1,4}|[A-Z]{4}) +([a-f|A-F|0-9]{16}) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$';

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = { var plugin = {
author: 'RaidMax, Xerxes', author: 'RaidMax, Xerxes',
version: 0.8, version: 0.9,
name: 'Plutonium T6 Parser', name: 'Plutonium T6 Parser',
isParser: true, isParser: true,
@ -26,6 +26,7 @@ var plugin = {
rconParser.Configuration.Dvar.AddMapping(106, 1); rconParser.Configuration.Dvar.AddMapping(106, 1);
rconParser.Configuration.Dvar.AddMapping(107, 2); rconParser.Configuration.Dvar.AddMapping(107, 2);
rconParser.Configuration.WaitForResponse = false; rconParser.Configuration.WaitForResponse = false;
rconParser.Configuration.NoticeLineSeparator = '. ';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *'; rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+|0) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$'; rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+|0) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$';

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = { var plugin = {
author: 'RaidMax', author: 'RaidMax',
version: 0.2, version: 0.3,
name: 'RektT5m Parser', name: 'RektT5m Parser',
isParser: true, isParser: true,
@ -16,7 +16,7 @@ var plugin = {
eventParser.Configuration.GameDirectory = 'data'; eventParser.Configuration.GameDirectory = 'data';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff\x01print'; rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff\x01print\n';
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}'; rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;

View File

@ -0,0 +1,30 @@
var rconParser;
var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.1,
name: 'Call of Duty 5: World at War Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Version = 'Call of Duty Multiplayer COD_WaW MP build 1.7.1263 CL(350073) JADAMS2 Thu Oct 29 15:43:55 2009 win-x86';
eventParser.Configuration.GuidNumberStyle = 7; // Integer
eventParser.GameName = 5; // T4
eventParser.Version = 'Call of Duty Multiplayer COD_WaW MP build 1.7.1263 CL(350073) JADAMS2 Thu Oct 29 15:43:55 2009 win-x86';
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = { var plugin = {
author: 'RaidMax', author: 'RaidMax',
version: 0.7, version: 0.8,
name: 'Tekno MW3 Parser', name: 'Tekno MW3 Parser',
isParser: true, isParser: true,
@ -27,6 +27,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.TempBan = 'tempbanclient {0} "{1}"'; rconParser.Configuration.CommandPrefixes.TempBan = 'tempbanclient {0} "{1}"';
rconParser.Configuration.Dvar.AddMapping(107, 1); // RCon DvarValue rconParser.Configuration.Dvar.AddMapping(107, 1); // RCon DvarValue
rconParser.Configuration.Dvar.Pattern = '^(.*)$'; rconParser.Configuration.Dvar.Pattern = '^(.*)$';
rconParser.Configuration.NoticeLineSeparator = '. ';
rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1'); rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1');
rconParser.Configuration.OverrideDvarNameMapping.Add('_website', 'sv_clanWebsite'); rconParser.Configuration.OverrideDvarNameMapping.Add('_website', 'sv_clanWebsite');

View File

@ -1,13 +1,13 @@
using IW4MAdmin.Plugins.Stats.Models; using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Plugins.Stats.Cheat namespace IW4MAdmin.Plugins.Stats.Cheat
{ {
@ -223,11 +223,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
if (weightedSessionAverage > Thresholds.MaxOffset(totalSessionHits) && if (weightedSessionAverage > Thresholds.MaxOffset(totalSessionHits) &&
totalSessionHits >= (Thresholds.MediumSampleMinKills * 2)) totalSessionHits >= (Thresholds.MediumSampleMinKills * 2))
{ {
Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***");
Log.WriteDebug($"Session Average = {weightedSessionAverage}");
Log.WriteDebug($"HitCount = {HitCount}");
Log.WriteDebug($"ID = {hit.AttackerId}");
results.Add(new DetectionPenaltyResult() results.Add(new DetectionPenaltyResult()
{ {
ClientPenalty = EFPenalty.PenaltyType.Ban, ClientPenalty = EFPenalty.PenaltyType.Ban,
@ -238,17 +233,13 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
}); });
} }
#if DEBUG Log.LogDebug("PredictVsReal={realAgainstPredict}", realAgainstPredict);
Log.WriteDebug($"PredictVsReal={realAgainstPredict}");
#endif
} }
#endregion #endregion
#region STRAIN #region STRAIN
double currentStrain = Strain.GetStrain(hit.Distance / 0.0254, hit.ViewAngles, Math.Max(50, LastOffset == 0 ? 50 : (hit.TimeOffset - LastOffset))); double currentStrain = Strain.GetStrain(hit.Distance / 0.0254, hit.ViewAngles, Math.Max(50, LastOffset == 0 ? 50 : (hit.TimeOffset - LastOffset)));
#if DEBUG == true Log.LogDebug("Current Strain: {currentStrain}", currentStrain);
Log.WriteDebug($"Current Strain: {currentStrain}");
#endif
LastOffset = hit.TimeOffset; LastOffset = hit.TimeOffset;
if (currentStrain > ClientStats.MaxStrain) if (currentStrain > ClientStats.MaxStrain)
@ -331,7 +322,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
if (!shouldIgnoreDetection) if (!shouldIgnoreDetection)
{ {
validButtonHitCount++; validButtonHitCount++;
}
double lastDiff = hit.TimeOffset - hit.TimeSinceLastAttack; double lastDiff = hit.TimeOffset - hit.TimeSinceLastAttack;
if (validButtonHitCount > 0 && lastDiff <= 0) if (validButtonHitCount > 0 && lastDiff <= 0)
@ -344,6 +334,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Type = DetectionType.Button Type = DetectionType.Button
}); });
} }
}
#endregion #endregion
#region SESSION_RATIOS #region SESSION_RATIOS

View File

@ -27,12 +27,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
double[] distance = Utilities.AngleStuff(newAngle, LastAngle); double[] distance = Utilities.AngleStuff(newAngle, LastAngle);
LastDistance = distance[0] + distance[1]; LastDistance = distance[0] + distance[1];
#if DEBUG == true
Console.WriteLine($"Angle Between = {LastDistance}");
Console.WriteLine($"Distance From Target = {killDistance}");
Console.WriteLine($"Time Offset = {deltaTime}");
Console.WriteLine($"Decay Factor = {decayFactor} ");
#endif
// this happens on first kill // this happens on first kill
if ((distance[0] == 0 && distance[1] == 0) || if ((distance[0] == 0 && distance[1] == 0) ||
deltaTime == 0 || deltaTime == 0 ||

View File

@ -2,7 +2,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using SharedLibraryCore; using SharedLibraryCore;
using IW4MAdmin.Plugins.Stats.Models; using IW4MAdmin.Plugins.Stats.Models;
using System.Collections.Generic; using System.Collections.Generic;
@ -19,7 +18,8 @@ namespace IW4MAdmin.Plugins.Stats.Commands
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly CommandConfiguration _config; private readonly CommandConfiguration _config;
public MostKillsCommand(CommandConfiguration config, ITranslationLookup translationLookup, IDatabaseContextFactory contextFactory) : base(config, translationLookup) public MostKillsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup)
{ {
Name = "mostkills"; Name = "mostkills";
Description = translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_DESC"]; Description = translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_DESC"];
@ -32,7 +32,8 @@ namespace IW4MAdmin.Plugins.Stats.Commands
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
var mostKills = await GetMostKills(StatManager.GetIdForServer(E.Owner), Plugin.Config.Configuration(), _contextFactory, _translationLookup); var mostKills = await GetMostKills(StatManager.GetIdForServer(E.Owner), Plugin.Config.Configuration(),
_contextFactory, _translationLookup);
if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {
foreach (var stat in mostKills) foreach (var stat in mostKills)
@ -50,10 +51,10 @@ namespace IW4MAdmin.Plugins.Stats.Commands
} }
} }
public static async Task<IEnumerable<string>> GetMostKills(long? serverId, StatsConfiguration config, IDatabaseContextFactory contextFactory, ITranslationLookup translationLookup) public static async Task<IEnumerable<string>> GetMostKills(long? serverId, StatsConfiguration config,
{ IDatabaseContextFactory contextFactory, ITranslationLookup translationLookup)
using (var ctx = contextFactory.CreateContext(enableTracking: false))
{ {
await using var ctx = contextFactory.CreateContext(enableTracking: false);
var dayInPast = DateTime.UtcNow.AddDays(-config.MostKillsMaxInactivityDays); var dayInPast = DateTime.UtcNow.AddDays(-config.MostKillsMaxInactivityDays);
var iqStats = (from stats in ctx.Set<EFClientStatistics>() var iqStats = (from stats in ctx.Set<EFClientStatistics>()
@ -74,9 +75,9 @@ namespace IW4MAdmin.Plugins.Stats.Commands
var iqList = await iqStats.ToListAsync(); var iqList = await iqStats.ToListAsync();
return iqList.Select((stats, index) => translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_FORMAT"].FormatExt(index + 1, stats.Name, stats.Kills)) return iqList.Select((stats, index) => translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_FORMAT"]
.FormatExt(index + 1, stats.Name, stats.Kills))
.Prepend(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTKILLS_HEADER"]); .Prepend(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTKILLS_HEADER"]);
} }
} }
}
} }

View File

@ -16,7 +16,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{ {
class MostPlayedCommand : Command class MostPlayedCommand : Command
{ {
public static async Task<List<string>> GetMostPlayed(Server s, ITranslationLookup translationLookup) public static async Task<List<string>> GetMostPlayed(Server s, ITranslationLookup translationLookup, IDatabaseContextFactory contextFactory)
{ {
long serverId = StatManager.GetIdForServer(s); long serverId = StatManager.GetIdForServer(s);
@ -25,17 +25,13 @@ namespace IW4MAdmin.Plugins.Stats.Commands
$"^5--{translationLookup["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--" $"^5--{translationLookup["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--"
}; };
using (var db = new DatabaseContext(true)) await using var context = contextFactory.CreateContext(false);
{
db.ChangeTracker.AutoDetectChangesEnabled = false;
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1); var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1);
var iqStats = (from stats in db.Set<EFClientStatistics>() var iqStats = (from stats in context.Set<EFClientStatistics>()
join client in db.Clients join client in context.Clients
on stats.ClientId equals client.ClientId on stats.ClientId equals client.ClientId
join alias in db.Aliases join alias in context.Aliases
on client.CurrentAliasId equals alias.AliasId on client.CurrentAliasId equals alias.AliasId
where stats.ServerId == serverId where stats.ServerId == serverId
where client.Level != EFClient.Permission.Banned where client.Level != EFClient.Permission.Banned
@ -52,15 +48,16 @@ namespace IW4MAdmin.Plugins.Stats.Commands
var iqList = await iqStats.ToListAsync(); var iqList = await iqStats.ToListAsync();
mostPlayed.AddRange(iqList.Select(stats => translationLookup["COMMANDS_MOST_PLAYED_FORMAT"].FormatExt(stats.Name, stats.Kills, (DateTime.UtcNow - DateTime.UtcNow.AddSeconds(-stats.TotalConnectionTime)).HumanizeForCurrentCulture()))); mostPlayed.AddRange(iqList.Select(stats => translationLookup["COMMANDS_MOST_PLAYED_FORMAT"].FormatExt(stats.Name, stats.Kills, (DateTime.UtcNow - DateTime.UtcNow.AddSeconds(-stats.TotalConnectionTime)).HumanizeForCurrentCulture())));
}
return mostPlayed; return mostPlayed;
} }
private readonly CommandConfiguration _config; private readonly CommandConfiguration _config;
private readonly IDatabaseContextFactory _contextFactory;
public MostPlayedCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) public MostPlayedCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup)
{ {
Name = "mostplayed"; Name = "mostplayed";
Description = translationLookup["PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC"]; Description = translationLookup["PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC"];
@ -69,11 +66,12 @@ namespace IW4MAdmin.Plugins.Stats.Commands
RequiresTarget = false; RequiresTarget = false;
_config = config; _config = config;
_contextFactory = contextFactory;
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
var topStats = await GetMostPlayed(E.Owner, _translationLookup); var topStats = await GetMostPlayed(E.Owner, _translationLookup, _contextFactory);
if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {
foreach (var stat in topStats) foreach (var stat in topStats)

View File

@ -12,31 +12,36 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{ {
public class ResetStats : Command public class ResetStats : Command
{ {
public ResetStats(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) private readonly IDatabaseContextFactory _contextFactory;
public ResetStats(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup)
{ {
Name = "resetstats"; Name = "resetstats";
Description = translationLookup["PLUGINS_STATS_COMMANDS_RESET_DESC"]; Description = translationLookup["PLUGINS_STATS_COMMANDS_RESET_DESC"];
Alias = "rs"; Alias = "rs";
Permission = EFClient.Permission.User; Permission = EFClient.Permission.User;
RequiresTarget = false; RequiresTarget = false;
//AllowImpersonation = true; AllowImpersonation = true;
_contextFactory = contextFactory;
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
if (E.Origin.ClientNumber >= 0) if (gameEvent.Origin.ClientNumber >= 0)
{ {
var serverId = Helpers.StatManager.GetIdForServer(gameEvent.Owner);
long serverId = Helpers.StatManager.GetIdForServer(E.Owner); await using var context = _contextFactory.CreateContext();
var clientStats = await context.Set<EFClientStatistics>()
EFClientStatistics clientStats; .Where(s => s.ClientId == gameEvent.Origin.ClientId)
using (var ctx = new DatabaseContext(disableTracking: true))
{
clientStats = await ctx.Set<EFClientStatistics>()
.Where(s => s.ClientId == E.Origin.ClientId)
.Where(s => s.ServerId == serverId) .Where(s => s.ServerId == serverId)
.FirstAsync(); .FirstOrDefaultAsync();
// want to prevent resetting stats before they've gotten any kills
if (clientStats != null)
{
clientStats.Deaths = 0; clientStats.Deaths = 0;
clientStats.Kills = 0; clientStats.Kills = 0;
clientStats.SPM = 0.0; clientStats.SPM = 0.0;
@ -44,19 +49,18 @@ namespace IW4MAdmin.Plugins.Stats.Commands
clientStats.TimePlayed = 0; clientStats.TimePlayed = 0;
// todo: make this more dynamic // todo: make this more dynamic
clientStats.EloRating = 200.0; clientStats.EloRating = 200.0;
await context.SaveChangesAsync();
}
// reset the cached version // reset the cached version
Plugin.Manager.ResetStats(E.Origin); Plugin.Manager.ResetStats(gameEvent.Origin);
// fixme: this doesn't work properly when another context exists gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_RESET_SUCCESS"]);
await ctx.SaveChangesAsync();
}
E.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_RESET_SUCCESS"]);
} }
else else
{ {
E.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_RESET_FAIL"]); gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_RESET_FAIL"]);
} }
} }
} }

View File

@ -16,7 +16,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{ {
class TopStats : Command class TopStats : Command
{ {
public static async Task<List<string>> GetTopStats(Server s, ITranslationLookup translationLookup) public static async Task<List<string>> GetTopStats(Server s, ITranslationLookup translationLookup, IDatabaseContextFactory contextFactory)
{ {
long serverId = StatManager.GetIdForServer(s); long serverId = StatManager.GetIdForServer(s);
var topStatsText = new List<string>() var topStatsText = new List<string>()
@ -24,15 +24,14 @@ namespace IW4MAdmin.Plugins.Stats.Commands
$"^5--{translationLookup["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--" $"^5--{translationLookup["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
}; };
using (var db = new DatabaseContext(true)) await using var context = contextFactory.CreateContext(false);
{
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15); var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
int minPlayTime = Plugin.Config.Configuration().TopPlayersMinPlayTime; int minPlayTime = Plugin.Config.Configuration().TopPlayersMinPlayTime;
var iqStats = (from stats in db.Set<EFClientStatistics>() var iqStats = (from stats in context.Set<EFClientStatistics>()
join client in db.Clients join client in context.Clients
on stats.ClientId equals client.ClientId on stats.ClientId equals client.ClientId
join alias in db.Aliases join alias in context.Aliases
on client.CurrentAliasId equals alias.AliasId on client.CurrentAliasId equals alias.AliasId
where stats.ServerId == serverId where stats.ServerId == serverId
where stats.TimePlayed >= minPlayTime where stats.TimePlayed >= minPlayTime
@ -51,7 +50,6 @@ namespace IW4MAdmin.Plugins.Stats.Commands
.Select(stats => $"^3{stats.Name}^7 - ^5{stats.KDR} ^7{translationLookup["PLUGINS_STATS_TEXT_KDR"]} | ^5{stats.Performance} ^7{translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"]}"); .Select(stats => $"^3{stats.Name}^7 - ^5{stats.KDR} ^7{translationLookup["PLUGINS_STATS_TEXT_KDR"]} | ^5{stats.Performance} ^7{translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"]}");
topStatsText.AddRange(statsList); topStatsText.AddRange(statsList);
}
// no one qualified // no one qualified
if (topStatsText.Count == 1) if (topStatsText.Count == 1)
@ -66,8 +64,10 @@ namespace IW4MAdmin.Plugins.Stats.Commands
} }
private readonly CommandConfiguration _config; private readonly CommandConfiguration _config;
private readonly IDatabaseContextFactory _contextFactory;
public TopStats(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) public TopStats(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup)
{ {
Name = "topstats"; Name = "topstats";
Description = translationLookup["PLUGINS_STATS_COMMANDS_TOP_DESC"]; Description = translationLookup["PLUGINS_STATS_COMMANDS_TOP_DESC"];
@ -76,11 +76,12 @@ namespace IW4MAdmin.Plugins.Stats.Commands
RequiresTarget = false; RequiresTarget = false;
_config = config; _config = config;
_contextFactory = contextFactory;
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
var topStats = await GetTopStats(E.Owner, _translationLookup); var topStats = await GetTopStats(E.Owner, _translationLookup, _contextFactory);
if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {
foreach (var stat in topStats) foreach (var stat in topStats)

View File

@ -14,9 +14,11 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{ {
public class ViewStatsCommand : Command public class ViewStatsCommand : Command
{ {
public ViewStatsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) private readonly IDatabaseContextFactory _contextFactory;
{
public ViewStatsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup)
{
Name = "stats"; Name = "stats";
Description = translationLookup["PLUGINS_STATS_COMMANDS_VIEW_DESC"]; Description = translationLookup["PLUGINS_STATS_COMMANDS_VIEW_DESC"];
Alias = "xlrstats"; Alias = "xlrstats";
@ -31,15 +33,13 @@ namespace IW4MAdmin.Plugins.Stats.Commands
} }
}; };
_config = config; _contextFactory = contextFactory;
} }
private readonly CommandConfiguration _config;
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
string statLine; string statLine;
EFClientStatistics pStats; EFClientStatistics pStats = null;
if (E.Data.Length > 0 && E.Target == null) if (E.Data.Length > 0 && E.Target == null)
{ {
@ -51,52 +51,67 @@ namespace IW4MAdmin.Plugins.Stats.Commands
} }
} }
long serverId = StatManager.GetIdForServer(E.Owner); var serverId = StatManager.GetIdForServer(E.Owner);
// getting stats for a particular client
if (E.Target != null) if (E.Target != null)
{ {
int performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Target.ClientId); var performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Target.ClientId);
string performanceRankingString = performanceRanking == 0 ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}"; var performanceRankingString = performanceRanking == 0
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}";
if (E.Owner.GetClientsAsList().Any(_client => _client.Equals(E.Target))) // target is currently connected so we want their cached stats if they exist
if (E.Owner.GetClientsAsList().Any(client => client.Equals(E.Target)))
{ {
pStats = E.Target.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY); pStats = E.Target.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY);
} }
else // target is not connected so we want to look up via database
if (pStats == null)
{ {
using (var ctx = new DatabaseContext(true)) await using var context = _contextFactory.CreateContext(false);
{ pStats = (await context.Set<EFClientStatistics>()
pStats = (await ctx.Set<EFClientStatistics>().FirstAsync(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId)); .FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId));
}
}
statLine = $"^5{pStats.Kills} ^7{_translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{_translationLookup["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{_translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}";
} }
// if it's still null then they've not gotten a kill or death yet
statLine = pStats == null
? _translationLookup["PLUGINS_STATS_COMMANDS_NOTAVAILABLE"]
: $"^5{pStats.Kills} ^7{_translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{_translationLookup["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{_translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}";
}
// getting self stats
else else
{ {
int performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Origin.ClientId); var performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Origin.ClientId);
string performanceRankingString = performanceRanking == 0 ? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}"; var performanceRankingString = performanceRanking == 0
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}";
if (E.Owner.GetClientsAsList().Any(_client => _client.Equals(E.Origin))) // check if current client is connected to the server
if (E.Owner.GetClientsAsList().Any(client => client.Equals(E.Origin)))
{ {
pStats = E.Origin.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY); pStats = E.Origin.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY);
} }
else // happens if the user has not gotten a kill/death since connecting
if (pStats == null)
{ {
using (var ctx = new DatabaseContext(true)) await using var context = _contextFactory.CreateContext(false);
{ pStats = (await context.Set<EFClientStatistics>()
pStats = (await ctx.Set<EFClientStatistics>().FirstAsync(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId)); .FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId));
} }
}
statLine = $"^5{pStats.Kills} ^7{_translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{_translationLookup["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{_translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}"; // if it's still null then they've not gotten a kill or death yet
statLine = pStats == null
? _translationLookup["PLUGINS_STATS_COMMANDS_NOTAVAILABLE"]
: $"^5{pStats.Kills} ^7{_translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{_translationLookup["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{_translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}";
} }
if (E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)) if (E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{ {
string name = E.Target == null ? E.Origin.Name : E.Target.Name; var name = E.Target == null ? E.Origin.Name : E.Target.Name;
E.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name)); E.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name));
E.Owner.Broadcast(statLine); E.Owner.Broadcast(statLine);
} }

View File

@ -15,7 +15,7 @@ namespace Stats.Config
Game.IW4, new Dictionary<DetectionType, string[]> Game.IW4, new Dictionary<DetectionType, string[]>
{ {
{ DetectionType.Chest, new[] { "m21.+" } }, { DetectionType.Chest, new[] { "m21.+" } },
{ DetectionType.Recoil, new[] { "ranger.*_mp", "model1887.*_mp", ".+shotgun.*_mp" } }, { DetectionType.Recoil, new[] { "ranger.*_mp", "model1887.*_mp", ".+shotgun.*_mp", "turret_minigun_mp" } },
{ DetectionType.Button, new[] { ".*akimbo.*" } } { DetectionType.Button, new[] { ".*akimbo.*" } }
} }
} }

View File

@ -0,0 +1,7 @@
namespace Stats.Helpers
{
public class MigrationHelper
{
}
}

View File

@ -4,7 +4,6 @@ using IW4MAdmin.Plugins.Stats.Models;
using IW4MAdmin.Plugins.Stats.Web.Dtos; using IW4MAdmin.Plugins.Stats.Web.Dtos;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -15,7 +14,9 @@ using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using static IW4MAdmin.Plugins.Stats.Cheat.Detection; using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Plugins.Stats.Helpers namespace IW4MAdmin.Plugins.Stats.Helpers
{ {
@ -29,22 +30,26 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private static List<EFServer> serverModels; private static List<EFServer> serverModels;
public static string CLIENT_STATS_KEY = "ClientStats"; public static string CLIENT_STATS_KEY = "ClientStats";
public static string CLIENT_DETECTIONS_KEY = "ClientDetections"; public static string CLIENT_DETECTIONS_KEY = "ClientDetections";
private readonly SemaphoreSlim _addPlayerWaiter = new SemaphoreSlim(1, 1);
public StatManager(IManager mgr, IDatabaseContextFactory contextFactory, IConfigurationHandler<StatsConfiguration> configHandler) public StatManager(ILogger<StatManager> logger, IManager mgr, IDatabaseContextFactory contextFactory, IConfigurationHandler<StatsConfiguration> configHandler)
{ {
_servers = new ConcurrentDictionary<long, ServerStats>(); _servers = new ConcurrentDictionary<long, ServerStats>();
_log = mgr.GetLogger(0); _log = logger;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_configHandler = configHandler; _configHandler = configHandler;
} }
~StatManager()
{
_addPlayerWaiter.Dispose();
}
private void SetupServerIds() private void SetupServerIds()
{ {
using (var ctx = _contextFactory.CreateContext(enableTracking: false)) using var ctx = _contextFactory.CreateContext(enableTracking: false);
{
serverModels = ctx.Set<EFServer>().ToList(); serverModels = ctx.Set<EFServer>().ToList();
} }
}
public Expression<Func<EFRating, bool>> GetRankingFunc(long? serverId = null) public Expression<Func<EFRating, bool>> GetRankingFunc(long? serverId = null)
{ {
@ -63,8 +68,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// <returns></returns> /// <returns></returns>
public async Task<int> GetClientOverallRanking(int clientId) public async Task<int> GetClientOverallRanking(int clientId)
{ {
using (var context = _contextFactory.CreateContext(enableTracking: false)) await using var context = _contextFactory.CreateContext(enableTracking: false);
{
var clientPerformance = await context.Set<EFRating>() var clientPerformance = await context.Set<EFRating>()
.Where(r => r.RatingHistory.ClientId == clientId) .Where(r => r.RatingHistory.ClientId == clientId)
.Where(r => r.ServerId == null) .Where(r => r.ServerId == null)
@ -84,12 +89,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return 0; return 0;
} }
}
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null) public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
{ {
using (var context = _contextFactory.CreateContext(enableTracking: false)) await using var context = _contextFactory.CreateContext(enableTracking: false);
{
// setup the query for the clients within the given rating range // setup the query for the clients within the given rating range
var iqClientRatings = (from rating in context.Set<EFRating>() var iqClientRatings = (from rating in context.Set<EFRating>()
.Where(GetRankingFunc(serverId)) .Where(GetRankingFunc(serverId))
@ -178,7 +181,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return finished; return finished;
} }
}
/// <summary> /// <summary>
/// Add a server to the StatManager server pool /// Add a server to the StatManager server pool
@ -197,8 +199,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
long serverId = GetIdForServer(sv); long serverId = GetIdForServer(sv);
EFServer server; EFServer server;
using (var ctx = _contextFactory.CreateContext(enableTracking: false)) using var ctx = _contextFactory.CreateContext(enableTracking: false);
{
var serverSet = ctx.Set<EFServer>(); var serverSet = ctx.Set<EFServer>();
// get the server from the database if it exists, otherwise create and insert a new one // get the server from the database if it exists, otherwise create and insert a new one
server = serverSet.FirstOrDefault(s => s.ServerId == serverId); server = serverSet.FirstOrDefault(s => s.ServerId == serverId);
@ -252,7 +253,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ctx.Entry(server).Property(_prop => _prop.IsPasswordProtected).IsModified = true; ctx.Entry(server).Property(_prop => _prop.IsPasswordProtected).IsModified = true;
server.IsPasswordProtected = !string.IsNullOrEmpty(sv.GamePassword); server.IsPasswordProtected = !string.IsNullOrEmpty(sv.GamePassword);
ctx.SaveChanges(); ctx.SaveChanges();
}
// check to see if the stats have ever been initialized // check to see if the stats have ever been initialized
var serverStats = InitializeServerStats(server.ServerId); var serverStats = InitializeServerStats(server.ServerId);
@ -265,8 +265,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (Exception e) catch (Exception e)
{ {
_log.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]} - {e.Message}"); _log.LogError(e, "{message}", Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]);
_log.WriteDebug(e.GetExceptionInfo());
} }
} }
@ -277,13 +276,21 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// <returns>EFClientStatistic of specified player</returns> /// <returns>EFClientStatistic of specified player</returns>
public async Task<EFClientStatistics> AddPlayer(EFClient pl) public async Task<EFClientStatistics> AddPlayer(EFClient pl)
{ {
var existingStats = pl.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
if (existingStats != null)
{
return existingStats;
}
try try
{ {
await _addPlayerWaiter.WaitAsync();
long serverId = GetIdForServer(pl.CurrentServer); long serverId = GetIdForServer(pl.CurrentServer);
if (!_servers.ContainsKey(serverId)) if (!_servers.ContainsKey(serverId))
{ {
_log.WriteError($"[Stats::AddPlayer] Server with id {serverId} could not be found"); _log.LogError("[Stats::AddPlayer] Server with id {serverId} could not be found", serverId);
return null; return null;
} }
@ -292,8 +299,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
EFClientStatistics clientStats; EFClientStatistics clientStats;
using (var ctx = _contextFactory.CreateContext(enableTracking: false)) await using var ctx = _contextFactory.CreateContext(enableTracking: false);
{
var clientStatsSet = ctx.Set<EFClientStatistics>(); var clientStatsSet = ctx.Set<EFClientStatistics>();
clientStats = clientStatsSet clientStats = clientStatsSet
.Include(cl => cl.HitLocations) .Include(cl => cl.HitLocations)
@ -311,7 +317,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Skill = 0.0, Skill = 0.0,
SPM = 0.0, SPM = 0.0,
EloRating = 200.0, EloRating = 200.0,
HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>().Select(hl => new EFHitLocationCount() HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>()
.Select(hl => new EFHitLocationCount()
{ {
Active = true, Active = true,
HitCount = 0, HitCount = 0,
@ -329,7 +336,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// migration for previous existing stats // migration for previous existing stats
if (clientStats.HitLocations.Count == 0) if (clientStats.HitLocations.Count == 0)
{ {
clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>() clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation))
.OfType<IW4Info.HitLocation>()
.Select(hl => new EFHitLocationCount() .Select(hl => new EFHitLocationCount()
{ {
Active = true, Active = true,
@ -360,16 +368,22 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
clientStats.LastScore = pl.Score; clientStats.LastScore = pl.Score;
pl.SetAdditionalProperty(CLIENT_DETECTIONS_KEY, new Detection(_log, clientStats)); pl.SetAdditionalProperty(CLIENT_DETECTIONS_KEY, new Detection(_log, clientStats));
pl.CurrentServer.Logger.WriteInfo($"Added {pl} to stats"); _log.LogDebug("Added {client} to stats", pl.ToString());
}
return clientStats; return clientStats;
} }
catch (Exception ex) catch (Exception ex)
{ {
_log.WriteWarning("Could not add client to stats"); _log.LogError(ex, "Could not add client to stats {@client}", pl.ToString());
_log.WriteDebug(ex.GetExceptionInfo()); }
finally
{
if (_addPlayerWaiter.CurrentCount == 0)
{
_addPlayerWaiter.Release(1);
}
} }
return null; return null;
@ -382,11 +396,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// <returns></returns> /// <returns></returns>
public async Task RemovePlayer(EFClient pl) public async Task RemovePlayer(EFClient pl)
{ {
pl.CurrentServer.Logger.WriteInfo($"Removing {pl} from stats"); _log.LogDebug("Removing {client} from stats", pl.ToString());
if (pl.CurrentServer == null) if (pl.CurrentServer == null)
{ {
pl.CurrentServer.Logger.WriteWarning($"Disconnecting client {pl} is not on a server, state is {pl.State}"); _log.LogWarning("Disconnecting client {client} is not on a server", pl.ToString());
return; return;
} }
@ -407,18 +421,16 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
else else
{ {
pl.CurrentServer.Logger.WriteWarning($"Disconnecting client {pl} has not been added to stats, state is {pl.State}"); _log.LogWarning("Disconnecting client {client} has not been added to stats", pl.ToString());
} }
} }
private async Task SaveClientStats(EFClientStatistics clientStats) private async Task SaveClientStats(EFClientStatistics clientStats)
{ {
using (var ctx = _contextFactory.CreateContext()) await using var ctx = _contextFactory.CreateContext();
{
ctx.Update(clientStats); ctx.Update(clientStats);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
}
public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, long serverId) public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, long serverId)
{ {
@ -452,10 +464,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
} }
catch (FormatException) catch (FormatException ex)
{ {
_log.WriteError("Could not parse vector data from hit"); _log.LogWarning(ex, "Could not parse vector data from hit");
_log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAngle - {viewAngles} Snapshot - {string.Join(",", snapshotAngles.Select(_a => _a.ToString()))}");
return; return;
} }
@ -527,8 +538,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (Exception e) catch (Exception e)
{ {
_log.WriteError("Could not store client kills"); _log.LogError(e, "Could not store client kills");
_log.WriteDebug(e.GetExceptionInfo());
} }
finally finally
@ -583,9 +593,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (TaskCanceledException) { } catch (TaskCanceledException) { }
catch (Exception ex) catch (Exception ex)
{ {
_log.WriteError("Could not save hit or AC info"); _log.LogError(ex, "Could not save hit or anti-cheat info {@attacker} {@victim} {server}", attacker, victim, serverId);
_log.WriteDebug(ex.GetExceptionInfo());
_log.WriteDebug($"Attacker: {attacker} Victim: {victim}, ServerId {serverId}");
} }
finally finally
@ -611,14 +619,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public async Task SaveHitCache(long serverId) public async Task SaveHitCache(long serverId)
{ {
using (var ctx = _contextFactory.CreateContext(enableTracking: false)) await using var ctx = _contextFactory.CreateContext(enableTracking: false);
{
var server = _servers[serverId]; var server = _servers[serverId];
ctx.AddRange(server.HitCache.ToList()); ctx.AddRange(server.HitCache.ToList());
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
server.HitCache.Clear(); server.HitCache.Clear();
} }
}
private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId) private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
{ {
@ -633,7 +639,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
try try
{ {
if (detectionTypes[server.EndPoint].Contains(detectionType)) if (!detectionTypes[server.EndPoint].Contains(detectionType))
{ {
return false; return false;
} }
@ -697,15 +703,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{ {
EFACSnapshot change; EFACSnapshot change;
using (var ctx = _contextFactory.CreateContext(enableTracking: false)) await using var ctx = _contextFactory.CreateContext();
{
while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot)) while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot))
{ {
ctx.Add(change); ctx.Add(change);
} }
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
}
public async Task AddStandardKill(EFClient attacker, EFClient victim) public async Task AddStandardKill(EFClient attacker, EFClient victim)
{ {
@ -714,9 +718,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var attackerStats = attacker.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY); var attackerStats = attacker.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
var victimStats = victim.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY); var victimStats = victim.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
#if DEBUG
_log.WriteDebug("Processing standard kill");
#endif
// update the total stats // update the total stats
_servers[serverId].ServerStatistics.TotalKills += 1; _servers[serverId].ServerStatistics.TotalKills += 1;
@ -754,14 +755,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// fixme: why? // fixme: why?
if (double.IsNaN(victimStats.SPM) || double.IsNaN(victimStats.Skill)) if (double.IsNaN(victimStats.SPM) || double.IsNaN(victimStats.Skill))
{ {
_log.WriteDebug($"[StatManager::AddStandardKill] victim SPM/SKILL {victimStats.SPM} {victimStats.Skill}"); _log.LogWarning("victim SPM/SKILL {@victimStats}", victimStats);
victimStats.SPM = 0.0; victimStats.SPM = 0.0;
victimStats.Skill = 0.0; victimStats.Skill = 0.0;
} }
if (double.IsNaN(attackerStats.SPM) || double.IsNaN(attackerStats.Skill)) if (double.IsNaN(attackerStats.SPM) || double.IsNaN(attackerStats.Skill))
{ {
_log.WriteDebug($"[StatManager::AddStandardKill] attacker SPM/SKILL {victimStats.SPM} {victimStats.Skill}"); _log.LogWarning("attacker SPM/SKILL {@attackerStats}", attackerStats);
attackerStats.SPM = 0.0; attackerStats.SPM = 0.0;
attackerStats.Skill = 0.0; attackerStats.Skill = 0.0;
} }
@ -781,8 +782,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (Exception e) catch (Exception e)
{ {
_log.WriteWarning($"Could not update stat history for {attacker}"); _log.LogWarning(e, "Could not update stat history for {attacker}", attacker.ToString());
_log.WriteDebug(e.GetExceptionInfo());
} }
finally finally
@ -813,8 +813,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime; int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime;
using (var ctx = _contextFactory.CreateContext(enableTracking: true)) await using var ctx = _contextFactory.CreateContext(enableTracking: true);
{
// select the rating history for client // select the rating history for client
var iqHistoryLink = from history in ctx.Set<EFClientRatingHistory>() var iqHistoryLink = from history in ctx.Set<EFClientRatingHistory>()
.Include(h => h.Ratings) .Include(h => h.Ratings)
@ -967,7 +966,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
}
/// <summary> /// <summary>
/// Performs the incrementation of kills and deaths for client statistics /// Performs the incrementation of kills and deaths for client statistics
@ -1099,8 +1097,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (clientStats.SPM < 0) if (clientStats.SPM < 0)
{ {
_log.WriteWarning("[StatManager:UpdateStats] clientStats SPM < 0"); _log.LogWarning("clientStats SPM < 0 {scoreDifference} {@clientStats}", scoreDifference, clientStats);
_log.WriteDebug($"{scoreDifference}-{clientStats.RoundScore} - {clientStats.LastScore} - {clientStats.SessionScore}");
clientStats.SPM = 0; clientStats.SPM = 0;
} }
@ -1110,8 +1107,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// fixme: how does this happen? // fixme: how does this happen?
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill)) if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
{ {
_log.WriteWarning("[StatManager::UpdateStats] clientStats SPM/Skill NaN"); _log.LogWarning("clientStats SPM/Skill NaN {@killInfo}", new {killSPM, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference});
_log.WriteDebug($"{killSPM}-{KDRWeight}-{totalPlayTime}-{SPMAgainstPlayWeight}-{clientStats.SPM}-{clientStats.Skill}-{scoreDifference}");
clientStats.SPM = 0; clientStats.SPM = 0;
clientStats.Skill = 0; clientStats.Skill = 0;
} }
@ -1126,14 +1122,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{ {
EFServerStatistics serverStats; EFServerStatistics serverStats;
using (var ctx = _contextFactory.CreateContext(enableTracking: false)) using var ctx = _contextFactory.CreateContext(enableTracking: false);
{
var serverStatsSet = ctx.Set<EFServerStatistics>(); var serverStatsSet = ctx.Set<EFServerStatistics>();
serverStats = serverStatsSet.FirstOrDefault(s => s.ServerId == serverId); serverStats = serverStatsSet.FirstOrDefault(s => s.ServerId == serverId);
if (serverStats == null) if (serverStats == null)
{ {
_log.WriteDebug($"Initializing server stats for {serverId}"); _log.LogDebug("Initializing server stats for {serverId}", serverId);
// server stats have never been generated before // server stats have never been generated before
serverStats = new EFServerStatistics() serverStats = new EFServerStatistics()
{ {
@ -1145,7 +1140,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
serverStats = serverStatsSet.Add(serverStats).Entity; serverStats = serverStatsSet.Add(serverStats).Entity;
ctx.SaveChanges(); ctx.SaveChanges();
} }
}
return serverStats; return serverStats;
} }
@ -1167,6 +1161,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public void ResetStats(EFClient client) public void ResetStats(EFClient client)
{ {
var stats = client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY); var stats = client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
// the cached stats have not been loaded yet
if (stats == null)
{
return;
}
stats.Kills = 0; stats.Kills = 0;
stats.Deaths = 0; stats.Deaths = 0;
stats.SPM = 0; stats.SPM = 0;
@ -1175,7 +1176,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
stats.EloRating = 200; stats.EloRating = 200;
} }
public async Task AddMessageAsync(int clientId, long serverId, string message) public async Task AddMessageAsync(int clientId, long serverId, bool sentIngame, string message)
{ {
// the web users can have no account // the web users can have no account
if (clientId < 1) if (clientId < 1)
@ -1183,19 +1184,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return; return;
} }
using (var ctx = _contextFactory.CreateContext(enableTracking: false)) await using var ctx = _contextFactory.CreateContext(enableTracking: false);
{
ctx.Set<EFClientMessage>().Add(new EFClientMessage() ctx.Set<EFClientMessage>().Add(new EFClientMessage()
{ {
ClientId = clientId, ClientId = clientId,
Message = message, Message = message,
ServerId = serverId, ServerId = serverId,
TimeSent = DateTime.UtcNow TimeSent = DateTime.UtcNow,
SentIngame = sentIngame
}); });
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
}
public async Task Sync(Server sv) public async Task Sync(Server sv)
{ {
@ -1206,12 +1206,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{ {
await waiter.WaitAsync(); await waiter.WaitAsync();
using (var ctx = _contextFactory.CreateContext()) await using var ctx = _contextFactory.CreateContext();
{
var serverStatsSet = ctx.Set<EFServerStatistics>(); var serverStatsSet = ctx.Set<EFServerStatistics>();
serverStatsSet.Update(_servers[serverId].ServerStatistics); serverStatsSet.Update(_servers[serverId].ServerStatistics);
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
}
foreach (var stats in sv.GetClientsAsList() foreach (var stats in sv.GetClientsAsList()
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY)) .Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))
@ -1225,8 +1223,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (Exception e) catch (Exception e)
{ {
_log.WriteError("There was a probably syncing server stats"); _log.LogError(e, "There was a problem syncing server stats");
_log.WriteDebug(e.GetExceptionInfo());
} }
finally finally

View File

@ -26,7 +26,7 @@ namespace Stats.Helpers
public async Task<ResourceQueryHelperResult<StatsInfoResult>> QueryResource(StatsInfoRequest query) public async Task<ResourceQueryHelperResult<StatsInfoResult>> QueryResource(StatsInfoRequest query)
{ {
var result = new ResourceQueryHelperResult<StatsInfoResult>(); var result = new ResourceQueryHelperResult<StatsInfoResult>();
using var context = _contextFactory.CreateContext(enableTracking: false); await using var context = _contextFactory.CreateContext(enableTracking: false);
// we need to get the ratings separately because there's not explicit FK // we need to get the ratings separately because there's not explicit FK
var ratings = await context.Set<EFClientRatingHistory>() var ratings = await context.Set<EFClientRatingHistory>()

View File

@ -17,5 +17,6 @@ namespace IW4MAdmin.Plugins.Stats.Models
public virtual EFClient Client { get; set; } public virtual EFClient Client { get; set; }
public string Message { get; set; } public string Message { get; set; }
public DateTime TimeSent { get; set; } public DateTime TimeSent { get; set; }
public bool SentIngame { get; set; }
} }
} }

View File

@ -23,8 +23,13 @@ namespace Stats.Models
builder.Entity<EFRating>() builder.Entity<EFRating>()
.HasIndex(p => new { p.Performance, p.Ranking, p.When }); .HasIndex(p => new { p.Performance, p.Ranking, p.When });
builder.Entity<EFClientMessage>() builder.Entity<EFRating>()
.HasIndex(p => p.TimeSent); .HasIndex(p => new { p.When, p.ServerId, p.Performance, p.ActivityAmount });
builder.Entity<EFClientMessage>(message =>
{
message.HasIndex(p => p.TimeSent);
});
// force pluralization // force pluralization
builder.Entity<EFClientKill>().ToTable("EFClientKills"); builder.Entity<EFClientKill>().ToTable("EFClientKills");

View File

@ -14,6 +14,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Commands;
namespace IW4MAdmin.Plugins.Stats namespace IW4MAdmin.Plugins.Stats
{ {
@ -28,23 +30,24 @@ namespace IW4MAdmin.Plugins.Stats
public static StatManager Manager { get; private set; } public static StatManager Manager { get; private set; }
public static IManager ServerManager; public static IManager ServerManager;
public static IConfigurationHandler<StatsConfiguration> Config { get; private set; } public static IConfigurationHandler<StatsConfiguration> Config { get; private set; }
#if DEBUG
int scriptDamageCount;
int scriptKillCount;
#endif
private readonly IDatabaseContextFactory _databaseContextFactory; private readonly IDatabaseContextFactory _databaseContextFactory;
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IMetaService _metaService; private readonly IMetaService _metaService;
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatQueryHelper; private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatQueryHelper;
private readonly ILogger<StatManager> _managerLogger;
private readonly ILogger<Plugin> _logger;
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory, public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory,
ITranslationLookup translationLookup, IMetaService metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper) ITranslationLookup translationLookup, IMetaService metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper, ILogger<StatManager> managerLogger)
{ {
Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings"); Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
_databaseContextFactory = databaseContextFactory; _databaseContextFactory = databaseContextFactory;
_translationLookup = translationLookup; _translationLookup = translationLookup;
_metaService = metaService; _metaService = metaService;
_chatQueryHelper = chatQueryHelper; _chatQueryHelper = chatQueryHelper;
_managerLogger = managerLogger;
_logger = logger;
} }
public async Task OnEventAsync(GameEvent E, Server S) public async Task OnEventAsync(GameEvent E, Server S)
@ -54,11 +57,6 @@ namespace IW4MAdmin.Plugins.Stats
case GameEvent.EventType.Start: case GameEvent.EventType.Start:
Manager.AddServer(S); Manager.AddServer(S);
break; break;
case GameEvent.EventType.Stop:
break;
case GameEvent.EventType.PreConnect:
await Manager.AddPlayer(E.Origin);
break;
case GameEvent.EventType.Disconnect: case GameEvent.EventType.Disconnect:
await Manager.RemovePlayer(E.Origin); await Manager.RemovePlayer(E.Origin);
break; break;
@ -66,7 +64,7 @@ namespace IW4MAdmin.Plugins.Stats
if (!string.IsNullOrEmpty(E.Data) && if (!string.IsNullOrEmpty(E.Data) &&
E.Origin.ClientId > 1) E.Origin.ClientId > 1)
{ {
await Manager.AddMessageAsync(E.Origin.ClientId, StatManager.GetIdForServer(S), E.Data); await Manager.AddMessageAsync(E.Origin.ClientId, StatManager.GetIdForServer(S), true, E.Data);
} }
break; break;
case GameEvent.EventType.MapChange: case GameEvent.EventType.MapChange:
@ -77,21 +75,13 @@ namespace IW4MAdmin.Plugins.Stats
case GameEvent.EventType.MapEnd: case GameEvent.EventType.MapEnd:
await Manager.Sync(S); await Manager.Sync(S);
break; break;
case GameEvent.EventType.JoinTeam: case GameEvent.EventType.Command:
break; var shouldPersist = !string.IsNullOrEmpty(E.Data) &&
case GameEvent.EventType.Broadcast: E.Extra is SayCommand;
break; if (shouldPersist)
case GameEvent.EventType.Tell: {
break; await Manager.AddMessageAsync(E.Origin.ClientId, StatManager.GetIdForServer(S), false, E.Data);
case GameEvent.EventType.Kick: }
break;
case GameEvent.EventType.Ban:
break;
case GameEvent.EventType.Unknown:
break;
case GameEvent.EventType.Report:
break;
case GameEvent.EventType.Flag:
break; break;
case GameEvent.EventType.ScriptKill: case GameEvent.EventType.ScriptKill:
string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0]; string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
@ -103,22 +93,14 @@ namespace IW4MAdmin.Plugins.Stats
E.Origin = E.Target; E.Origin = E.Target;
} }
#if DEBUG await EnsureClientsAdded(E.Origin, E.Target);
scriptKillCount++;
S.Logger.WriteInfo($"Start ScriptKill {scriptKillCount}");
#endif
await Manager.AddScriptHit(false, E.Time, E.Origin, E.Target, StatManager.GetIdForServer(S), S.CurrentMap.Name, killInfo[7], killInfo[8], await Manager.AddScriptHit(false, E.Time, E.Origin, E.Target, StatManager.GetIdForServer(S), S.CurrentMap.Name, killInfo[7], killInfo[8],
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]); killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]);
#if DEBUG
S.Logger.WriteInfo($"End ScriptKill {scriptKillCount}");
#endif
} }
else else
{ {
S.Logger.WriteDebug("Skipping script kill as it is ignored or data in customcallbacks is outdated/missing"); _logger.LogDebug("Skipping script kill as it is ignored or data in customcallbacks is outdated/missing");
} }
break; break;
case GameEvent.EventType.Kill: case GameEvent.EventType.Kill:
@ -130,6 +112,7 @@ namespace IW4MAdmin.Plugins.Stats
E.Origin = E.Target; E.Origin = E.Target;
} }
await EnsureClientsAdded(E.Origin, E.Target);
await Manager.AddStandardKill(E.Origin, E.Target); await Manager.AddStandardKill(E.Origin, E.Target);
} }
break; break;
@ -155,22 +138,14 @@ namespace IW4MAdmin.Plugins.Stats
E.Origin = E.Target; E.Origin = E.Target;
} }
#if DEBUG await EnsureClientsAdded(E.Origin, E.Target);
scriptDamageCount++;
S.Logger.WriteInfo($"Start ScriptDamage {scriptDamageCount}");
#endif
await Manager.AddScriptHit(true, E.Time, E.Origin, E.Target, StatManager.GetIdForServer(S), S.CurrentMap.Name, killInfo[7], killInfo[8], await Manager.AddScriptHit(true, E.Time, E.Origin, E.Target, StatManager.GetIdForServer(S), S.CurrentMap.Name, killInfo[7], killInfo[8],
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]); killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]);
#if DEBUG
S.Logger.WriteInfo($"End ScriptDamage {scriptDamageCount}");
#endif
} }
else else
{ {
S.Logger.WriteDebug("Skipping script damage as it is ignored or data in customcallbacks is outdated/missing"); _logger.LogDebug("Skipping script damage as it is ignored or data in customcallbacks is outdated/missing");
} }
break; break;
} }
@ -198,11 +173,9 @@ namespace IW4MAdmin.Plugins.Stats
{ {
IList<EFClientStatistics> clientStats; IList<EFClientStatistics> clientStats;
int messageCount = 0; int messageCount = 0;
using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false)) await using var ctx = _databaseContextFactory.CreateContext(enableTracking: false);
{
clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == request.ClientId).ToListAsync(); clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == request.ClientId).ToListAsync();
messageCount = await ctx.Set<EFClientMessage>().CountAsync(_message => _message.ClientId == request.ClientId); messageCount = await ctx.Set<EFClientMessage>().CountAsync(_message => _message.ClientId == request.ClientId);
}
int kills = clientStats.Sum(c => c.Kills); int kills = clientStats.Sum(c => c.Kills);
int deaths = clientStats.Sum(c => c.Deaths); int deaths = clientStats.Sum(c => c.Deaths);
@ -277,13 +250,11 @@ namespace IW4MAdmin.Plugins.Stats
{ {
IList<EFClientStatistics> clientStats; IList<EFClientStatistics> clientStats;
using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false)) await using var ctx = _databaseContextFactory.CreateContext(enableTracking: false);
{
clientStats = await ctx.Set<EFClientStatistics>() clientStats = await ctx.Set<EFClientStatistics>()
.Include(c => c.HitLocations) .Include(c => c.HitLocations)
.Where(c => c.ClientId == request.ClientId) .Where(c => c.ClientId == request.ClientId)
.ToListAsync(); .ToListAsync();
}
double headRatio = 0; double headRatio = 0;
double chestRatio = 0; double chestRatio = 0;
@ -416,32 +387,28 @@ namespace IW4MAdmin.Plugins.Stats
async Task<string> totalKills(Server server) async Task<string> totalKills(Server server)
{ {
using (var ctx = new DatabaseContext(disableTracking: true)) await using var context = _databaseContextFactory.CreateContext(false);
{ long kills = await context.Set<EFServerStatistics>().Where(s => s.Active).SumAsync(s => s.TotalKills);
long kills = await ctx.Set<EFServerStatistics>().Where(s => s.Active).SumAsync(s => s.TotalKills);
return kills.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)); return kills.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName));
} }
}
async Task<string> totalPlayTime(Server server) async Task<string> totalPlayTime(Server server)
{ {
using (var ctx = new DatabaseContext(disableTracking: true)) await using var context = _databaseContextFactory.CreateContext(false);
{ long playTime = await context.Set<EFServerStatistics>().Where(s => s.Active).SumAsync(s => s.TotalPlayTime);
long playTime = await ctx.Set<EFServerStatistics>().Where(s => s.Active).SumAsync(s => s.TotalPlayTime);
return (playTime / 3600.0).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)); return (playTime / 3600.0).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName));
} }
}
async Task<string> topStats(Server s) async Task<string> topStats(Server s)
{ {
// todo: this needs to needs to be updated when we DI the lookup // todo: this needs to needs to be updated when we DI the lookup
return string.Join(Environment.NewLine, await Commands.TopStats.GetTopStats(s, Utilities.CurrentLocalization.LocalizationIndex)); return string.Join(Environment.NewLine, await Commands.TopStats.GetTopStats(s, Utilities.CurrentLocalization.LocalizationIndex, _databaseContextFactory));
} }
async Task<string> mostPlayed(Server s) async Task<string> mostPlayed(Server s)
{ {
// todo: this needs to needs to be updated when we DI the lookup // todo: this needs to needs to be updated when we DI the lookup
return string.Join(Environment.NewLine, await Commands.MostPlayedCommand.GetMostPlayed(s, Utilities.CurrentLocalization.LocalizationIndex)); return string.Join(Environment.NewLine, await Commands.MostPlayedCommand.GetMostPlayed(s, Utilities.CurrentLocalization.LocalizationIndex, _databaseContextFactory));
} }
async Task<string> mostKills(Server gameServer) async Task<string> mostKills(Server gameServer)
@ -457,7 +424,7 @@ namespace IW4MAdmin.Plugins.Stats
manager.GetMessageTokens().Add(new MessageToken("MOSTKILLS", mostKills)); manager.GetMessageTokens().Add(new MessageToken("MOSTKILLS", mostKills));
ServerManager = manager; ServerManager = manager;
Manager = new StatManager(manager, _databaseContextFactory, Config); Manager = new StatManager(_managerLogger, manager, _databaseContextFactory, Config);
} }
public Task OnTickAsync(Server S) public Task OnTickAsync(Server S)
@ -498,5 +465,21 @@ namespace IW4MAdmin.Plugins.Stats
/// <param name="s"></param> /// <param name="s"></param>
/// <returns></returns> /// <returns></returns>
private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().AnticheatConfiguration.Enable && s.GameName == Server.Game.IW5; private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().AnticheatConfiguration.Enable && s.GameName == Server.Game.IW5;
/// <summary>
/// Makes sure both clients are added
/// </summary>
/// <param name="origin"></param>
/// <param name="target"></param>
/// <returns></returns>
private async Task EnsureClientsAdded(EFClient origin, EFClient target)
{
await Manager.AddPlayer(origin);
if (!origin.Equals(target))
{
await Manager.AddPlayer(target);
}
}
} }
} }

View File

@ -13,13 +13,15 @@
<Copyright>2018</Copyright> <Copyright>2018</Copyright>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>8.0</LangVersion> <LangVersion>8.0</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2020.12.20.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" /> <Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target> </Target>
</Project> </Project>

View File

@ -1,11 +1,12 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using Stats.Dtos; using Stats.Dtos;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace StatsWeb.API namespace StatsWeb.API
{ {
@ -16,7 +17,7 @@ namespace StatsWeb.API
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IResourceQueryHelper<StatsInfoRequest, StatsInfoResult> _statsQueryHelper; private readonly IResourceQueryHelper<StatsInfoRequest, StatsInfoResult> _statsQueryHelper;
public StatsController(ILogger logger, IResourceQueryHelper<StatsInfoRequest, StatsInfoResult> statsQueryHelper) public StatsController(ILogger<StatsController> logger, IResourceQueryHelper<StatsInfoRequest, StatsInfoResult> statsQueryHelper)
{ {
_statsQueryHelper = statsQueryHelper; _statsQueryHelper = statsQueryHelper;
_logger = logger; _logger = logger;
@ -56,8 +57,7 @@ namespace StatsWeb.API
catch (Exception e) catch (Exception e)
{ {
_logger.WriteWarning($"Could not get client stats for client id {clientId}"); _logger.LogWarning(e, "Could not get client stats for client id {clientId}", clientId);
_logger.WriteDebug(e.GetExceptionInfo());
return StatusCode(StatusCodes.Status500InternalServerError, new ErrorResponse return StatusCode(StatusCodes.Status500InternalServerError, new ErrorResponse
{ {

View File

@ -10,6 +10,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace StatsWeb namespace StatsWeb
{ {
@ -23,7 +25,7 @@ namespace StatsWeb
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
private List<EFServer> serverCache; private List<EFServer> serverCache;
public ChatResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory, ApplicationConfiguration appConfig) public ChatResourceQueryHelper(ILogger<ChatResourceQueryHelper> logger, IDatabaseContextFactory contextFactory, ApplicationConfiguration appConfig)
{ {
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
@ -39,7 +41,7 @@ namespace StatsWeb
} }
var result = new ResourceQueryHelperResult<MessageResponse>(); var result = new ResourceQueryHelperResult<MessageResponse>();
using var context = _contextFactory.CreateContext(enableTracking: false); await using var context = _contextFactory.CreateContext(enableTracking: false);
if (serverCache == null) if (serverCache == null)
{ {
@ -79,7 +81,8 @@ namespace StatsWeb
When = _message.TimeSent, When = _message.TimeSent,
Message = _message.Message, Message = _message.Message,
ServerName = query.IsProfileMeta ? "" : _message.Server.HostName, ServerName = query.IsProfileMeta ? "" : _message.Server.HostName,
GameName = _message.Server.GameName == null ? Server.Game.IW4 : _message.Server.GameName.Value GameName = _message.Server.GameName == null ? Server.Game.IW4 : _message.Server.GameName.Value,
SentIngame = _message.SentIngame
}); });
if (query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending) if (query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending)

View File

@ -10,8 +10,11 @@ using SharedLibraryCore.Interfaces;
using Stats.Dtos; using Stats.Dtos;
using StatsWeb.Extensions; using StatsWeb.Extensions;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
{ {
@ -21,14 +24,17 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
private readonly IManager _manager; private readonly IManager _manager;
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatResourceQueryHelper; private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatResourceQueryHelper;
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IDatabaseContextFactory _contextFactory;
public StatsController(ILogger logger, IManager manager, IResourceQueryHelper<ChatSearchQuery, MessageResponse> resourceQueryHelper, public StatsController(ILogger<StatsController> logger, IManager manager, IResourceQueryHelper<ChatSearchQuery,
ITranslationLookup translationLookup) : base(manager) MessageResponse> resourceQueryHelper, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(manager)
{ {
_logger = logger; _logger = logger;
_manager = manager; _manager = manager;
_chatResourceQueryHelper = resourceQueryHelper; _chatResourceQueryHelper = resourceQueryHelper;
_translationLookup = translationLookup; _translationLookup = translationLookup;
_contextFactory = contextFactory;
} }
[HttpGet] [HttpGet]
@ -86,7 +92,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
SentAfter = whenLower SentAfter = whenLower
}); });
return View("_MessageContext", messages.Results); return View("_MessageContext", messages.Results.ToList());
} }
[HttpGet("Message/Find")] [HttpGet("Message/Find")]
@ -108,15 +114,13 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
catch (ArgumentException e) catch (ArgumentException e)
{ {
_logger.WriteWarning($"Could not parse chat message search query - {query}"); _logger.LogWarning(e, "Could not parse chat message search query {query}", query);
_logger.WriteDebug(e.GetExceptionInfo());
ViewBag.Error = e; ViewBag.Error = e;
} }
catch (FormatException e) catch (FormatException e)
{ {
_logger.WriteWarning($"Could not parse chat message search query filter format - {query}"); _logger.LogWarning(e, "Could not parse chat message search query filter format {query}", query);
_logger.WriteDebug(e.GetExceptionInfo());
ViewBag.Error = e; ViewBag.Error = e;
} }
@ -136,15 +140,13 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
catch (ArgumentException e) catch (ArgumentException e)
{ {
_logger.WriteWarning($"Could not parse chat message search query - {query}"); _logger.LogWarning(e, "Could not parse chat message search query {query}", query);
_logger.WriteDebug(e.GetExceptionInfo());
throw; throw;
} }
catch (FormatException e) catch (FormatException e)
{ {
_logger.WriteWarning($"Could not parse chat message search query filter format - {query}"); _logger.LogWarning(e, "Could not parse chat message search query filter format {query}", query);
_logger.WriteDebug(e.GetExceptionInfo());
throw; throw;
} }
@ -156,9 +158,9 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
[Authorize] [Authorize]
public async Task<IActionResult> GetAutomatedPenaltyInfoAsync(int penaltyId) public async Task<IActionResult> GetAutomatedPenaltyInfoAsync(int penaltyId)
{ {
using (var ctx = new SharedLibraryCore.Database.DatabaseContext(true)) await using var context = _contextFactory.CreateContext(false);
{
var penalty = await ctx.Penalties var penalty = await context.Penalties
.Select(_penalty => new { _penalty.OffenderId, _penalty.PenaltyId, _penalty.When, _penalty.AutomatedOffense }) .Select(_penalty => new { _penalty.OffenderId, _penalty.PenaltyId, _penalty.When, _penalty.AutomatedOffense })
.FirstOrDefaultAsync(_penalty => _penalty.PenaltyId == penaltyId); .FirstOrDefaultAsync(_penalty => _penalty.PenaltyId == penaltyId);
@ -168,7 +170,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
} }
// todo: this can be optimized // todo: this can be optimized
var iqSnapshotInfo = ctx.Set<Stats.Models.EFACSnapshot>() var iqSnapshotInfo = context.Set<Stats.Models.EFACSnapshot>()
.Where(s => s.ClientId == penalty.OffenderId) .Where(s => s.ClientId == penalty.OffenderId)
.Include(s => s.LastStrainAngle) .Include(s => s.LastStrainAngle)
.Include(s => s.HitOrigin) .Include(s => s.HitOrigin)
@ -189,17 +191,16 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
// we want to show anything related to the automated offense // we want to show anything related to the automated offense
else else
{ {
return View("_MessageContext", new[] return View("_MessageContext", new List<MessageResponse>
{ {
new ChatInfo() new MessageResponse()
{ {
ClientId = penalty.OffenderId, ClientId = penalty.OffenderId,
Message = penalty.AutomatedOffense, Message = penalty.AutomatedOffense,
Time = penalty.When When = penalty.When
} }
}); });
} }
} }
} }
}
} }

View File

@ -14,7 +14,7 @@
<RunPostBuildEvent>Always</RunPostBuildEvent> <RunPostBuildEvent>Always</RunPostBuildEvent>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2020.12.20.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -6,21 +6,37 @@
string rankIcon(double elo) string rankIcon(double elo)
{ {
if (elo >= getDeviation(-0.75) && elo < getDeviation(1.25)) if (elo >= getDeviation(-0.75) && elo < getDeviation(1.25))
{
return "0_no-place/menu_div_no_place.png"; return "0_no-place/menu_div_no_place.png";
}
if (elo >= getDeviation(0.125) && elo < getDeviation(0.625)) if (elo >= getDeviation(0.125) && elo < getDeviation(0.625))
{
return "1_iron/menu_div_iron_sub03.png"; return "1_iron/menu_div_iron_sub03.png";
}
if (elo >= getDeviation(0.625) && elo < getDeviation(1.0)) if (elo >= getDeviation(0.625) && elo < getDeviation(1.0))
{
return "2_bronze/menu_div_bronze_sub03.png"; return "2_bronze/menu_div_bronze_sub03.png";
}
if (elo >= getDeviation(1.0) && elo < getDeviation(1.25)) if (elo >= getDeviation(1.0) && elo < getDeviation(1.25))
{
return "3_silver/menu_div_silver_sub03.png"; return "3_silver/menu_div_silver_sub03.png";
}
if (elo >= getDeviation(1.25) && elo < getDeviation(1.5)) if (elo >= getDeviation(1.25) && elo < getDeviation(1.5))
{
return "4_gold/menu_div_gold_sub03.png"; return "4_gold/menu_div_gold_sub03.png";
}
if (elo >= getDeviation(1.5) && elo < getDeviation(1.75)) if (elo >= getDeviation(1.5) && elo < getDeviation(1.75))
{
return "5_platinum/menu_div_platinum_sub03.png"; return "5_platinum/menu_div_platinum_sub03.png";
}
if (elo >= getDeviation(1.75) && elo < getDeviation(2.0)) if (elo >= getDeviation(1.75) && elo < getDeviation(2.0))
{
return "6_semipro/menu_div_semipro_sub03.png"; return "6_semipro/menu_div_semipro_sub03.png";
}
if (elo >= getDeviation(2.0)) if (elo >= getDeviation(2.0))
{
return "7_pro/menu_div_pro_sub03.png"; return "7_pro/menu_div_pro_sub03.png";
}
return "0_no-place/menu_div_no_place.png"; return "0_no-place/menu_div_no_place.png";
} }

View File

@ -1,5 +1,5 @@
@using SharedLibraryCore.Dtos.Meta.Responses @using SharedLibraryCore.Dtos.Meta.Responses
@model IEnumerable<MessageResponse> @model IList<MessageResponse>
@{ @{
Layout = null; Layout = null;
} }

View File

@ -65,10 +65,12 @@ namespace IW4MAdmin.Plugins.Welcome
public string Name => "Welcome Plugin"; public string Name => "Welcome Plugin";
private readonly IConfigurationHandler<WelcomeConfiguration> _configHandler; private readonly IConfigurationHandler<WelcomeConfiguration> _configHandler;
private readonly IDatabaseContextFactory _contextFactory;
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory) public Plugin(IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory contextFactory)
{ {
_configHandler = configurationHandlerFactory.GetConfigurationHandler<WelcomeConfiguration>("WelcomePluginSettings"); _configHandler = configurationHandlerFactory.GetConfigurationHandler<WelcomeConfiguration>("WelcomePluginSettings");
_contextFactory = contextFactory;
} }
public async Task OnLoadAsync(IManager manager) public async Task OnLoadAsync(IManager manager)
@ -98,9 +100,9 @@ namespace IW4MAdmin.Plugins.Welcome
{ {
string penaltyReason; string penaltyReason;
using (var ctx = new DatabaseContext(disableTracking: true)) await using var context = _contextFactory.CreateContext(false);
{ {
penaltyReason = await ctx.Penalties penaltyReason = await context.Penalties
.Where(p => p.OffenderId == newPlayer.ClientId && p.Type == EFPenalty.PenaltyType.Flag) .Where(p => p.OffenderId == newPlayer.ClientId && p.Type == EFPenalty.PenaltyType.Flag)
.OrderByDescending(p => p.When) .OrderByDescending(p => p.When)
.Select(p => p.AutomatedOffense ?? p.Offense) .Select(p => p.AutomatedOffense ?? p.Offense)

View File

@ -12,11 +12,11 @@
<Description>Welcome plugin for IW4MAdmin welcomes clients to the server</Description> <Description>Welcome plugin for IW4MAdmin welcomes clients to the server</Description>
<Copyright>2018</Copyright> <Copyright>2018</Copyright>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion> <LangVersion>Latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2020.12.20.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -3,7 +3,7 @@
[![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/J3J821KUJ) [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/J3J821KUJ)
## About ## About
**IW4MAdmin** is an administration tool for [IW4x](https://iw4x.org/), [Pluto T6](https://forum.plutonium.pw/category/6/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/14/plutonium-iw5), [CoD4x](https://cod4x.me/), [TeknoMW3](https://github.com/Musta1337/TeknoMW3), and most Call of Duty® dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze. **IW4MAdmin** is an administration tool for [IW4x](https://iw4x.org/), [IW6x](https://xlabs.dev), [Pluto T6](https://forum.plutonium.pw/category/6/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/14/plutonium-iw5), [CoD4x](https://cod4x.me/), [TeknoMW3](https://github.com/Musta1337/TeknoMW3), and most Call of Duty® dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze.
### Download ### Download
Latest binary builds are always available at: Latest binary builds are always available at:
- [GitHub](https://github.com/RaidMax/IW4M-Admin/releases) - [GitHub](https://github.com/RaidMax/IW4M-Admin/releases)

View File

@ -1,7 +0,0 @@
dotnet publish WebfrontCore/WebfrontCore.csproj -c Prerelease -f netcoreapp3.0 --force -o X:\IW4MAdmin\Publish\WindowsPrerelease /p:PublishProfile=Prerelease
dotnet publish Application/Application.csproj -c Prerelease -f netcoreapp3.0 --force -o X:\IW4MAdmin\Publish\WindowsPrerelease /p:PublishProfile=Prerelease
dotnet publish GameLogServer/GameLogServer.pyproj -c Release -f netcoreapp3.0 --force -o X:\IW4MAdmin\Publish\WindowsPrerelease\GameLogServer
call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\Tools\VsDevCmd.bat"
msbuild GameLogServer/GameLogServer.pyproj /p:PublishProfile=PreRelease /p:DeployOnBuild=true /p:PublishProfileRootFolder=X:\IW4MAdmin\GameLogServer\
cd "X:\IW4MAdmin\DEPLOY\"
PowerShell ".\upload_prerelease.ps1"

View File

@ -1,7 +0,0 @@
dotnet publish WebfrontCore/WebfrontCore.csproj -c Release -f netcoreapp3.0 --force -o X:\IW4MAdmin\Publish\Windows /p:PublishProfile=sTABLE
dotnet publish Application/Application.csproj -c Release -f netcoreapp3.0 --force -o X:\IW4MAdmin\Publish\Windows /p:PublishProfile=Stable
dotnet publish GameLogServer/GameLogServer.pyproj -c Release -f netcoreapp3.0 --force -o X:\IW4MAdmin\Publish\WindowsPrerelease\GameLogServer
call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\Tools\VsDevCmd.bat"
msbuild GameLogServer/GameLogServer.pyproj /p:PublishProfile=Stable /p:DeployOnBuild=true /p:PublishProfileRootFolder=X:\IW4MAdmin\GameLogServer\
cd "X:\IW4MAdmin\DEPLOY\"
PowerShell ".\upload_release.ps1"

View File

@ -134,6 +134,7 @@ namespace SharedLibraryCore
ViewBag.Localization = Utilities.CurrentLocalization.LocalizationIndex; ViewBag.Localization = Utilities.CurrentLocalization.LocalizationIndex;
ViewBag.CustomBranding = Manager.GetApplicationSettings().Configuration().WebfrontCustomBranding ?? "IW4MAdmin"; ViewBag.CustomBranding = Manager.GetApplicationSettings().Configuration().WebfrontCustomBranding ?? "IW4MAdmin";
ViewBag.EnableColorCodes = Manager.GetApplicationSettings().Configuration().EnableColorCodes; ViewBag.EnableColorCodes = Manager.GetApplicationSettings().Configuration().EnableColorCodes;
ViewBag.EnablePrivilegedUserPrivacy = Manager.GetApplicationSettings().Configuration().EnablePrivilegedUserPrivacy;
base.OnActionExecuting(context); base.OnActionExecuting(context);
} }

View File

@ -6,6 +6,7 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace SharedLibraryCore namespace SharedLibraryCore
{ {

View File

@ -12,6 +12,8 @@ using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
namespace SharedLibraryCore.Commands namespace SharedLibraryCore.Commands
@ -99,7 +101,8 @@ namespace SharedLibraryCore.Commands
/// </summary> /// </summary>
public class WarnCommand : Command public class WarnCommand : Command
{ {
public WarnCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) private readonly ApplicationConfiguration _appConfig;
public WarnCommand(ApplicationConfiguration appConfig, CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{ {
Name = "warn"; Name = "warn";
Description = _translationLookup["COMMANDS_WARN_DESC"]; Description = _translationLookup["COMMANDS_WARN_DESC"];
@ -119,13 +122,15 @@ namespace SharedLibraryCore.Commands
Required = true Required = true
} }
}; };
_appConfig = appConfig;
} }
public override Task ExecuteAsync(GameEvent E) public override Task ExecuteAsync(GameEvent gameEvent)
{ {
if (E.Target.Warn(E.Data, E.Origin).Failed) var reason = gameEvent.Data.FindRuleForReason(_appConfig, gameEvent.Owner);
if (gameEvent.Target.Warn(reason, gameEvent.Origin).Failed)
{ {
E.Origin.Tell(_translationLookup["COMMANDS_WARN_FAIL"].FormatExt(E.Target.Name)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_WARN_FAIL"].FormatExt(gameEvent.Target.Name));
} }
return Task.CompletedTask; return Task.CompletedTask;
@ -170,7 +175,9 @@ namespace SharedLibraryCore.Commands
/// </summary> /// </summary>
public class KickCommand : Command public class KickCommand : Command
{ {
public KickCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) private readonly ApplicationConfiguration _appConfig;
public KickCommand(ApplicationConfiguration appConfig, CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{ {
Name = "kick"; Name = "kick";
Description = _translationLookup["COMMANDS_KICK_DESC"]; Description = _translationLookup["COMMANDS_KICK_DESC"];
@ -190,20 +197,22 @@ namespace SharedLibraryCore.Commands
Required = true Required = true
} }
}; };
_appConfig = appConfig;
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
switch ((await E.Target.Kick(E.Data, E.Origin).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason) var reason = gameEvent.Data.FindRuleForReason(_appConfig, gameEvent.Owner);
switch ((await gameEvent.Target.Kick(reason, gameEvent.Origin).WaitAsync(Utilities.DefaultCommandTimeout, gameEvent.Owner.Manager.CancellationToken)).FailReason)
{ {
case GameEvent.EventFailReason.None: case GameEvent.EventFailReason.None:
E.Origin.Tell(_translationLookup["COMMANDS_KICK_SUCCESS"].FormatExt(E.Target.Name)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_KICK_SUCCESS"].FormatExt(gameEvent.Target.Name));
break; break;
case GameEvent.EventFailReason.Exception: case GameEvent.EventFailReason.Exception:
E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]); gameEvent.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
break; break;
default: default:
E.Origin.Tell(_translationLookup["COMMANDS_KICK_FAIL"].FormatExt(E.Target.Name)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_KICK_FAIL"].FormatExt(gameEvent.Target.Name));
break; break;
} }
} }
@ -239,12 +248,50 @@ namespace SharedLibraryCore.Commands
} }
} }
/// <summary>
/// Prints out a message to all clients on all servers
/// </summary>
public class SayAllCommand : Command
{
public SayAllCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{
Name = "sayall";
Description = _translationLookup["COMMANDS_SAY_ALL_DESC"];
Alias = "sa";
Permission = Permission.Moderator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument()
{
Name = _translationLookup["COMMANDS_ARGS_MESSAGE"],
Required = true
}
};
}
public override Task ExecuteAsync(GameEvent E)
{
string message = _translationLookup["COMMANDS_SAY_ALL_MESSAGE_FORMAT"].FormatExt(E.Origin.Name, E.Data);
foreach (var server in E.Owner.Manager.GetServers())
{
server.Broadcast(message, E.Origin);
}
E.Origin.Tell(_translationLookup["COMMANDS_SAY_SUCCESS"]);
return Task.CompletedTask;
}
}
/// <summary> /// <summary>
/// Temporarily bans a client /// Temporarily bans a client
/// </summary> /// </summary>
public class TempBanCommand : Command public class TempBanCommand : Command
{ {
public TempBanCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) private readonly ApplicationConfiguration _appConfig;
public TempBanCommand(ApplicationConfiguration appConfig, CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{ {
Name = "tempban"; Name = "tempban";
Description = _translationLookup["COMMANDS_TEMPBAN_DESC"]; Description = _translationLookup["COMMANDS_TEMPBAN_DESC"];
@ -269,35 +316,36 @@ namespace SharedLibraryCore.Commands
Required = true Required = true
} }
}; };
_appConfig = appConfig;
} }
private static readonly string TempBanRegex = @"([0-9]+\w+)\ (.+)"; private static readonly string TempBanRegex = @"([0-9]+\w+)\ (.+)";
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
var match = Regex.Match(E.Data, TempBanRegex); var match = Regex.Match(gameEvent.Data, TempBanRegex);
if (match.Success) if (match.Success)
{ {
string tempbanReason = match.Groups[2].ToString(); var tempbanReason = match.Groups[2].ToString().FindRuleForReason(_appConfig, gameEvent.Owner);
var length = match.Groups[1].ToString().ParseTimespan(); var length = match.Groups[1].ToString().ParseTimespan();
if (length > E.Owner.Manager.GetApplicationSettings().Configuration().MaximumTempBanTime) if (length > gameEvent.Owner.Manager.GetApplicationSettings().Configuration().MaximumTempBanTime)
{ {
E.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_FAIL_TOOLONG"]); gameEvent.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_FAIL_TOOLONG"]);
} }
else else
{ {
switch ((await E.Target.TempBan(tempbanReason, length, E.Origin).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason) switch ((await gameEvent.Target.TempBan(tempbanReason, length, gameEvent.Origin).WaitAsync(Utilities.DefaultCommandTimeout, gameEvent.Owner.Manager.CancellationToken)).FailReason)
{ {
case GameEvent.EventFailReason.None: case GameEvent.EventFailReason.None:
E.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_SUCCESS"].FormatExt(E.Target, length.HumanizeForCurrentCulture())); gameEvent.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_SUCCESS"].FormatExt(gameEvent.Target, length.HumanizeForCurrentCulture()));
break; break;
case GameEvent.EventFailReason.Exception: case GameEvent.EventFailReason.Exception:
E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]); gameEvent.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
break; break;
default: default:
E.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_FAIL"].FormatExt(E.Target.Name)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_FAIL"].FormatExt(gameEvent.Target.Name));
break; break;
} }
} }
@ -310,7 +358,8 @@ namespace SharedLibraryCore.Commands
/// </summary> /// </summary>
public class BanCommand : Command public class BanCommand : Command
{ {
public BanCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) private readonly ApplicationConfiguration _appConfig;
public BanCommand(ApplicationConfiguration appConfig, CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{ {
Name = "ban"; Name = "ban";
Description = _translationLookup["COMMANDS_BAN_DESC"]; Description = _translationLookup["COMMANDS_BAN_DESC"];
@ -330,20 +379,22 @@ namespace SharedLibraryCore.Commands
Required = true Required = true
} }
}; };
_appConfig = appConfig;
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
switch ((await E.Target.Ban(E.Data, E.Origin, false).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason) var reason = gameEvent.Data.FindRuleForReason(_appConfig, gameEvent.Owner);
switch ((await gameEvent.Target.Ban(reason, gameEvent.Origin, false).WaitAsync(Utilities.DefaultCommandTimeout, gameEvent.Owner.Manager.CancellationToken)).FailReason)
{ {
case GameEvent.EventFailReason.None: case GameEvent.EventFailReason.None:
E.Origin.Tell(_translationLookup["COMMANDS_BAN_SUCCESS"].FormatExt(E.Target.Name)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_BAN_SUCCESS"].FormatExt(gameEvent.Target.Name));
break; break;
case GameEvent.EventFailReason.Exception: case GameEvent.EventFailReason.Exception:
E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]); gameEvent.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
break; break;
default: default:
E.Origin.Tell(_translationLookup["COMMANDS_BAN_FAIL"].FormatExt(E.Target.Name)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_BAN_FAIL"].FormatExt(gameEvent.Target.Name));
break; break;
} }
} }
@ -596,11 +647,11 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
var _ = !E.Origin.Masked ? _ = !E.Origin.Masked ?
E.Owner.Broadcast($"{_translationLookup["COMMANDS_MAPROTATE"]} [^5{E.Origin.Name}^7]", E.Origin) : E.Owner.Broadcast($"{_translationLookup["COMMANDS_MAPROTATE"]} [^5{E.Origin.Name}^7]", E.Origin) :
E.Owner.Broadcast(_translationLookup["COMMANDS_MAPROTATE"], E.Origin); E.Owner.Broadcast(_translationLookup["COMMANDS_MAPROTATE"], E.Origin);
await Task.Delay(5000); await Task.Delay(E.Owner.Manager.GetApplicationSettings().Configuration().MapChangeDelaySeconds * 1000);
await E.Owner.ExecuteCommandAsync("map_rotate"); await E.Owner.ExecuteCommandAsync("map_rotate");
} }
} }
@ -610,7 +661,7 @@ namespace SharedLibraryCore.Commands
/// </summary> /// </summary>
public class SetLevelCommand : Command public class SetLevelCommand : Command
{ {
public SetLevelCommand(CommandConfiguration config, ITranslationLookup translationLookup, ILogger logger) : base(config, translationLookup) public SetLevelCommand(CommandConfiguration config, ITranslationLookup translationLookup, ILogger<SetLevelCommand> logger) : base(config, translationLookup)
{ {
Name = "setlevel"; Name = "setlevel";
Description = _translationLookup["COMMANDS_SETLEVEL_DESC"]; Description = _translationLookup["COMMANDS_SETLEVEL_DESC"];
@ -675,6 +726,12 @@ namespace SharedLibraryCore.Commands
return; return;
} }
else if (gameEvent.Target.Level == Permission.Flagged)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SETLEVEL_FLAGGED"].FormatExt(gameEvent.Target.Name));
return;
}
// stepped privilege is enabled, but the new level is too high // stepped privilege is enabled, but the new level is too high
else if (steppedPrivileges && !canPromoteSteppedPriv) else if (steppedPrivileges && !canPromoteSteppedPriv)
{ {
@ -691,13 +748,26 @@ namespace SharedLibraryCore.Commands
gameEvent.Owner.Manager.GetActiveClients() gameEvent.Owner.Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient : targetClient; .FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient : targetClient;
logger.WriteInfo($"Beginning set level of client {gameEvent.Origin} to {newPerm}"); logger.LogDebug("Beginning set level of client {origin} to {newPermission}", gameEvent.Origin.ToString(), newPerm);
var result = await targetClient.SetLevel(newPerm, gameEvent.Origin).WaitAsync(Utilities.DefaultCommandTimeout, gameEvent.Owner.Manager.CancellationToken); var result = await targetClient.SetLevel(newPerm, gameEvent.Origin).WaitAsync(Utilities.DefaultCommandTimeout, gameEvent.Owner.Manager.CancellationToken);
if (result.Failed) if (result.Failed)
{ {
logger.WriteInfo($"Failed to set level of client {gameEvent.Origin}"); // user is the same level
if (result.FailReason == GameEvent.EventFailReason.Invalid)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SETLEVEL_INVALID"]
.FormatExt(gameEvent.Target.Name, newPerm.ToString()));
return;
}
using (LogContext.PushProperty("Server", gameEvent.Origin.CurrentServer?.ToString()))
{
logger.LogWarning("Failed to set level of client {origin} {reason}",
gameEvent.Origin.ToString(),
result.FailReason);
}
gameEvent.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]); gameEvent.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
return; return;
} }
@ -760,9 +830,9 @@ namespace SharedLibraryCore.Commands
public override Task ExecuteAsync(GameEvent E) public override Task ExecuteAsync(GameEvent E)
{ {
TimeSpan uptime = DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime; var uptime = DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime;
var loc = _translationLookup; var loc = _translationLookup;
E.Origin.Tell(loc["COMMANDS_UPTIME_TEXT"].FormatExt(uptime.Days, uptime.Hours, uptime.Minutes)); E.Origin.Tell(loc["COMMANDS_UPTIME_TEXT"].FormatExt(uptime.HumanizeForCurrentCulture(4)));
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
@ -828,22 +898,18 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
string newMap = E.Data.Trim().ToLower(); string newMap = E.Data.Trim();
foreach (Map m in E.Owner.Maps) int delay = E.Owner.Manager.GetApplicationSettings().Configuration().MapChangeDelaySeconds * 1000;
{
if (m.Name.ToLower() == newMap || m.Alias.ToLower() == newMap)
{
E.Owner.Broadcast(_translationLookup["COMMANDS_MAP_SUCCESS"].FormatExt(m.Alias));
await Task.Delay((int)(Utilities.DefaultCommandTimeout.TotalMilliseconds / 2.0));
await E.Owner.LoadMap(m.Name);
return;
}
}
// todo: this can be moved into a single statement var foundMap = E.Owner.Maps.FirstOrDefault(_map => _map.Name.Equals(newMap, StringComparison.InvariantCultureIgnoreCase) ||
E.Owner.Broadcast(_translationLookup["COMMANDS_MAP_UKN"].FormatExt(newMap)); _map.Alias.Equals(newMap, StringComparison.InvariantCultureIgnoreCase));
await Task.Delay(5000);
await E.Owner.LoadMap(newMap); _ = foundMap == null ?
E.Owner.Broadcast(_translationLookup["COMMANDS_MAP_UKN"].FormatExt(newMap)) :
E.Owner.Broadcast(_translationLookup["COMMANDS_MAP_SUCCESS"].FormatExt(foundMap.Alias));
await Task.Delay(delay);
await E.Owner.LoadMap(foundMap?.Name ?? newMap);
} }
} }
@ -1384,7 +1450,10 @@ namespace SharedLibraryCore.Commands
/// </summary> /// </summary>
public class PruneAdminsCommand : Command public class PruneAdminsCommand : Command
{ {
public PruneAdminsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) private readonly IDatabaseContextFactory _contextFactory;
public PruneAdminsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup)
{ {
Name = "prune"; Name = "prune";
Description = _translationLookup["COMMANDS_PRUNE_DESC"]; Description = _translationLookup["COMMANDS_PRUNE_DESC"];
@ -1426,8 +1495,7 @@ namespace SharedLibraryCore.Commands
List<EFClient> inactiveUsers = null; List<EFClient> inactiveUsers = null;
// todo: make an event for this // todo: make an event for this
// update user roles // update user roles
using (var context = new DatabaseContext()) await using var context = _contextFactory.CreateContext();
{
var lastActive = DateTime.UtcNow.AddDays(-inactiveDays); var lastActive = DateTime.UtcNow.AddDays(-inactiveDays);
inactiveUsers = await context.Clients inactiveUsers = await context.Clients
.Where(c => c.Level > Permission.Flagged && c.Level <= Permission.Moderator) .Where(c => c.Level > Permission.Flagged && c.Level <= Permission.Moderator)
@ -1435,7 +1503,7 @@ namespace SharedLibraryCore.Commands
.ToListAsync(); .ToListAsync();
inactiveUsers.ForEach(c => c.SetLevel(Permission.User, E.Origin)); inactiveUsers.ForEach(c => c.SetLevel(Permission.User, E.Origin));
await context.SaveChangesAsync(); await context.SaveChangesAsync();
}
E.Origin.Tell(_translationLookup["COMMANDS_PRUNE_SUCCESS"].FormatExt(inactiveUsers.Count)); E.Origin.Tell(_translationLookup["COMMANDS_PRUNE_SUCCESS"].FormatExt(inactiveUsers.Count));
} }
} }

View File

@ -25,40 +25,39 @@ namespace SharedLibraryCore.Commands
}; };
} }
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
if (E.IsTargetingSelf()) if (gameEvent.IsTargetingSelf())
{ {
E.Origin.Tell(_translationLookup["COMMANDS_RUN_AS_SELF"]); gameEvent.Origin.Tell(_translationLookup["COMMANDS_RUN_AS_SELF"]);
return; return;
} }
if (!E.CanPerformActionOnTarget()) if (!gameEvent.CanPerformActionOnTarget())
{ {
E.Origin.Tell(_translationLookup["COMMANDS_RUN_AS_FAIL_PERM"]); gameEvent.Origin.Tell(_translationLookup["COMMANDS_RUN_AS_FAIL_PERM"]);
return; return;
} }
string cmd = $"{Utilities.CommandPrefix}{E.Data}"; var cmd = $"{Utilities.CommandPrefix}{gameEvent.Data}";
var impersonatedCommandEvent = new GameEvent() var impersonatedCommandEvent = new GameEvent()
{ {
Type = GameEvent.EventType.Command, Type = GameEvent.EventType.Command,
Origin = E.Target, Origin = gameEvent.Target,
ImpersonationOrigin = E.Origin, ImpersonationOrigin = gameEvent.Origin,
Message = cmd, Message = cmd,
Data = cmd, Data = cmd,
Owner = E.Owner Owner = gameEvent.Owner,
CorrelationId = gameEvent.CorrelationId
}; };
E.Owner.Manager.AddEvent(impersonatedCommandEvent); gameEvent.Owner.Manager.AddEvent(impersonatedCommandEvent);
var result = await impersonatedCommandEvent.WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken); var result = await impersonatedCommandEvent.WaitAsync(Utilities.DefaultCommandTimeout, gameEvent.Owner.Manager.CancellationToken);
var response = E.Owner.CommandResult.Where(c => c.ClientId == E.Target.ClientId).ToList();
// remove the added command response // remove the added command response
for (int i = 0; i < response.Count; i++) foreach (var output in result.Output)
{ {
E.Origin.Tell(_translationLookup["COMMANDS_RUN_AS_SUCCESS"].FormatExt(response[i].Response)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_RUN_AS_SUCCESS"].FormatExt(output));
E.Owner.CommandResult.Remove(response[i]);
} }
} }
} }

View File

@ -1,6 +1,7 @@
using SharedLibraryCore.Configuration.Attributes; using SharedLibraryCore.Configuration.Attributes;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace SharedLibraryCore.Configuration namespace SharedLibraryCore.Configuration
@ -8,61 +9,79 @@ namespace SharedLibraryCore.Configuration
public class ApplicationConfiguration : IBaseConfiguration public class ApplicationConfiguration : IBaseConfiguration
{ {
[LocalizedDisplayName("SETUP_ENABLE_WEBFRONT")] [LocalizedDisplayName("SETUP_ENABLE_WEBFRONT")]
[ConfigurationLinked("WebfrontBindUrl", "ManualWebfrontUrl", "WebfrontPrimaryColor", "WebfrontSecondaryColor", "WebfrontCustomBranding")] [ConfigurationLinked("WebfrontBindUrl", "ManualWebfrontUrl", "WebfrontPrimaryColor", "WebfrontSecondaryColor",
"WebfrontCustomBranding")]
public bool EnableWebFront { get; set; } public bool EnableWebFront { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_BIND_URL")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_BIND_URL")]
public string WebfrontBindUrl { get; set; } public string WebfrontBindUrl { get; set; }
[ConfigurationOptional] [ConfigurationOptional]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_MANUAL_URL")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_MANUAL_URL")]
public string ManualWebfrontUrl { get; set; } public string ManualWebfrontUrl { get; set; }
[ConfigurationOptional] [ConfigurationOptional]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRIMARY_COLOR")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRIMARY_COLOR")]
public string WebfrontPrimaryColor { get; set; } public string WebfrontPrimaryColor { get; set; }
[ConfigurationOptional] [ConfigurationOptional]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_SECONDARY_COLOR")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_SECONDARY_COLOR")]
public string WebfrontSecondaryColor { get; set; } public string WebfrontSecondaryColor { get; set; }
[ConfigurationOptional] [ConfigurationOptional]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_BRANDING")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_BRANDING")]
public string WebfrontCustomBranding { get; set; } public string WebfrontCustomBranding { get; set; }
[LocalizedDisplayName("SETUP_ENABLE_MULTIOWN")] [LocalizedDisplayName("SETUP_ENABLE_MULTIOWN")]
public bool EnableMultipleOwners { get; set; } public bool EnableMultipleOwners { get; set; }
[LocalizedDisplayName("SETUP_ENABLE_STEPPEDPRIV")] [LocalizedDisplayName("SETUP_ENABLE_STEPPEDPRIV")]
public bool EnableSteppedHierarchy { get; set; } public bool EnableSteppedHierarchy { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_USE_LOCAL_TRANSLATIONS")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_USE_LOCAL_TRANSLATIONS")]
public bool UseLocalTranslations { get; set; } public bool UseLocalTranslations { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_IGNORE_BOTS")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_IGNORE_BOTS")]
public bool IgnoreBots { get; set; } public bool IgnoreBots { get; set; }
[ConfigurationLinked("CustomSayName")] [ConfigurationLinked("CustomSayName")]
[LocalizedDisplayName("SETUP_ENABLE_CUSTOMSAY")] [LocalizedDisplayName("SETUP_ENABLE_CUSTOMSAY")]
public bool EnableCustomSayName { get; set; } public bool EnableCustomSayName { get; set; }
[LocalizedDisplayName("SETUP_SAY_NAME")] [LocalizedDisplayName("SETUP_SAY_NAME")]
public string CustomSayName { get; set; } public string CustomSayName { get; set; }
[LocalizedDisplayName("SETUP_DISPLAY_SOCIAL")] [LocalizedDisplayName("SETUP_DISPLAY_SOCIAL")]
[ConfigurationLinked("SocialLinkAddress", "SocialLinkTitle")] [ConfigurationLinked("SocialLinkAddress", "SocialLinkTitle")]
public bool EnableSocialLink { get; set; } public bool EnableSocialLink { get; set; }
[LocalizedDisplayName("SETUP_SOCIAL_LINK")] [LocalizedDisplayName("SETUP_SOCIAL_LINK")]
public string SocialLinkAddress { get; set; } public string SocialLinkAddress { get; set; }
[LocalizedDisplayName("SETUP_SOCIAL_TITLE")] [LocalizedDisplayName("SETUP_SOCIAL_TITLE")]
public string SocialLinkTitle { get; set; } public string SocialLinkTitle { get; set; }
[LocalizedDisplayName("SETUP_CONTACT_URI")]
public string ContactUri { get; set; }
[LocalizedDisplayName("SETUP_USE_CUSTOMENCODING")] [LocalizedDisplayName("SETUP_USE_CUSTOMENCODING")]
[ConfigurationLinked("CustomParserEncoding")] [ConfigurationLinked("CustomParserEncoding")]
public bool EnableCustomParserEncoding { get; set; } public bool EnableCustomParserEncoding { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_ENCODING")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_ENCODING")]
public string CustomParserEncoding { get; set; } public string CustomParserEncoding { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_ENABLE_WHITELIST")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_ENABLE_WHITELIST")]
[ConfigurationLinked("WebfrontConnectionWhitelist")] [ConfigurationLinked("WebfrontConnectionWhitelist")]
public bool EnableWebfrontConnectionWhitelist { get; set; } public bool EnableWebfrontConnectionWhitelist { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_WHITELIST_LIST")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_WHITELIST_LIST")]
public string[] WebfrontConnectionWhitelist { get; set; } = new string[0]; public string[] WebfrontConnectionWhitelist { get; set; } = new string[0];
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")]
[ConfigurationLinked("CustomLocale")] [ConfigurationLinked("CustomLocale")]
public bool EnableCustomLocale { get; set; } public bool EnableCustomLocale { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")]
public string CustomLocale { get; set; } public string CustomLocale { get; set; }
@ -74,38 +93,65 @@ namespace SharedLibraryCore.Configuration
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_DB_PROVIDER")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_DB_PROVIDER")]
public string DatabaseProvider { get; set; } = "sqlite"; public string DatabaseProvider { get; set; } = "sqlite";
[ConfigurationOptional] [ConfigurationOptional]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_CONNECTION_STRING")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_CONNECTION_STRING")]
public string ConnectionString { get; set; } public string ConnectionString { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_RCON_POLLRATE")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_RCON_POLLRATE")]
public int RConPollRate { get; set; } = 5000; public int RConPollRate { get; set; } = 5000;
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_MAX_TB")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_MAX_TB")]
public TimeSpan MaximumTempBanTime { get; set; } = new TimeSpan(24 * 30, 0, 0); public TimeSpan MaximumTempBanTime { get; set; } = new TimeSpan(24 * 30, 0, 0);
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_ENABLE_COLOR_CODES")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_ENABLE_COLOR_CODES")]
public bool EnableColorCodes { get; set; } public bool EnableColorCodes { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_AUTOMESSAGE_PERIOD")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_AUTOMESSAGE_PERIOD")]
public int AutoMessagePeriod { get; set; } public int AutoMessagePeriod { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_AUTOMESSAGES")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_AUTOMESSAGES")]
public string[] AutoMessages { get; set; } = new string[0]; public string[] AutoMessages { get; set; } = new string[0];
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_GLOBAL_RULES")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_GLOBAL_RULES")]
public string[] GlobalRules { get; set; } = new string[0]; public string[] GlobalRules { get; set; } = new string[0];
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_DISALLOWED_NAMES")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_DISALLOWED_NAMES")]
public string[] DisallowedClientNames { get; set; } = new string[0]; public string[] DisallowedClientNames { get; set; } = new string[0];
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_MAP_CHANGE_DELAY")]
public int MapChangeDelaySeconds { get; set; } = 5;
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_BAN_DURATIONS")]
public TimeSpan[] BanDurations { get; set; } = {
TimeSpan.FromHours(1),
TimeSpan.FromHours(6),
TimeSpan.FromDays(1),
TimeSpan.FromDays(2),
TimeSpan.FromDays(7),
TimeSpan.FromDays(30)
};
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")]
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new Dictionary<string, string>
{{"afk", "Away from keyboard"}, {"ci", "Connection interrupted. Reconnect"}};
[LocalizedDisplayName(("WEBFRONT_CONFIGURATION_ENABLE_PRIVILEGED_USER_PRIVACY"))]
public bool EnablePrivilegedUserPrivacy { get; set; }
[UIHint("ServerConfiguration")] [UIHint("ServerConfiguration")]
public ServerConfiguration[] Servers { get; set; } public ServerConfiguration[] Servers { get; set; }
[ConfigurationIgnore] public string Id { get; set; }
[ConfigurationIgnore] public string SubscriptionId { get; set; }
[ConfigurationIgnore] public MapConfiguration[] Maps { get; set; }
[ConfigurationIgnore] public QuickMessageConfiguration[] QuickMessages { get; set; }
[ConfigurationIgnore] [ConfigurationIgnore]
public string Id { get; set; } public string WebfrontUrl => string.IsNullOrEmpty(ManualWebfrontUrl)
[ConfigurationIgnore] ? WebfrontBindUrl?.Replace("0.0.0.0", "127.0.0.1")
public MapConfiguration[] Maps { get; set; } : ManualWebfrontUrl;
[ConfigurationIgnore]
public QuickMessageConfiguration[] QuickMessages { get; set; } [ConfigurationIgnore] public bool IgnoreServerConnectionLost { get; set; }
[ConfigurationIgnore] [ConfigurationIgnore] public Uri MasterUrl { get; set; } = new Uri("http://api.raidmax.org:5000");
public string WebfrontUrl => string.IsNullOrEmpty(ManualWebfrontUrl) ? WebfrontBindUrl?.Replace("0.0.0.0", "127.0.0.1") : ManualWebfrontUrl;
[ConfigurationIgnore]
public bool IgnoreServerConnectionLost { get; set; }
[ConfigurationIgnore]
public Uri MasterUrl { get; set; } = new Uri("http://api.raidmax.org:5000");
public IBaseConfiguration Generate() public IBaseConfiguration Generate()
{ {

View File

@ -2,31 +2,30 @@
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using SharedLibraryCore.Interfaces;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
namespace SharedLibraryCore.Database namespace SharedLibraryCore.Database
{ {
public class ContextSeed public static class ContextSeed
{ {
private DatabaseContext context; public static async Task Seed(IDatabaseContextFactory contextFactory, CancellationToken token)
public ContextSeed(DatabaseContext ctx)
{ {
context = ctx; await using var context = contextFactory.CreateContext();
} var strategy = context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
public async Task Seed()
{ {
context.Database.Migrate(); await context.Database.MigrateAsync(token);
});
if (context.AliasLinks.Count() == 0) if (!await context.AliasLinks.AnyAsync(token))
{ {
var link = new EFAliasLink(); var link = new EFAliasLink();
context.Clients.Add(new EFClient() context.Clients.Add(new EFClient()
{ {
ClientId = 1,
Active = false, Active = false,
Connections = 0, Connections = 0,
FirstConnection = DateTime.UtcNow, FirstConnection = DateTime.UtcNow,
@ -44,7 +43,7 @@ namespace SharedLibraryCore.Database
}, },
}); });
await context.SaveChangesAsync(); await context.SaveChangesAsync(token);
} }
} }
} }

View File

@ -15,7 +15,7 @@ using System.Threading.Tasks;
namespace SharedLibraryCore.Database namespace SharedLibraryCore.Database
{ {
public class DatabaseContext : DbContext public abstract class DatabaseContext : DbContext
{ {
public DbSet<EFClient> Clients { get; set; } public DbSet<EFClient> Clients { get; set; }
public DbSet<EFAlias> Aliases { get; set; } public DbSet<EFAlias> Aliases { get; set; }
@ -24,88 +24,6 @@ namespace SharedLibraryCore.Database
public DbSet<EFMeta> EFMeta { get; set; } public DbSet<EFMeta> EFMeta { get; set; }
public DbSet<EFChangeHistory> EFChangeHistory { get; set; } public DbSet<EFChangeHistory> EFChangeHistory { get; set; }
static string _ConnectionString;
static string _provider;
private static readonly ILoggerFactory _loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole()
.AddDebug()
.AddFilter((category, level) => true);
});
public DatabaseContext(DbContextOptions<DatabaseContext> opt) : base(opt)
{
}
public DatabaseContext()
{
}
public override void Dispose()
{
}
public DatabaseContext(bool disableTracking) : this()
{
if (disableTracking)
{
this.ChangeTracker.AutoDetectChangesEnabled = false;
this.ChangeTracker.LazyLoadingEnabled = false;
this.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
else
{
this.ChangeTracker.AutoDetectChangesEnabled = true;
this.ChangeTracker.LazyLoadingEnabled = true;
this.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
}
}
public DatabaseContext(string connStr, string provider) : this()
{
_ConnectionString = connStr;
_provider = provider;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//optionsBuilder.UseLoggerFactory(_loggerFactory)
// .EnableSensitiveDataLogging();
if (string.IsNullOrEmpty(_ConnectionString))
{
string currentPath = Utilities.OperatingDirectory;
// allows the application to find the database file
currentPath = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
$"{Path.DirectorySeparatorChar}{currentPath}" :
currentPath;
var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = Path.Join(currentPath, "Database", "Database.db") };
var connectionString = connectionStringBuilder.ToString();
var connection = new SqliteConnection(connectionString);
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlite(connection);
}
}
else
{
switch (_provider)
{
default:
case "mysql":
optionsBuilder.UseMySql(_ConnectionString, _options => _options.EnableRetryOnFailure());
break;
case "postgresql":
optionsBuilder.UseNpgsql(_ConnectionString, _options => _options.EnableRetryOnFailure());
break;
}
}
}
private void SetAuditColumns() private void SetAuditColumns()
{ {
return; return;
@ -129,6 +47,24 @@ namespace SharedLibraryCore.Database
} }
} }
public DatabaseContext()
{
if (!Utilities.IsMigration)
{
throw new InvalidOperationException();
}
}
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{
}
protected DatabaseContext(DbContextOptions options) : base(options)
{
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{ {
SetAuditColumns(); SetAuditColumns();
@ -205,7 +141,18 @@ namespace SharedLibraryCore.Database
pluginDir = Path.Join(Utilities.OperatingDirectory, "..", "..", "..", "..", "BUILD", "Plugins"); pluginDir = Path.Join(Utilities.OperatingDirectory, "..", "..", "..", "..", "BUILD", "Plugins");
} }
IEnumerable<string> directoryFiles = Directory.GetFiles(pluginDir).Where(f => f.EndsWith(".dll")); IEnumerable<string> directoryFiles = Enumerable.Empty<string>();
try
{
directoryFiles = Directory.GetFiles(pluginDir).Where(f => f.EndsWith(".dll"));
}
catch (DirectoryNotFoundException)
{
// this is just an ugly thing for unit testing
directoryFiles = Directory.GetFiles(@"X:\IW4MAdmin\Tests\ApplicationTests\bin\Debug\netcoreapp3.1").Where(f => f.EndsWith("dll"));
}
foreach (string dllPath in directoryFiles) foreach (string dllPath in directoryFiles)
{ {

View File

@ -0,0 +1,31 @@
using System;
using Microsoft.EntityFrameworkCore;
namespace SharedLibraryCore.Database.MigrationContext
{
public class MySqlDatabaseContext : DatabaseContext
{
public MySqlDatabaseContext()
{
if (!Utilities.IsMigration)
{
throw new InvalidOperationException();
}
}
public MySqlDatabaseContext(DbContextOptions options) : base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (Utilities.IsMigration)
{
optionsBuilder.UseMySql("Server=127.0.0.1;Database=IW4MAdmin_Migration;Uid=root;Pwd=password;")
.EnableDetailedErrors(true)
.EnableSensitiveDataLogging(true);
}
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using Microsoft.EntityFrameworkCore;
namespace SharedLibraryCore.Database.MigrationContext
{
public class PostgresqlDatabaseContext : DatabaseContext
{
public PostgresqlDatabaseContext()
{
if (!Utilities.IsMigration)
{
throw new InvalidOperationException();
}
}
public PostgresqlDatabaseContext(DbContextOptions options) : base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (Utilities.IsMigration)
{
optionsBuilder.UseNpgsql(
"Host=127.0.0.1;Database=IW4MAdmin_Migration;Username=postgres;Password=password;")
.EnableDetailedErrors(true)
.EnableSensitiveDataLogging(true);
}
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using Microsoft.EntityFrameworkCore;
namespace SharedLibraryCore.Database.MigrationContext
{
public class SqliteDatabaseContext : DatabaseContext
{
public SqliteDatabaseContext()
{
if (!Utilities.IsMigration)
{
throw new InvalidOperationException();
}
}
public SqliteDatabaseContext(DbContextOptions options) : base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (Utilities.IsMigration)
{
optionsBuilder.UseSqlite("Data Source=IW4MAdmin_Migration.db")
.EnableDetailedErrors(true)
.EnableSensitiveDataLogging(true);
}
}
}
}

View File

@ -28,6 +28,11 @@ namespace SharedLibraryCore.Dtos.Meta.Responses
/// </summary> /// </summary>
public bool IsQuickMessage { get; set; } public bool IsQuickMessage { get; set; }
/// <summary>
/// indicates if the message was sent ingame
/// </summary>
public bool SentIngame { get; set; }
public string HiddenMessage => string.Concat(Enumerable.Repeat('●', Message.Length)); public string HiddenMessage => string.Concat(Enumerable.Repeat('●', Message.Length));
} }
} }

View File

@ -1,8 +1,11 @@
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events; using SharedLibraryCore.Events;
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Serilog.Context;
namespace SharedLibraryCore namespace SharedLibraryCore
{ {
@ -233,7 +236,7 @@ namespace SharedLibraryCore
/// <summary> /// <summary>
/// Specifies the game time offset as printed in the log /// Specifies the game time offset as printed in the log
/// </summary> /// </summary>
public int? GameTime { get; set; } public long? GameTime { get; set; }
public EFClient Origin; public EFClient Origin;
public EFClient Target; public EFClient Target;
public EFClient ImpersonationOrigin { get; set; } public EFClient ImpersonationOrigin { get; set; }
@ -245,6 +248,8 @@ namespace SharedLibraryCore
public long Id { get; private set; } public long Id { get; private set; }
public EventFailReason FailReason { get; set; } public EventFailReason FailReason { get; set; }
public bool Failed => FailReason != EventFailReason.None; public bool Failed => FailReason != EventFailReason.None;
public Guid CorrelationId { get; set; } = Guid.NewGuid();
public List<string> Output { get; set; } = new List<string>();
/// <summary> /// <summary>
/// Indicates if the event should block until it is complete /// Indicates if the event should block until it is complete
@ -254,9 +259,6 @@ namespace SharedLibraryCore
public void Complete() public void Complete()
{ {
_eventFinishedWaiter.Set(); _eventFinishedWaiter.Set();
#if DEBUG
Owner?.Logger.WriteDebug($"Completed internal for event {Id}");
#endif
} }
/// <summary> /// <summary>
@ -266,11 +268,7 @@ namespace SharedLibraryCore
public async Task<GameEvent> WaitAsync(TimeSpan timeSpan, CancellationToken token) public async Task<GameEvent> WaitAsync(TimeSpan timeSpan, CancellationToken token)
{ {
bool processed = false; bool processed = false;
Utilities.DefaultLogger.LogDebug("Begin wait for event {Id}", Id);
#if DEBUG
Owner?.Logger.WriteDebug($"Begin wait for event {Id}");
#endif
try try
{ {
processed = await Task.Run(() => _eventFinishedWaiter.WaitOne(timeSpan), token); processed = await Task.Run(() => _eventFinishedWaiter.WaitOne(timeSpan), token);
@ -283,13 +281,11 @@ namespace SharedLibraryCore
if (!processed) if (!processed)
{ {
Owner?.Logger.WriteError("Waiting for event to complete timed out"); using(LogContext.PushProperty("Server", Owner?.ToString()))
Owner?.Logger.WriteDebug($"{Id}, {Type}, {Data}, {Extra}, {FailReason.ToString()}, {Message}, {Origin}, {Target}"); {
#if DEBUG Utilities.DefaultLogger.LogError("Waiting for event to complete timed out {@eventData}", new { Event = this, Message, Origin = Origin?.ToString(), Target = Target?.ToString()});
//throw new Exception(); }
#endif
} }
// this lets us know if the the action timed out // this lets us know if the the action timed out
FailReason = FailReason == EventFailReason.None && !processed ? EventFailReason.Timeout : FailReason; FailReason = FailReason == EventFailReason.None && !processed ? EventFailReason.Timeout : FailReason;

View File

@ -0,0 +1,11 @@
using System;
namespace SharedLibraryCore.Exceptions
{
public class RConException : Exception
{
public RConException(string message) : base(message)
{
}
}
}

View File

@ -0,0 +1,16 @@
using SharedLibraryCore.Database.Models;
namespace SharedLibraryCore.Interfaces
{
public interface IClientNoticeMessageFormatter
{
/// <summary>
/// builds a game formatted notice message
/// </summary>
/// <param name="currentPenalty">current penalty the message is for</param>
/// <param name="originalPenalty">previous penalty the current penalty relates to</param>
/// <param name="config">RCon parser config</param>
/// <returns></returns>
string BuildFormattedMessage(IRConParserConfiguration config, EFPenalty currentPenalty, EFPenalty originalPenalty = null);
}
}

View File

@ -0,0 +1,18 @@
using System.Threading.Tasks;
using SharedLibraryCore.Database.Models;
namespace SharedLibraryCore.Interfaces
{
public interface IGameServer
{
/// <summary>
/// kicks target on behalf of origin for given reason
/// </summary>
/// <param name="reason">reason client is being kicked</param>
/// <param name="target">client to kick</param>
/// <param name="origin">source of kick action</param>
/// <param name="previousPenalty">previous penalty the kick is occuring for (if applicable)</param>
/// <returns></returns>
public Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null);
}
}

View File

@ -1,11 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SharedLibraryCore.Interfaces namespace SharedLibraryCore.Interfaces
{ {
[Obsolete]
public interface ILogger public interface ILogger
{ {
void WriteVerbose(string msg); void WriteVerbose(string msg);

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