Compare commits

...

28 Commits

Author SHA1 Message Date
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
136 changed files with 5175 additions and 1828 deletions

1
.gitignore vendored
View File

@ -244,3 +244,4 @@ 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/*

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>
@ -59,6 +59,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; }
@ -54,7 +57,6 @@ namespace IW4MAdmin.Application
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;
@ -67,30 +69,34 @@ namespace IW4MAdmin.Application
private readonly IEventHandler _eventHandler; private readonly IEventHandler _eventHandler;
private readonly IScriptCommandFactory _scriptCommandFactory; private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IMetaRegistration _metaRegistration; private readonly IMetaRegistration _metaRegistration;
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
private readonly IServiceProvider _serviceProvider;
private readonly ChangeHistoryService _changeHistoryService;
private readonly ApplicationConfiguration _appConfig;
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) IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig)
{ {
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(); AliasSvc = new AliasService();
PenaltySvc = new 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;
@ -100,6 +106,10 @@ namespace IW4MAdmin.Application
_eventHandler = eventHandler; _eventHandler = eventHandler;
_scriptCommandFactory = scriptCommandFactory; _scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration; _metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver;
_serviceProvider = serviceProvider;
_changeHistoryService = changeHistoryService;
_appConfig = appConfig;
Plugins = plugins; Plugins = plugins;
} }
@ -107,10 +117,6 @@ namespace IW4MAdmin.Application
public async Task ExecuteEvent(GameEvent newEvent) public async Task ExecuteEvent(GameEvent newEvent)
{ {
#if DEBUG == true
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)
{ {
@ -122,22 +128,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
@ -150,31 +151,35 @@ 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:
// 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()
@ -224,17 +229,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
@ -243,12 +245,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);
@ -277,18 +274,18 @@ namespace IW4MAdmin.Application
{ {
if (plugin is ScriptPlugin scriptPlugin) if (plugin is ScriptPlugin scriptPlugin)
{ {
await scriptPlugin.Initialize(this, _scriptCommandFactory); await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
scriptPlugin.Watcher.Changed += async (sender, e) => scriptPlugin.Watcher.Changed += async (sender, e) =>
{ {
try try
{ {
await scriptPlugin.Initialize(this, _scriptCommandFactory); await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
} }
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);
} }
}; };
} }
@ -301,32 +298,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
{ {
@ -341,30 +335,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)
{ {
@ -375,7 +368,7 @@ namespace IW4MAdmin.Application
}; };
} }
foreach (var serverConfig in config.Servers) foreach (var serverConfig in _appConfig.Servers)
{ {
Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig); Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
@ -397,13 +390,13 @@ 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
@ -412,6 +405,7 @@ namespace IW4MAdmin.Application
GetApplicationSettings().Configuration()?.DatabaseProvider)) GetApplicationSettings().Configuration()?.DatabaseProvider))
{ {
await new ContextSeed(db).Seed(); await new ContextSeed(db).Seed();
DatabaseHousekeeping.RemoveOldRatings(db);
} }
#endregion #endregion
@ -438,8 +432,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)
{ {
@ -449,7 +443,8 @@ namespace IW4MAdmin.Application
Name = cmd.Name, Name = cmd.Name,
Alias = cmd.Alias, Alias = cmd.Alias,
MinimumPermission = cmd.Permission, MinimumPermission = cmd.Permission,
AllowImpersonation = cmd.AllowImpersonation AllowImpersonation = cmd.AllowImpersonation,
SupportedGames = cmd.SupportedGames
}); });
} }
@ -469,6 +464,7 @@ namespace IW4MAdmin.Application
} }
#endregion #endregion
Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
await InitializeServers(); await InitializeServers();
} }
@ -484,13 +480,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;
await ServerInstance.Initialize(); using (LogContext.PushProperty("Server", ServerInstance.ToString()))
{
_logger.LogInformation("Beginning server communication initialization");
await ServerInstance.Initialize();
_servers.Add(ServerInstance); _servers.Add(ServerInstance);
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname.StripColors()));
_logger.LogInformation("Finishing initialization and now monitoring [{server}]", ServerInstance.Hostname, ServerInstance.ToString());
}
Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname));
// add the start event for this server // add the start event for this server
var e = new GameEvent() var e = new GameEvent()
{ {
Type = GameEvent.EventType.Start, Type = GameEvent.EventType.Start,
@ -504,13 +504,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;
} }
} }
@ -545,20 +543,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()
@ -604,7 +592,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,24 +4,13 @@ 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"
md "%TargetDir%Plugins" md "%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
{ {
@ -255,6 +257,7 @@ namespace IW4MAdmin.Application.EventParsers
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()), ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting, State = EFClient.ClientState.Connecting,
}, },
Extra = originIdString,
RequiredEntity = GameEvent.EventRequiredEntity.None, RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true, IsBlocking = true,
GameTime = gameTime, GameTime = gameTime,
@ -347,7 +350,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,41 @@
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
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);
if (Utilities.IsDevelopment)
{
loggerConfig = loggerConfig.WriteTo.Console(
outputTemplate:"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.MinimumLevel.Debug();
}
_defaultLogger = loggerConfig.CreateLogger();
}
services.AddLogging(builder => builder.AddSerilog(_defaultLogger, dispose: true));
return services;
}
}
}

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, 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, 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);
} }
} }
} }

File diff suppressed because it is too large Load Diff

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");
} }
} }
@ -60,13 +68,11 @@ namespace IW4MAdmin.Application.Localization
{ {
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
{ {
@ -76,29 +79,32 @@ namespace IW4MAdmin.Application
{ {
restart: restart:
ITranslationLookup translationLookup = null; ITranslationLookup translationLookup = null;
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
Utilities.DefaultLogger = logger;
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); var 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 +137,14 @@ namespace IW4MAdmin.Application
try try
{ {
ApplicationTask = RunApplicationTasksAsync(); ApplicationTask = RunApplicationTasksAsync(logger);
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,7 +160,7 @@ 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)
{ {
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ? var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ?
WebfrontCore.Program.Init(ServerManager, serviceProvider, ServerManager.CancellationToken) : WebfrontCore.Program.Init(ServerManager, serviceProvider, ServerManager.CancellationToken) :
@ -161,7 +168,7 @@ namespace IW4MAdmin.Application
// 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 +178,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 +190,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,67 +230,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<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"))
{
serviceCollection.AddSingleton<IEventHandler, SerialGameEventHandler>();
}
else
{
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
}
var pluginImporter = pluginServiceProvider.GetRequiredService<IPluginImporter>();
// we need to register the rest client with regular collection
serviceCollection.AddSingleton(masterApi);
// 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);
} }
@ -289,14 +260,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);
} }
@ -320,5 +291,87 @@ 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<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);
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;

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;

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;

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,20 @@
using System;
using System.Linq;
using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore.Database;
namespace IW4MAdmin.Application.Migration
{
public static class DatabaseHousekeeping
{
private static DateTime _cutoffDate = DateTime.UtcNow.AddMonths(-6);
public static void RemoveOldRatings(DatabaseContext context)
{
var dbSet = context.Set<EFRating>();
var itemsToDelete = dbSet.Where(rating => rating.When <= _cutoffDate);
dbSet.RemoveRange(itemsToDelete);
context.SaveChanges();
}
}
}

View File

@ -0,0 +1,101 @@
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 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(Environment.NewLine);
// build the reason
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense);
foreach (var splitReason in SplitOverMaxLength(reason, config.NoticeMaxCharactersPerLine))
{
builder.Append(splitReason);
builder.Append(Environment.NewLine);
}
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);
foreach (var splitReason in SplitOverMaxLength(timeRemaining, config.NoticeMaxCharactersPerLine))
{
builder.Append(splitReason);
builder.Append(Environment.NewLine);
}
}
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>>();

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,28 +16,38 @@ 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, 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;
RequiresTarget = isTargetRequired;
Permission = permission; Permission = permission;
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()
{ {
@ -61,7 +67,7 @@ namespace IW4MAdmin.Application.Misc
_onProcessing.Dispose(); _onProcessing.Dispose();
} }
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory) public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory, IScriptPluginServiceResolver serviceResolver)
{ {
await _onProcessing.WaitAsync(); await _onProcessing.WaitAsync();
@ -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);
} }
@ -114,6 +120,7 @@ namespace IW4MAdmin.Application.Misc
_scriptEngine.Execute(script); _scriptEngine.Execute(script);
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization); _scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject(); dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
Author = pluginObject.author; Author = pluginObject.author;
@ -128,7 +135,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);
} }
@ -164,9 +171,22 @@ namespace IW4MAdmin.Application.Misc
successfullyLoaded = true; successfullyLoaded = true;
} }
catch catch (JavaScriptException ex)
{ {
throw; _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 (Exception ex)
{
_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
@ -191,10 +211,29 @@ namespace IW4MAdmin.Application.Misc
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(S)); _scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(S));
_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
@ -209,7 +248,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());
} }
@ -246,6 +285,7 @@ namespace IW4MAdmin.Application.Misc
string alias = dynamicCommand.alias; string alias = dynamicCommand.alias;
string description = dynamicCommand.description; string description = dynamicCommand.description;
string permission = dynamicCommand.permission; string permission = dynamicCommand.permission;
bool targetRequired = false;
List<(string, bool)> args = new List<(string, bool)>(); List<(string, bool)> args = new List<(string, bool)>();
dynamic arguments = null; dynamic arguments = null;
@ -260,6 +300,16 @@ namespace IW4MAdmin.Application.Misc
// arguments are optional // arguments are optional
} }
try
{
targetRequired = dynamicCommand.targetRequired;
}
catch (RuntimeBinderException)
{
// arguments are optional
}
if (arguments != null) if (arguments != null)
{ {
foreach (var arg in dynamicCommand.arguments) foreach (var arg in dynamicCommand.arguments)
@ -284,7 +334,7 @@ namespace IW4MAdmin.Application.Misc
} }
} }
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, args, execute)); commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, targetRequired, args, execute));
} }
return commandList; return commandList;

View File

@ -0,0 +1,48 @@
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IScriptPluginServiceResolver
/// </summary>
public class ScriptPluginServiceResolver : IScriptPluginServiceResolver
{
private readonly IServiceProvider _serviceProvider;
public ScriptPluginServiceResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public object ResolveService(string serviceName)
{
var serviceType = DetermineRootType(serviceName);
return _serviceProvider.GetService(serviceType);
}
public object ResolveService(string serviceName, string[] genericParameters)
{
var serviceType = DetermineRootType(serviceName, genericParameters.Length);
var genericTypes = genericParameters.Select(_genericTypeParam => DetermineRootType(_genericTypeParam));
var resolvedServiceType = serviceType.MakeGenericType(genericTypes.ToArray());
return _serviceProvider.GetService(resolvedServiceType);
}
private Type DetermineRootType(string serviceName, int genericParamCount = 0)
{
var typeCollection = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(t => t.GetTypes());
string generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
var serviceType = typeCollection.FirstOrDefault(_type => _type.Name.ToLower() == generatedName);
if (serviceType == null)
{
throw new InvalidOperationException($"No object type '{serviceName}' defined in loaded assemblies");
}
return serviceType;
}
}
}

View File

@ -11,6 +11,9 @@ using System.Net.Sockets;
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 ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.RCon namespace IW4MAdmin.Application.RCon
{ {
@ -23,11 +26,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 +39,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 +54,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 +68,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,15 +114,31 @@ 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 = true,
@ -133,16 +151,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 +171,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 +201,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 +229,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 +270,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 +323,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 +349,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,9 +398,7 @@ 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)
@ -330,9 +421,7 @@ namespace IW4MAdmin.Application.RCon
if (!sock.ReceiveAsync(state.ReceiveEventArgs)) if (!sock.ReceiveAsync(state.ReceiveEventArgs))
{ {
#if DEBUG == true _log.LogDebug("Read {bytesTransferred} synchronous bytes from {endpoint}", state.ReceiveEventArgs.BytesTransferred, e.RemoteEndPoint);
_log.WriteDebug($"Read {state.ReceiveEventArgs.BytesTransferred} synchronous bytes from {e.RemoteEndPoint.ToString()}");
#endif
// 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(); ActiveQueries[this.Endpoint].OnReceivedData.Set();
@ -354,9 +443,7 @@ namespace IW4MAdmin.Application.RCon
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]]);
@ -203,10 +224,11 @@ 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;
try try
{ {
string networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]]; networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkId = networkIdString.IsBotGuid() ? networkId = networkIdString.IsBotGuid() ?
name.GenerateGuidFromString() : name.GenerateGuidFromString() :
@ -234,6 +256,8 @@ namespace IW4MAdmin.Application.RconParsers
State = EFClient.ClientState.Connecting State = EFClient.ClientState.Connecting
}; };
client.SetAdditionalProperty("BotGuid", networkIdString);
StatusPlayers.Add(client); StatusPlayers.Add(client);
} }
} }
@ -260,5 +284,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

@ -22,6 +22,8 @@ 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 DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory) public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
{ {

View File

@ -1,13 +1,11 @@
name: '$(Date:yyyy.MM.dd)$(Rev:.r)'
trigger: trigger:
batch: true batch: true
branches: branches:
include: include:
- releases/* - release/pre
- 2.4-pr - master
paths:
exclude:
- azure-pipelines.yml
- Master/*
pr: none pr: none
@ -17,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)'
@ -135,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
@ -149,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
@ -184,7 +207,8 @@ 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)'
publishLocation: 'pipeline' publishLocation: 'pipeline'

View File

@ -8,10 +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
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
@ -36,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

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.11.18.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.11.18.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -74,14 +74,14 @@ namespace LiveRadar.Web.Controllers
[Route("Radar/Update")] [Route("Radar/Update")]
public IActionResult Update(string payload) public IActionResult Update(string payload)
{ {
var radarUpdate = RadarEvent.Parse(payload); /*var radarUpdate = RadarEvent.Parse(payload);
var client = _manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid); var client = _manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
if (client != null) if (client != null)
{ {
radarUpdate.Name = client.Name.StripColors(); radarUpdate.Name = client.Name.StripColors();
client.SetAdditionalProperty("LiveRadar", radarUpdate); client.SetAdditionalProperty("LiveRadar", radarUpdate);
} }*/
return Ok(); return Ok();
} }

View File

@ -0,0 +1,33 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using EventGeneratorCallback = System.ValueTuple<string, string,
System.Func<string, SharedLibraryCore.Interfaces.IEventParserConfiguration,
SharedLibraryCore.GameEvent,
SharedLibraryCore.GameEvent>>;
namespace LiveRadar.Events
{
public class Script : IRegisterEvent
{
private const string EVENT_LIVERADAR = "LiveRadar";
private EventGeneratorCallback LiveRadar()
{
return (EVENT_LIVERADAR, EVENT_LIVERADAR, (string eventLine, IEventParserConfiguration config, GameEvent autoEvent) =>
{
string[] lineSplit = eventLine.Split(";");
autoEvent.Type = GameEvent.EventType.Other;
autoEvent.Subtype = EVENT_LIVERADAR;
autoEvent.Origin = new EFClient() { NetworkId = 0 };
autoEvent.Extra = lineSplit[1]; // guid
return autoEvent;
}
);
}
public IEnumerable<EventGeneratorCallback> Events => new[] { LiveRadar() };
}
}

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.11.18.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,10 +1,12 @@
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.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
{ {
@ -17,12 +19,16 @@ namespace LiveRadar
public string Author => "RaidMax"; public string Author => "RaidMax";
private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler; private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler;
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>();
_logger = logger;
} }
public Task OnEventAsync(GameEvent E, Server S) public Task OnEventAsync(GameEvent E, Server S)
@ -41,28 +47,44 @@ namespace LiveRadar
} }
} }
if (E.Type == GameEvent.EventType.Unknown) if (E.Type == GameEvent.EventType.PreConnect && E.Origin.IsBot)
{ {
if (E.Data?.StartsWith("LiveRadar") ?? false) string botKey = $"BotGuid_{E.Extra}";
lock (lockObject)
{ {
try if (!_botGuidLookups.ContainsKey(botKey))
{ {
var radarUpdate = RadarEvent.Parse(E.Data); _botGuidLookups.Add(botKey, E.Origin.NetworkId);
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid); }
}
}
if (client != null) if (E.Type == GameEvent.EventType.Other && E.Subtype == "LiveRadar")
{ {
radarUpdate.Name = client.Name.StripColors(); try
client.SetAdditionalProperty("LiveRadar", radarUpdate); {
} string botKey = $"BotGuid_{E.Extra}";
long generatedBotGuid;
lock (lockObject)
{
generatedBotGuid = _botGuidLookups.ContainsKey(botKey) ? _botGuidLookups[botKey] : 0;
} }
catch (Exception e) var radarUpdate = RadarEvent.Parse(E.Data, generatedBotGuid);
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
if (client != null)
{ {
S.Logger.WriteWarning($"Could not parse live radar output: {e.Data}"); radarUpdate.Name = client.Name.StripColors();
S.Logger.WriteDebug(e.GetExceptionInfo()); client.SetAdditionalProperty("LiveRadar", radarUpdate);
} }
} }
catch (Exception e)
{
_logger.LogError(e, "Could not parse live radar output: {data}", e.Data);
}
} }
return Task.CompletedTask; return Task.CompletedTask;

View File

@ -1,9 +1,7 @@
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
namespace LiveRadar namespace LiveRadar
{ {
@ -39,13 +37,13 @@ namespace LiveRadar
return false; return false;
} }
public static RadarEvent Parse(string input) public static RadarEvent Parse(string input, long generatedBotGuid)
{ {
var items = input.Split(';').Skip(1).ToList(); var items = input.Split(';').Skip(1).ToList();
var parsedEvent = new RadarEvent() var parsedEvent = new RadarEvent()
{ {
Guid = items[0].ConvertGuidToLong(System.Globalization.NumberStyles.HexNumber), Guid = generatedBotGuid,
Location = Vector3.Parse(items[1]), Location = Vector3.Parse(items[1]),
ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(), ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(),
Team = items[3], Team = items[3],

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.11.18.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.11.18.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

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.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

@ -7,6 +7,8 @@ let commands = [{
alias: "pp", alias: "pp",
// required // required
permission: "User", permission: "User",
// optional (defaults to false)
targetRequired: false,
// optional // optional
arguments: [{ arguments: [{
name: "times to ping", name: "times to ping",
@ -44,6 +46,8 @@ let plugin = {
}, },
onLoadAsync: function (manager) { onLoadAsync: function (manager) {
this.logger = _serviceResolver.ResolveService("ILogger");
this.logger.WriteDebug("sample plugin loaded");
}, },
onUnloadAsync: function () { onUnloadAsync: function () {

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
{ {
@ -20,7 +20,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Offset, Offset,
Strain, Strain,
Recoil, Recoil,
Snap Snap,
Button
}; };
public ChangeTracking<EFACSnapshot> Tracker { get; private set; } public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
@ -38,11 +39,12 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
ILogger Log; ILogger Log;
Strain Strain; Strain Strain;
readonly DateTime ConnectionTime = DateTime.UtcNow; readonly DateTime ConnectionTime = DateTime.UtcNow;
private double sessionAverageRecoilAmount; private double mapAverageRecoilAmount;
private double sessionAverageSnapAmount; private double sessionAverageSnapAmount;
private int sessionSnapHits; private int sessionSnapHits;
private EFClientKill lastHit; private EFClientKill lastHit;
private int validRecoilHitCount; private int validRecoilHitCount;
private int validButtonHitCount;
private class HitInfo private class HitInfo
{ {
@ -221,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,
@ -235,18 +232,14 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Location = hitLoc.Location Location = hitLoc.Location
}); });
} }
#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)
@ -282,18 +275,30 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
#region RECOIL #region RECOIL
float hitRecoilAverage = 0; float hitRecoilAverage = 0;
if (!Plugin.Config.Configuration().RecoilessWeapons.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex))) bool shouldIgnoreDetection = false;
try
{
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Recoil]
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
}
catch (KeyNotFoundException)
{
}
if (!shouldIgnoreDetection)
{ {
validRecoilHitCount++; validRecoilHitCount++;
hitRecoilAverage = (hit.AnglesList.Sum(_angle => _angle.Z) + hit.ViewAngles.Z) / (hit.AnglesList.Count + 1); hitRecoilAverage = (hit.AnglesList.Sum(_angle => _angle.Z) + hit.ViewAngles.Z) / (hit.AnglesList.Count + 1);
sessionAverageRecoilAmount = (sessionAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount; mapAverageRecoilAmount = (mapAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount;
if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && sessionAverageRecoilAmount == 0) if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && mapAverageRecoilAmount == 0)
{ {
results.Add(new DetectionPenaltyResult() results.Add(new DetectionPenaltyResult()
{ {
ClientPenalty = EFPenalty.PenaltyType.Ban, ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = sessionAverageRecoilAmount, Value = mapAverageRecoilAmount,
HitCount = HitCount, HitCount = HitCount,
Type = DetectionType.Recoil Type = DetectionType.Recoil
}); });
@ -301,6 +306,37 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
} }
#endregion #endregion
#region BUTTON
try
{
shouldIgnoreDetection = false;
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Button]
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
}
catch (KeyNotFoundException)
{
}
if (!shouldIgnoreDetection)
{
validButtonHitCount++;
double lastDiff = hit.TimeOffset - hit.TimeSinceLastAttack;
if (validButtonHitCount > 0 && lastDiff <= 0)
{
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = lastDiff,
HitCount = HitCount,
Type = DetectionType.Button
});
}
}
#endregion
#region SESSION_RATIOS #region SESSION_RATIOS
if (Kills >= Thresholds.LowSampleMinKills) if (Kills >= Thresholds.LowSampleMinKills)
{ {
@ -384,7 +420,19 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
#region CHEST_ABDOMEN_RATIO_SESSION #region CHEST_ABDOMEN_RATIO_SESSION
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count; int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count;
if (chestHits >= Thresholds.MediumSampleMinKills) try
{
shouldIgnoreDetection = false; // reset previous value
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Chest]
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
}
catch (KeyNotFoundException)
{
}
if (chestHits >= Thresholds.MediumSampleMinKills && !shouldIgnoreDetection)
{ {
double marginOfError = Thresholds.GetMarginOfError(chestHits); double marginOfError = Thresholds.GetMarginOfError(chestHits);
double lerpAmount = Math.Min(1.0, (chestHits - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills)); double lerpAmount = Math.Min(1.0, (chestHits - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills));
@ -466,5 +514,11 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
return results; return results;
} }
public void OnMapChange()
{
mapAverageRecoilAmount = 0;
validRecoilHitCount = 0;
}
} }
} }

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

@ -0,0 +1,24 @@
using System.Collections.Generic;
using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
using static SharedLibraryCore.Server;
namespace Stats.Config
{
public class AnticheatConfiguration
{
public bool Enable { get; set; }
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; } = new Dictionary<long, DetectionType[]>();
public IList<long> IgnoredClientIds { get; set; } = new List<long>();
public IDictionary<Game, IDictionary<DetectionType, string[]>> IgnoredDetectionSpecification{ get; set; } = new Dictionary<Game, IDictionary<DetectionType, string[]>>
{
{
Game.IW4, new Dictionary<DetectionType, string[]>
{
{ DetectionType.Chest, new[] { "m21.+" } },
{ DetectionType.Recoil, new[] { "ranger.*_mp", "model1887.*_mp", ".+shotgun.*_mp", "turret_minigun_mp" } },
{ DetectionType.Button, new[] { ".*akimbo.*" } }
}
}
};
}
}

View File

@ -1,6 +1,7 @@
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using Stats.Config; using Stats.Config;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using static IW4MAdmin.Plugins.Stats.Cheat.Detection; using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
@ -8,21 +9,41 @@ namespace IW4MAdmin.Plugins.Stats.Config
{ {
public class StatsConfiguration : IBaseConfiguration public class StatsConfiguration : IBaseConfiguration
{ {
public bool EnableAntiCheat { get; set; } [Obsolete]
public bool? EnableAntiCheat { get; set; }
public List<StreakMessageConfiguration> KillstreakMessages { get; set; } public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
public List<StreakMessageConfiguration> DeathstreakMessages { get; set; } public List<StreakMessageConfiguration> DeathstreakMessages { get; set; }
public List<string> RecoilessWeapons { get; set; }
public int TopPlayersMinPlayTime { get; set; } public int TopPlayersMinPlayTime { get; set; }
public bool StoreClientKills { get; set; } public bool StoreClientKills { get; set; }
public int MostKillsMaxInactivityDays { get; set; } = 30; public int MostKillsMaxInactivityDays { get; set; } = 30;
public int MostKillsClientLimit { get; set; } = 5; public int MostKillsClientLimit { get; set; } = 5;
public IDictionary<DetectionType, DistributionConfiguration> DetectionDistributions { get; set; } [Obsolete]
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; } public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; }
public AnticheatConfiguration AnticheatConfiguration { get; set; } = new AnticheatConfiguration();
#pragma warning disable CS0612 // Type or member is obsolete
public void ApplyMigration()
{
if (ServerDetectionTypes != null)
{
AnticheatConfiguration.ServerDetectionTypes = ServerDetectionTypes;
}
ServerDetectionTypes = null;
if (EnableAntiCheat != null)
{
AnticheatConfiguration.Enable = EnableAntiCheat.Value;
}
EnableAntiCheat = null;
}
#pragma warning restore CS0612 // Type or member is obsolete
public string Name() => "StatsPluginSettings"; public string Name() => "StatsPluginSettings";
public IBaseConfiguration Generate() public IBaseConfiguration Generate()
{ {
EnableAntiCheat = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]); AnticheatConfiguration.Enable = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]);
KillstreakMessages = new List<StreakMessageConfiguration>() KillstreakMessages = new List<StreakMessageConfiguration>()
{ {
new StreakMessageConfiguration(){ new StreakMessageConfiguration(){
@ -57,16 +78,9 @@ namespace IW4MAdmin.Plugins.Stats.Config
}, },
}; };
RecoilessWeapons = new List<string>()
{
"ranger.*_mp",
"model1887.*_mp",
".+shotgun.*_mp"
};
TopPlayersMinPlayTime = 3600 * 3; TopPlayersMinPlayTime = 3600 * 3;
StoreClientKills = false; StoreClientKills = false;
return this; return this;
} }
} }

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,15 +30,21 @@ 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))
@ -265,8 +272,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 +283,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;
} }
@ -311,12 +325,13 @@ 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, {
HitCount = 0, Active = true,
Location = hl HitCount = 0,
}).ToList() Location = hl
}).ToList()
}; };
// insert if they've not been added // insert if they've not been added
@ -329,7 +344,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,7 +376,7 @@ 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;
@ -368,8 +384,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
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 +405,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,7 +430,7 @@ 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());
} }
} }
@ -452,10 +475,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;
} }
@ -481,7 +503,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
IsKill = !isDamage, IsKill = !isDamage,
AnglesList = snapshotAngles, AnglesList = snapshotAngles,
IsAlive = isAlive == "1", IsAlive = isAlive == "1",
TimeSinceLastAttack = long.Parse(lastAttackTime) TimeSinceLastAttack = long.Parse(lastAttackTime),
GameName = attacker.CurrentServer.GameName
}; };
if (hit.HitLoc == IW4Info.HitLocation.shield) if (hit.HitLoc == IW4Info.HitLocation.shield)
@ -526,8 +549,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
@ -539,7 +561,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
} }
if (Plugin.Config.Configuration().EnableAntiCheat && !attacker.IsBot && attacker.ClientId != victim.ClientId) if (Plugin.Config.Configuration().AnticheatConfiguration.Enable && !attacker.IsBot && attacker.ClientId != victim.ClientId)
{ {
clientDetection.TrackedHits.Add(hit); clientDetection.TrackedHits.Add(hit);
@ -555,10 +577,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (oldestHit.IsAlive) if (oldestHit.IsAlive)
{ {
var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker.CurrentServer.EndPoint); var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker);
#if !DEBUG
await ApplyPenalty(result, attacker); if (!Utilities.IsDevelopment)
#endif {
await ApplyPenalty(result, attacker);
}
if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any) if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any)
{ {
@ -580,9 +604,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
@ -594,10 +616,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
} }
private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> results, long serverId) private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> results, EFClient client)
{ {
// allow disabling of certain detection types // allow disabling of certain detection types
results = results.Where(_result => ShouldUseDetection(serverId, _result.Type)); results = results.Where(_result => ShouldUseDetection(client.CurrentServer, _result.Type, client.ClientId));
return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ?? return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ??
results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ?? results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
new DetectionPenaltyResult() new DetectionPenaltyResult()
@ -617,21 +639,31 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
} }
private bool ShouldUseDetection(long serverId, DetectionType detectionType) private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
{ {
var detectionTypes = Plugin.Config.Configuration().ServerDetectionTypes; var detectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes;
var ignoredClients = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredClientIds;
if (detectionTypes == null) if (ignoredClients.Contains(clientId))
{ {
return true; return false;
} }
if (!detectionTypes.ContainsKey(serverId))
try
{ {
return true; if (!detectionTypes[server.EndPoint].Contains(detectionType))
{
return false;
}
} }
return detectionTypes[serverId].Contains(detectionType); catch (KeyNotFoundException)
{
}
return true;
} }
async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker) async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker)
@ -701,9 +733,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;
@ -741,14 +770,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;
} }
@ -768,8 +797,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
@ -1086,8 +1114,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;
} }
@ -1097,8 +1124,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;
} }
@ -1120,7 +1146,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
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()
{ {
@ -1139,10 +1165,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public void ResetKillstreaks(Server sv) public void ResetKillstreaks(Server sv)
{ {
foreach (var stat in sv.GetClientsAsList() foreach (var session in sv.GetClientsAsList()
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))) .Select(_client => new
{
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
}))
{ {
stat?.StartNewSession(); session.stat?.StartNewSession();
session.detection?.OnMapChange();
} }
} }
@ -1157,7 +1188,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)
@ -1165,18 +1196,17 @@ 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,
{ Message = message,
ClientId = clientId, ServerId = serverId,
Message = message, TimeSent = DateTime.UtcNow,
ServerId = serverId, SentIngame = sentIngame
TimeSent = DateTime.UtcNow });
});
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
}
} }
public async Task Sync(Server sv) public async Task Sync(Server sv)
@ -1207,8 +1237,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
@ -1232,6 +1261,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return 886229536; return 886229536;
} }
// todo: this is not stable and will need to be migrated again...
long id = HashCode.Combine(server.IP, server.Port); long id = HashCode.Combine(server.IP, server.Port);
id = id < 0 ? Math.Abs(id) : id; id = id < 0 ? Math.Abs(id) : id;
long? serverId; long? serverId;

View File

@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Collections.Generic; using System.Collections.Generic;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Plugins.Stats.Models namespace IW4MAdmin.Plugins.Stats.Models
{ {
@ -44,6 +45,8 @@ namespace IW4MAdmin.Plugins.Stats.Models
public float AdsPercent { get; set; } public float AdsPercent { get; set; }
[NotMapped] [NotMapped]
public List<Vector3> AnglesList { get; set; } public List<Vector3> AnglesList { get; set; }
[NotMapped]
public Game GameName { get; set; }
/// <summary> /// <summary>
/// Indicates if the attacker was alive after last captured angle /// Indicates if the attacker was alive after last captured angle

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

@ -22,7 +22,10 @@ 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<EFRating>()
.HasIndex(p => new { p.When, p.ServerId, p.Performance, p.ActivityAmount });
builder.Entity<EFClientMessage>() builder.Entity<EFClientMessage>()
.HasIndex(p => p.TimeSent); .HasIndex(p => p.TimeSent);

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;
} }
@ -182,8 +157,9 @@ namespace IW4MAdmin.Plugins.Stats
if (Config.Configuration() == null) if (Config.Configuration() == null)
{ {
Config.Set((StatsConfiguration)new StatsConfiguration().Generate()); Config.Set((StatsConfiguration)new StatsConfiguration().Generate());
await Config.Save();
} }
Config.Configuration().ApplyMigration();
await Config.Save();
// register the topstats page // register the topstats page
// todo:generate the URL/Location instead of hardcoding // todo:generate the URL/Location instead of hardcoding
@ -405,7 +381,7 @@ namespace IW4MAdmin.Plugins.Stats
return (await _chatQueryHelper.QueryResource(query)).Results; return (await _chatQueryHelper.QueryResource(query)).Results;
} }
if (Config.Configuration().EnableAntiCheat) if (Config.Configuration().AnticheatConfiguration.Enable)
{ {
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, getAnticheatInfo); _metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, getAnticheatInfo);
} }
@ -456,7 +432,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)
@ -496,6 +472,22 @@ namespace IW4MAdmin.Plugins.Stats
/// </summary> /// </summary>
/// <param name="s"></param> /// <param name="s"></param>
/// <returns></returns> /// <returns></returns>
private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().EnableAntiCheat && 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.11.18.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;
@ -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
{ {
@ -22,7 +25,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatResourceQueryHelper; private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatResourceQueryHelper;
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
public StatsController(ILogger logger, IManager manager, IResourceQueryHelper<ChatSearchQuery, MessageResponse> resourceQueryHelper, public StatsController(ILogger<StatsController> logger, IManager manager, IResourceQueryHelper<ChatSearchQuery, MessageResponse> resourceQueryHelper,
ITranslationLookup translationLookup) : base(manager) ITranslationLookup translationLookup) : base(manager)
{ {
_logger = logger; _logger = logger;
@ -86,7 +89,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 +111,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 +137,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;
} }
@ -189,13 +188,13 @@ 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.11.18.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -28,7 +28,7 @@
<CopyToPublishDirectory>Never</CopyToPublishDirectory> <CopyToPublishDirectory>Never</CopyToPublishDirectory>
</Content> </Content>
</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>

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

@ -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.11.18.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

@ -5,6 +5,8 @@ using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace SharedLibraryCore namespace SharedLibraryCore
{ {
@ -13,7 +15,7 @@ namespace SharedLibraryCore
/// </summary> /// </summary>
public abstract class Command : IManagerCommand public abstract class Command : IManagerCommand
{ {
private readonly CommandConfiguration _config; protected readonly CommandConfiguration _config;
protected readonly ITranslationLookup _translationLookup; protected readonly ITranslationLookup _translationLookup;
protected ILogger logger; protected ILogger logger;
@ -113,6 +115,25 @@ namespace SharedLibraryCore
} }
private EFClient.Permission permission; private EFClient.Permission permission;
public Game[] SupportedGames
{
get => supportedGames;
protected set
{
try
{
var savedGames = _config?.Commands[GetType().Name].SupportedGames;
supportedGames = savedGames?.Length != 0 ? savedGames : value;
}
catch (KeyNotFoundException)
{
supportedGames = value;
}
}
}
private Game[] supportedGames;
/// <summary> /// <summary>
/// Argument list for the command /// Argument list for the command

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"];
@ -691,13 +742,16 @@ 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}"); using (LogContext.PushProperty("Server", gameEvent.Origin.CurrentServer?.ToString()))
{
logger.LogWarning("Failed to set level of client {origin}", gameEvent.Origin.ToString());
}
gameEvent.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]); gameEvent.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
return; return;
} }
@ -772,8 +826,6 @@ namespace SharedLibraryCore.Commands
/// </summary> /// </summary>
public class ListAdminsCommand : Command public class ListAdminsCommand : Command
{ {
private readonly CommandConfiguration _config;
public ListAdminsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) public ListAdminsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{ {
Name = "admins"; Name = "admins";
@ -781,8 +833,6 @@ namespace SharedLibraryCore.Commands
Alias = "a"; Alias = "a";
Permission = Permission.User; Permission = Permission.User;
RequiresTarget = false; RequiresTarget = false;
_config = config;
} }
public static string OnlineAdmins(Server S, ITranslationLookup lookup) public static string OnlineAdmins(Server S, ITranslationLookup lookup)
@ -832,22 +882,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);
} }
} }
@ -901,8 +947,6 @@ namespace SharedLibraryCore.Commands
/// </summary> /// </summary>
public class ListRulesCommands : Command public class ListRulesCommands : Command
{ {
private readonly CommandConfiguration _config;
public ListRulesCommands(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) public ListRulesCommands(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{ {
Name = "rules"; Name = "rules";
@ -910,8 +954,6 @@ namespace SharedLibraryCore.Commands
Alias = "r"; Alias = "r";
Permission = Permission.User; Permission = Permission.User;
RequiresTarget = false; RequiresTarget = false;
_config = config;
} }
public override Task ExecuteAsync(GameEvent E) public override Task ExecuteAsync(GameEvent E)
@ -1537,7 +1579,7 @@ namespace SharedLibraryCore.Commands
/// </summary> /// </summary>
public class SetGravatarCommand : Command public class SetGravatarCommand : Command
{ {
private readonly IMetaService _metaService; private readonly IMetaService _metaService;
public SetGravatarCommand(CommandConfiguration config, ITranslationLookup translationLookup, IMetaService metaService) : base(config, translationLookup) public SetGravatarCommand(CommandConfiguration config, ITranslationLookup translationLookup, IMetaService metaService) : base(config, translationLookup)
{ {

View File

@ -0,0 +1,37 @@
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using System.Threading.Tasks;
using static SharedLibraryCore.Server;
namespace SharedLibraryCore.Commands
{
public class PrivateMessageAdminsCommand : Command
{
public PrivateMessageAdminsCommand(CommandConfiguration config, ITranslationLookup lookup) : base(config, lookup)
{
Name = "privatemessageadmin";
Description = lookup["COMMANDS_PMADMINS_DESC"];
Alias = "pma";
Permission = EFClient.Permission.Moderator;
SupportedGames = new[] { Game.IW4, Game.IW5 };
}
public override Task ExecuteAsync(GameEvent E)
{
bool isGameSupported = _config.Commands[nameof(PrivateMessageAdminsCommand)].SupportedGames.Length > 0 &&
_config.Commands[nameof(PrivateMessageAdminsCommand)].SupportedGames.Contains(E.Owner.GameName);
if (!isGameSupported)
{
E.Origin.Tell(_translationLookup["COMMANDS_GAME_NOT_SUPPORTED"].FormatExt(nameof(PrivateMessageAdminsCommand)));
return Task.CompletedTask;
}
E.Owner.ToAdmins(E.Data);
return Task.CompletedTask;
}
}
}

View File

@ -47,6 +47,8 @@ namespace SharedLibraryCore.Configuration
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")]
@ -91,12 +93,16 @@ namespace SharedLibraryCore.Configuration
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;
[UIHint("ServerConfiguration")] [UIHint("ServerConfiguration")]
public ServerConfiguration[] Servers { get; set; } public ServerConfiguration[] Servers { get; set; }
[ConfigurationIgnore] [ConfigurationIgnore]
public string Id { get; set; } public string Id { get; set; }
[ConfigurationIgnore] [ConfigurationIgnore]
public string SubscriptionId { get; set; }
[ConfigurationIgnore]
public MapConfiguration[] Maps { get; set; } public MapConfiguration[] Maps { get; set; }
[ConfigurationIgnore] [ConfigurationIgnore]
public QuickMessageConfiguration[] QuickMessages { get; set; } public QuickMessageConfiguration[] QuickMessages { get; set; }

View File

@ -1,6 +1,7 @@
using Newtonsoft.Json.Converters; using Newtonsoft.Json;
using System.Text.Json.Serialization; using Newtonsoft.Json.Converters;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.Server;
namespace SharedLibraryCore.Configuration namespace SharedLibraryCore.Configuration
{ {
@ -29,5 +30,11 @@ namespace SharedLibraryCore.Configuration
/// Indicates if the command can be run by another user (impersonation) /// Indicates if the command can be run by another user (impersonation)
/// </summary> /// </summary>
public bool AllowImpersonation { get; set; } public bool AllowImpersonation { get; set; }
/// <summary>
/// Specifies the games supporting the functionality of the command
/// </summary>
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
public Game[] SupportedGames { get; set; } = new Game[0];
} }
} }

View File

@ -205,7 +205,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

@ -27,6 +27,11 @@ namespace SharedLibraryCore.Dtos.Meta.Responses
/// indicates if the chat message is a quick message phrase /// indicates if the chat message is a quick message phrase
/// </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,6 +1,4 @@
using System; namespace SharedLibraryCore.Dtos.Meta.Responses
namespace SharedLibraryCore.Dtos.Meta.Responses
{ {
public class UpdatedAliasResponse : BaseMetaResponse public class UpdatedAliasResponse : BaseMetaResponse
{ {
@ -17,6 +15,6 @@ namespace SharedLibraryCore.Dtos.Meta.Responses
return false; return false;
} }
public override int GetHashCode() => HashCode.Combine(Name.StripColors(), IPAddress); public override int GetHashCode() => $"{Name.StripColors()}{IPAddress}".GetStableHashCode();
} }
} }

View File

@ -3,6 +3,8 @@ using SharedLibraryCore.Events;
using System; using System;
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
{ {
@ -254,9 +256,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 +265,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,14 +278,12 @@ 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;
return this; return this;

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);

View File

@ -6,6 +6,7 @@ using SharedLibraryCore.Database.Models;
using System.Threading; using System.Threading;
using System.Collections; using System.Collections;
using System; using System;
using Microsoft.Extensions.Logging;
namespace SharedLibraryCore.Interfaces namespace SharedLibraryCore.Interfaces
{ {
@ -15,6 +16,7 @@ namespace SharedLibraryCore.Interfaces
Task Start(); Task Start();
void Stop(); void Stop();
void Restart(); void Restart();
[Obsolete]
ILogger GetLogger(long serverId); ILogger GetLogger(long serverId);
IList<Server> GetServers(); IList<Server> GetServers();
IList<IManagerCommand> GetCommands(); IList<IManagerCommand> GetCommands();

View File

@ -1,5 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.Server;
namespace SharedLibraryCore.Interfaces namespace SharedLibraryCore.Interfaces
{ {
@ -35,6 +36,11 @@ namespace SharedLibraryCore.Interfaces
/// </summary> /// </summary>
Permission Permission { get; } Permission Permission { get; }
/// <summary>
/// Games the command is supported on
/// </summary>
Game[] SupportedGames { get; }
/// <summary> /// <summary>
/// Syntax for using the command /// Syntax for using the command
/// </summary> /// </summary>

View File

@ -17,9 +17,9 @@ namespace SharedLibraryCore.Interfaces
Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = ""); Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "");
/// <summary> /// <summary>
/// sets the rcon parser configuration /// sets the rcon parser
/// </summary> /// </summary>
/// <param name="config">parser config</param> /// <param name="config">parser</param>
void SetConfiguration(IRConParserConfiguration config); void SetConfiguration(IRConParser config);
} }
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
@ -82,5 +83,12 @@ namespace SharedLibraryCore.Interfaces
/// <param name="dvarName">dvar key name</param> /// <param name="dvarName">dvar key name</param>
/// <returns></returns> /// <returns></returns>
T GetDefaultDvarValue<T>(string dvarName); T GetDefaultDvarValue<T>(string dvarName);
/// <summary>
/// determines the amount of time to wait for the command to respond
/// </summary>
/// <param name="command">name of command being executed</param>
/// <returns></returns>
TimeSpan OverrideTimeoutForCommand(string command);
} }
} }

View File

@ -62,5 +62,9 @@ namespace SharedLibraryCore.Interfaces
/// specifies the default dvar values for games that don't support certain dvars /// specifies the default dvar values for games that don't support certain dvars
/// </summary> /// </summary>
IDictionary<string, string> DefaultDvarValues { get; set; } IDictionary<string, string> DefaultDvarValues { get; set; }
int NoticeMaximumLines { get; set; }
int NoticeMaxCharactersPerLine { get; set; }
} }
} }

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Reflection;
namespace SharedLibraryCore.Interfaces
{
public interface IRemoteAssemblyHandler
{
IEnumerable<Assembly> DecryptAssemblies(string[] encryptedAssemblies);
IEnumerable<string> DecryptScripts(string[] encryptedScripts);
}
}

View File

@ -15,9 +15,10 @@ namespace SharedLibraryCore.Interfaces
/// <param name="alias">alias of command</param> /// <param name="alias">alias of command</param>
/// <param name="description">description of command</param> /// <param name="description">description of command</param>
/// <param name="permission">minimum required permission</param> /// <param name="permission">minimum required permission</param>
/// <param name="isTargetRequired">target required or not</param>
/// <param name="args">command arguments (name, is required)</param> /// <param name="args">command arguments (name, is required)</param>
/// <param name="executeAction">action to peform when commmand is executed</param> /// <param name="executeAction">action to peform when commmand is executed</param>
/// <returns></returns> /// <returns></returns>
IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction); IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, bool isTargetRequired, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction);
} }
} }

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace SharedLibraryCore.Interfaces
{
/// <summary>
/// interface used to dynamically resolve services by string name
/// </summary>
public interface IScriptPluginServiceResolver
{
/// <summary>
/// resolves a service with the given name
/// </summary>
/// <param name="serviceName">class name of service</param>
/// <returns></returns>
object ResolveService(string serviceName);
/// <summary>
/// resolves a service with the given name and generic params
/// </summary>
/// <param name="serviceName">class name of service</param>
/// <param name="genericParams">generic class names</param>
/// <returns></returns>
object ResolveService(string serviceName, string[] genericParameters);
}
}

View File

@ -0,0 +1,927 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SharedLibraryCore.Database;
namespace SharedLibraryCore.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20201114232340_UpdateEFRatingIndex")]
partial class UpdateEFRatingIndex
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.7");
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b =>
{
b.Property<int>("SnapshotId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<int>("CurrentSessionLength")
.HasColumnType("INTEGER");
b.Property<double>("CurrentStrain")
.HasColumnType("REAL");
b.Property<int>("CurrentViewAngleId")
.HasColumnType("INTEGER");
b.Property<int>("Deaths")
.HasColumnType("INTEGER");
b.Property<double>("Distance")
.HasColumnType("REAL");
b.Property<double>("EloRating")
.HasColumnType("REAL");
b.Property<int>("HitDestinationId")
.HasColumnType("INTEGER");
b.Property<int>("HitLocation")
.HasColumnType("INTEGER");
b.Property<int>("HitOriginId")
.HasColumnType("INTEGER");
b.Property<int>("HitType")
.HasColumnType("INTEGER");
b.Property<int>("Hits")
.HasColumnType("INTEGER");
b.Property<int>("Kills")
.HasColumnType("INTEGER");
b.Property<int>("LastStrainAngleId")
.HasColumnType("INTEGER");
b.Property<double>("RecoilOffset")
.HasColumnType("REAL");
b.Property<double>("SessionAngleOffset")
.HasColumnType("REAL");
b.Property<double>("SessionAverageSnapValue")
.HasColumnType("REAL");
b.Property<double>("SessionSPM")
.HasColumnType("REAL");
b.Property<int>("SessionScore")
.HasColumnType("INTEGER");
b.Property<int>("SessionSnapHits")
.HasColumnType("INTEGER");
b.Property<double>("StrainAngleBetween")
.HasColumnType("REAL");
b.Property<int>("TimeSinceLastEvent")
.HasColumnType("INTEGER");
b.Property<int>("WeaponId")
.HasColumnType("INTEGER");
b.Property<DateTime>("When")
.HasColumnType("TEXT");
b.HasKey("SnapshotId");
b.HasIndex("ClientId");
b.HasIndex("CurrentViewAngleId");
b.HasIndex("HitDestinationId");
b.HasIndex("HitOriginId");
b.HasIndex("LastStrainAngleId");
b.ToTable("EFACSnapshot");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshotVector3", b =>
{
b.Property<int>("ACSnapshotVector3Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("SnapshotId")
.HasColumnType("INTEGER");
b.Property<int>("Vector3Id")
.HasColumnType("INTEGER");
b.HasKey("ACSnapshotVector3Id");
b.HasIndex("SnapshotId");
b.HasIndex("Vector3Id");
b.ToTable("EFACSnapshotVector3");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
{
b.Property<long>("KillId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("AttackerId")
.HasColumnType("INTEGER");
b.Property<int>("Damage")
.HasColumnType("INTEGER");
b.Property<int?>("DeathOriginVector3Id")
.HasColumnType("INTEGER");
b.Property<int>("DeathType")
.HasColumnType("INTEGER");
b.Property<double>("Fraction")
.HasColumnType("REAL");
b.Property<int>("HitLoc")
.HasColumnType("INTEGER");
b.Property<bool>("IsKill")
.HasColumnType("INTEGER");
b.Property<int?>("KillOriginVector3Id")
.HasColumnType("INTEGER");
b.Property<int>("Map")
.HasColumnType("INTEGER");
b.Property<long>("ServerId")
.HasColumnType("INTEGER");
b.Property<int>("VictimId")
.HasColumnType("INTEGER");
b.Property<int?>("ViewAnglesVector3Id")
.HasColumnType("INTEGER");
b.Property<double>("VisibilityPercentage")
.HasColumnType("REAL");
b.Property<int>("Weapon")
.HasColumnType("INTEGER");
b.Property<DateTime>("When")
.HasColumnType("TEXT");
b.HasKey("KillId");
b.HasIndex("AttackerId");
b.HasIndex("DeathOriginVector3Id");
b.HasIndex("KillOriginVector3Id");
b.HasIndex("ServerId");
b.HasIndex("VictimId");
b.HasIndex("ViewAnglesVector3Id");
b.ToTable("EFClientKills");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
{
b.Property<long>("MessageId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.HasColumnType("TEXT");
b.Property<long>("ServerId")
.HasColumnType("INTEGER");
b.Property<DateTime>("TimeSent")
.HasColumnType("TEXT");
b.HasKey("MessageId");
b.HasIndex("ClientId");
b.HasIndex("ServerId");
b.HasIndex("TimeSent");
b.ToTable("EFClientMessages");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b =>
{
b.Property<int>("RatingHistoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.HasKey("RatingHistoryId");
b.HasIndex("ClientId");
b.ToTable("EFClientRatingHistory");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
{
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<long>("ServerId")
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<double>("AverageRecoilOffset")
.HasColumnType("REAL");
b.Property<double>("AverageSnapValue")
.HasColumnType("REAL");
b.Property<int>("Deaths")
.HasColumnType("INTEGER");
b.Property<double>("EloRating")
.HasColumnType("REAL");
b.Property<int>("Kills")
.HasColumnType("INTEGER");
b.Property<double>("MaxStrain")
.HasColumnType("REAL");
b.Property<double>("RollingWeightedKDR")
.HasColumnType("REAL");
b.Property<double>("SPM")
.HasColumnType("REAL");
b.Property<double>("Skill")
.HasColumnType("REAL");
b.Property<int>("SnapHitCount")
.HasColumnType("INTEGER");
b.Property<int>("TimePlayed")
.HasColumnType("INTEGER");
b.Property<double>("VisionAverage")
.HasColumnType("REAL");
b.HasKey("ClientId", "ServerId");
b.HasIndex("ServerId");
b.ToTable("EFClientStatistics");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
{
b.Property<int>("HitLocationCountId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("EFClientStatisticsClientId")
.HasColumnName("EFClientStatisticsClientId")
.HasColumnType("INTEGER");
b.Property<long>("EFClientStatisticsServerId")
.HasColumnName("EFClientStatisticsServerId")
.HasColumnType("INTEGER");
b.Property<int>("HitCount")
.HasColumnType("INTEGER");
b.Property<float>("HitOffsetAverage")
.HasColumnType("REAL");
b.Property<int>("Location")
.HasColumnType("INTEGER");
b.Property<float>("MaxAngleDistance")
.HasColumnType("REAL");
b.HasKey("HitLocationCountId");
b.HasIndex("EFClientStatisticsServerId");
b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId");
b.ToTable("EFHitLocationCounts");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b =>
{
b.Property<int>("RatingId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("ActivityAmount")
.HasColumnType("INTEGER");
b.Property<bool>("Newest")
.HasColumnType("INTEGER");
b.Property<double>("Performance")
.HasColumnType("REAL");
b.Property<int>("Ranking")
.HasColumnType("INTEGER");
b.Property<int>("RatingHistoryId")
.HasColumnType("INTEGER");
b.Property<long?>("ServerId")
.HasColumnType("INTEGER");
b.Property<DateTime>("When")
.HasColumnType("TEXT");
b.HasKey("RatingId");
b.HasIndex("RatingHistoryId");
b.HasIndex("ServerId");
b.HasIndex("Performance", "Ranking", "When");
b.HasIndex("When", "ServerId", "Performance", "ActivityAmount");
b.ToTable("EFRating");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b =>
{
b.Property<long>("ServerId")
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<string>("EndPoint")
.HasColumnType("TEXT");
b.Property<int?>("GameName")
.HasColumnType("INTEGER");
b.Property<string>("HostName")
.HasColumnType("TEXT");
b.Property<bool>("IsPasswordProtected")
.HasColumnType("INTEGER");
b.Property<int>("Port")
.HasColumnType("INTEGER");
b.HasKey("ServerId");
b.ToTable("EFServers");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
{
b.Property<int>("StatisticId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<long>("ServerId")
.HasColumnType("INTEGER");
b.Property<long>("TotalKills")
.HasColumnType("INTEGER");
b.Property<long>("TotalPlayTime")
.HasColumnType("INTEGER");
b.HasKey("StatisticId");
b.HasIndex("ServerId");
b.ToTable("EFServerStatistics");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
{
b.Property<int>("AliasId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.Property<int?>("IPAddress")
.HasColumnType("INTEGER");
b.Property<int>("LinkId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(24);
b.Property<string>("SearchableName")
.HasColumnType("TEXT")
.HasMaxLength(24);
b.HasKey("AliasId");
b.HasIndex("IPAddress");
b.HasIndex("LinkId");
b.HasIndex("Name");
b.HasIndex("SearchableName");
b.HasIndex("Name", "IPAddress")
.IsUnique();
b.ToTable("EFAlias");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b =>
{
b.Property<int>("AliasLinkId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.HasKey("AliasLinkId");
b.ToTable("EFAliasLinks");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b =>
{
b.Property<int>("ChangeHistoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<string>("Comment")
.HasColumnType("TEXT")
.HasMaxLength(128);
b.Property<string>("CurrentValue")
.HasColumnType("TEXT");
b.Property<int?>("ImpersonationEntityId")
.HasColumnType("INTEGER");
b.Property<int>("OriginEntityId")
.HasColumnType("INTEGER");
b.Property<string>("PreviousValue")
.HasColumnType("TEXT");
b.Property<int>("TargetEntityId")
.HasColumnType("INTEGER");
b.Property<DateTime>("TimeChanged")
.HasColumnType("TEXT");
b.Property<int>("TypeOfChange")
.HasColumnType("INTEGER");
b.HasKey("ChangeHistoryId");
b.ToTable("EFChangeHistory");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
{
b.Property<int>("ClientId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("AliasLinkId")
.HasColumnType("INTEGER");
b.Property<int>("Connections")
.HasColumnType("INTEGER");
b.Property<int>("CurrentAliasId")
.HasColumnType("INTEGER");
b.Property<DateTime>("FirstConnection")
.HasColumnType("TEXT");
b.Property<DateTime>("LastConnection")
.HasColumnType("TEXT");
b.Property<int>("Level")
.HasColumnType("INTEGER");
b.Property<bool>("Masked")
.HasColumnType("INTEGER");
b.Property<long>("NetworkId")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasColumnType("TEXT");
b.Property<string>("PasswordSalt")
.HasColumnType("TEXT");
b.Property<int>("TotalConnectionTime")
.HasColumnType("INTEGER");
b.HasKey("ClientId");
b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId")
.IsUnique();
b.ToTable("EFClients");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b =>
{
b.Property<int>("MetaId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<string>("Extra")
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<DateTime>("Updated")
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("MetaId");
b.HasIndex("ClientId");
b.HasIndex("Key");
b.ToTable("EFMeta");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
{
b.Property<int>("PenaltyId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<string>("AutomatedOffense")
.HasColumnType("TEXT");
b.Property<DateTime?>("Expires")
.HasColumnType("TEXT");
b.Property<bool>("IsEvadedOffense")
.HasColumnType("INTEGER");
b.Property<int>("LinkId")
.HasColumnType("INTEGER");
b.Property<int>("OffenderId")
.HasColumnType("INTEGER");
b.Property<string>("Offense")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("PunisherId")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<DateTime>("When")
.HasColumnType("TEXT");
b.HasKey("PenaltyId");
b.HasIndex("LinkId");
b.HasIndex("OffenderId");
b.HasIndex("PunisherId");
b.ToTable("EFPenalties");
});
modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b =>
{
b.Property<int>("Vector3Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<float>("X")
.HasColumnType("REAL");
b.Property<float>("Y")
.HasColumnType("REAL");
b.Property<float>("Z")
.HasColumnType("REAL");
b.HasKey("Vector3Id");
b.ToTable("Vector3");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "CurrentViewAngle")
.WithMany()
.HasForeignKey("CurrentViewAngleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitDestination")
.WithMany()
.HasForeignKey("HitDestinationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitOrigin")
.WithMany()
.HasForeignKey("HitOriginId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "LastStrainAngle")
.WithMany()
.HasForeignKey("LastStrainAngleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshotVector3", b =>
{
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", "Snapshot")
.WithMany("PredictedViewAngles")
.HasForeignKey("SnapshotId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "Vector")
.WithMany()
.HasForeignKey("Vector3Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker")
.WithMany()
.HasForeignKey("AttackerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin")
.WithMany()
.HasForeignKey("DeathOriginVector3Id");
b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin")
.WithMany()
.HasForeignKey("KillOriginVector3Id");
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim")
.WithMany()
.HasForeignKey("VictimId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles")
.WithMany()
.HasForeignKey("ViewAnglesVector3Id");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("EFClientStatisticsClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("EFClientStatisticsServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", null)
.WithMany("HitLocations")
.HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b =>
{
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", "RatingHistory")
.WithMany("Ratings")
.HasForeignKey("RatingHistoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
{
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
.WithMany("Children")
.HasForeignKey("LinkId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink")
.WithMany()
.HasForeignKey("AliasLinkId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias")
.WithMany()
.HasForeignKey("CurrentAliasId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany("Meta")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
.WithMany("ReceivedPenalties")
.HasForeignKey("LinkId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender")
.WithMany("ReceivedPenalties")
.HasForeignKey("OffenderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher")
.WithMany("AdministeredPenalties")
.HasForeignKey("PunisherId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace SharedLibraryCore.Migrations
{
public partial class UpdateEFRatingIndex : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFRating_When_ServerId_Performance_ActivityAmount",
table: "EFRating",
columns: new[] { "When", "ServerId", "Performance", "ActivityAmount" });
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFRating_When_ServerId_Performance_ActivityAmount",
table: "EFRating");
}
}
}

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