Compare commits

...

94 Commits

Author SHA1 Message Date
1b773f21c6 fix alignment for long server names 2021-06-30 10:44:43 -05:00
bccbcce3c1 add lobby rating to home
add gametype (WIP) to home
misc UI tweaks
2021-06-30 09:57:07 -05:00
fc0bed2405 show "out of" ranked players for stats command 2021-06-29 17:14:25 -05:00
16cfb33109 improvements and consistencies to the top stats, most played and top players commands 2021-06-29 15:35:56 -05:00
42979dc5ae Use string for AC snapshot weapon and hit location
Add webfront logging
2021-06-29 15:02:01 -05:00
95cbc85144 fix issue with selecting wrong parser during setup
add minimum name length option
fix issue with stats spm
2021-06-27 20:31:39 -05:00
9cbca390fe Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2021-06-16 08:55:56 -05:00
38c0c81451 Added CSGO maps (#210)
Added all current default CSGO maps (Competitive, Wingman, Casual, War Games, Retakes, Danger Zone)
2021-06-16 08:54:49 -05:00
af4630ecb9 Additional CSGO compatibility improvements 2021-06-16 08:53:50 -05:00
dbceb23823 fix issue with custom event registration 2021-06-16 08:51:22 -05:00
e628ac0e9e improve CS:GO compatibility 2021-06-11 11:52:30 -05:00
3a1e8359c2 add one log indicator for games (Pluto IW5) that don't log to mods folder even when fs_game is specified. 2021-06-07 16:58:36 -05:00
c397fd5479 update pluto iw5 parser for new version
fix issue with finding players with color codes in name
2021-06-06 13:40:58 -05:00
16e1bbb1b5 fix bug with additional group mapping key 2021-06-03 13:21:34 -05:00
eff1fe237d Fix null pointer exception (#207) 2021-06-03 10:52:27 -05:00
b09ce46ff9 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2021-06-03 10:51:19 -05:00
be08d49f0a add initial CS:GO support 2021-06-03 10:51:03 -05:00
b9fb274db6 Update ParserPT6.js (#206) 2021-05-15 09:22:34 -05:00
9488f754d4 Fix stupid idiot things 2021-05-15 09:20:49 -05:00
1595c1fa99 Initial implementation of configuration support for script plugins 2021-05-14 21:52:55 -05:00
4d21680d59 small issue fix with api and more checks for welcome tags 2021-05-04 19:01:09 -05:00
127af98b00 fix issue with help and dynamically loaded plugins with commands 2021-04-30 12:37:55 -05:00
21a9eb8716 Update DefaultSettings.json (T4, IW5, S1x) (#202)
* Update DefaultSettings.json
2021-04-30 12:35:38 -05:00
f1593e2f99 fix issue with chat message search 2021-04-18 09:17:01 -05:00
74dbc3572f Added WaW bot guid (#200)
may be PlutoniumT4 only.
2021-04-16 13:48:52 -05:00
e6d149736a Added T4 weapon names. (#198) 2021-04-16 13:47:58 -05:00
a034394610 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2021-04-16 13:38:34 -05:00
34e7d69110 Add RCon support for S1x 2021-04-16 13:35:51 -05:00
4b686e5fdd Update Plutonium T4 Parser [v0.2]
Static version string
2021-04-08 09:36:32 -05:00
0428453426 Update Pluto T4 Parser
Uses new static version string.
2021-04-08 09:36:32 -05:00
e80e5d6a70 remove test code 2021-04-07 09:53:32 -05:00
22cf3081e1 update parser for Plutonium T4 2021-04-07 09:50:41 -05:00
76a18d9797 add parser support for Plutonium T4 2021-04-07 09:33:49 -05:00
fc13363c9c add user agent header for vpn detection issue #195 2021-04-07 08:47:42 -05:00
f916c51bc0 fix issue with iw5 weapon prefix not being removed properly 2021-04-01 13:12:47 -05:00
21087d6c25 remove whitespace on alias display and client name search 2021-03-31 11:20:32 -05:00
c84e374274 fix issue with client api for issue #191 2021-03-27 19:01:27 -05:00
e777a68105 properly pass game name to game string config finder.
add weapon prefix to weapon name parser for (iw5).
add some iw3 game strings
2021-03-23 21:42:26 -05:00
1f9c80e23b strip colors from header penalty on profile 2021-03-23 21:42:26 -05:00
33371a6d28 Added iw6 aliases (#184) 2021-03-23 21:42:26 -05:00
Edo
d164ef2eab Removed tempbanclient (#187)
Removed tempbanclient because Tekno has "weird" internal DB that manages temp bans it it would interfere with iw4m
2021-03-23 10:36:33 -05:00
e2ed57f674 prevent autoflag from running player has been manually unflagged 2021-03-23 10:34:44 -05:00
824b1c0990 prevent loading of privileged clients page for issue #188 2021-03-23 10:28:17 -05:00
a8b331a5e5 prevent missing config from causing stats error
small advanced stats fixes
2021-03-23 10:16:27 -05:00
802ec8cea5 Added iw6 aliases (#184) 2021-03-23 08:14:07 -05:00
2313c4357b add removal of obsolete plugins 2021-03-22 11:46:32 -05:00
c5375b661b huge commit for advanced stats feature.
broke data out into its own library.
may be breaking changes with existing plugins
2021-03-22 11:09:25 -05:00
db2e1deb2f modify rule shortcut to just have 1 list 2021-02-27 09:40:25 -06:00
191a68e7dd revert unintended commit file 2021-01-24 13:30:22 -06:00
c4f19e94ef implement custom tag (descriptor) feature
allow override of level names through configuration
few small fixes/improvements
2021-01-24 11:47:19 -06:00
c419d80b57 preemptive checks 2021-01-17 22:12:18 -06:00
23a33ba489 implement more robust command api and login
improve web console command response reliability and consistency
2021-01-17 21:58:18 -06:00
dd3ebf6b34 increase buffer size for rcon connection 2021-01-17 20:04:32 -06:00
28373b9325 implement admin "privacy" for issue #185 2021-01-09 12:37:20 -06:00
843c01061d update 'uptime' output
use translations for certain webfront page meta that was neglected
update plutonium parsers to not use new line in notices as it is not supported
2021-01-08 19:21:23 -06:00
5cb2d05f33 add preset rules, configurable time spans, and separate rule shortcut for issue #180 2020-12-31 18:48:58 -06:00
5a288dafc1 update shared library core version and plugins 2020-12-20 19:23:14 -06:00
4afc478076 fix issue with view stats and reset stats failing
fix issue with set level returning wrong error message if setting a client to the same level they're currently at
update CoD4x parser version
update nuget packages
2020-12-16 13:11:30 -06:00
928cbef845 resolve bot guid issue with T5
remove unneeded check for CNCT state
2020-12-14 21:10:50 -06:00
02b910234a add official T4/WaW support for issue #178
CoD4x parser tweak to parse full guid as decimal
2020-12-13 20:33:37 -06:00
f03626c3ae Another tweak for CoD4x rcon parsing. 2020-12-12 21:43:27 -06:00
6648b75255 update CoD4x parser
tweak handling segmented status response
actually support more than 18 clients LOL
2020-12-02 14:29:49 -06:00
bd3f0caf60 fix memory leak issue related to AddDbContext not working as expected 2020-11-29 16:01:52 -06:00
b2d282d412 include ; for timeout string 2020-11-27 22:08:13 -06:00
36a02b3d7b update for database provider specific migrations
fix issues with live radar
2020-11-27 21:52:52 -06:00
8ef2959f63 make notice line separator configurable for different parsers
(updated tekno's as it doesn't support \n)
2020-11-19 20:48:25 -06:00
d58b24b5b2 add shortcut for rules in penalty reasons for issue #159 2020-11-18 18:48:51 -06:00
09f37d7941 clean up some logic related to tracking stats on player join 2020-11-18 16:28:14 -06:00
103d2726c2 persist say command messages with webfront denotation to chat log
per issue #159
2020-11-18 09:08:24 -06:00
941d9cea73 more consistent/enhanced game penalty messages per issue #171 2020-11-17 18:24:54 -06:00
a574fb0d4b update index for ratings/prune old entries
small stat tweaks to add players on first kill/damage event
(instead of on connect which causes issues with slow writes)
2020-11-14 18:24:51 -06:00
664eb32587 fix small logging issue with loading plugins
add minigun turret to list of ignored ac weapons
2020-11-14 10:53:01 -06:00
6619ce714a modify iw6x parser to default game log vars temporarily, small amount of code cleanup to git rid of warnings 2020-11-12 20:39:56 -06:00
e997b94b3b update unit tests 2020-11-12 19:46:17 -06:00
5d9c8f5369 fix introduced issue with map/map_rotate commands 2020-11-11 18:53:23 -06:00
570a228c92 refactor logging in pretty big overhaul 2020-11-11 17:35:55 -06:00
fd7bd7e0da partial support of IW6x until the game log is implemented 2020-11-07 10:40:58 -06:00
e76976799b fix issue with partial matches for map load command 2020-11-03 20:04:11 -06:00
84189cf136 fix issue with T5 status response parsing 2020-10-31 09:18:37 -05:00
98ee997bf3 update pipeline versioning 2020-10-25 10:03:15 -05:00
3f7372e780 add pre release pipeline to master 2020-10-24 21:45:30 -05:00
08676f1d1e implement remote assembly loading 2020-10-24 15:02:38 -05:00
2bbafbd8f0 fix issue with delay on map command 2020-10-17 10:55:49 -05:00
40cb2a9df6 add say all (broadcast) command 2020-10-17 10:55:42 -05:00
59f1699228 fix issue with button detection 2020-10-17 10:55:29 -05:00
1484d63b97 hide flag status for non logged in users
remove erroneous anticheat detection reason on kick
2020-10-17 10:55:19 -05:00
04217e96ee fix anticheat detection type logic 2020-10-17 10:54:54 -05:00
c41fc27a1a fix introduced bug :) 2020-09-30 21:00:40 -05:00
1f1f4de67a anticheat tweaks
- reset recoil state on map change
- refactor config
- remove m21 from chest detection
- allow ignored client ids
2020-09-30 17:15:47 -05:00
7f11921757 enhance script plugin features
(support service resolver with generic args)
(support requiresTarget for command)
2020-09-28 20:32:53 -05:00
70cae976a0 implement service resolver for script plugins 2020-09-26 18:13:56 -05:00
2ab0cfa9be implement pm admins command for issue #170 2020-09-26 17:17:21 -05:00
7e3c74e63c add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x 2020-09-21 15:32:49 -05:00
a4a65a486a update GenerateGuidFromString to resolve to a stable hash code.
fix bots not showing up on live radar
2020-09-21 15:30:42 -05:00
637 changed files with 114186 additions and 6596 deletions

4
.gitignore vendored
View File

@ -244,3 +244,7 @@ launchSettings.json
/Tests/ApplicationTests/Files/GameEvents.json /Tests/ApplicationTests/Files/GameEvents.json
/Tests/ApplicationTests/Files/replay.json /Tests/ApplicationTests/Files/replay.json
/GameLogServer/game_log_server_env /GameLogServer/game_log_server_env
.idea/*
*.db
/Data/IW4MAdmin_Migration.db-shm
/Data/IW4MAdmin_Migration.db-wal

View File

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

View File

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

View File

@ -1,19 +1,15 @@
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;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Commands; using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Configuration.Validation; using SharedLibraryCore.Configuration.Validation;
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 +20,15 @@ using System.Reflection;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Data.Context;
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 +36,7 @@ namespace IW4MAdmin.Application
{ {
private readonly ConcurrentBag<Server> _servers; private readonly ConcurrentBag<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList(); public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
public ILogger Logger => GetLogger(0); [Obsolete] public ObsoleteLogger Logger => _serviceProvider.GetRequiredService<ObsoleteLogger>();
public bool IsRunning { get; private set; } public bool IsRunning { get; private set; }
public bool IsInitialized { get; private set; } public bool IsInitialized { get; private set; }
public DateTime StartTime { get; private set; } public DateTime StartTime { get; private set; }
@ -50,11 +54,9 @@ namespace IW4MAdmin.Application
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly List<MessageToken> MessageTokens; private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc; private readonly ClientService ClientSvc;
readonly AliasService AliasSvc;
readonly PenaltyService PenaltySvc; readonly PenaltyService PenaltySvc;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList; readonly IPageList PageList;
private readonly Dictionary<long, ILogger> _loggers = new Dictionary<long, ILogger>();
private readonly IMetaService _metaService; private readonly IMetaService _metaService;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource; private readonly CancellationTokenSource _tokenSource;
@ -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 ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new ConcurrentDictionary<long, GameEvent>();
public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands, public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration, ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory, IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
IMetaRegistration metaRegistration) IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>(); MessageTokens = new List<MessageToken>();
ClientSvc = new ClientService(contextFactory); ClientSvc = clientService;
AliasSvc = new AliasService(); PenaltySvc = penaltyService;
PenaltySvc = new PenaltyService();
ConfigHandler = appConfigHandler; ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow; StartTime = DateTime.UtcNow;
PageList = new PageList(); PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, appConfigHandler.Configuration()) }; AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(parserRegexFactory) }; AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication(); TokenAuthenticator = new TokenAuthentication();
_logger = logger; _logger = logger;
_metaService = metaService; _metaService = metaService;
_tokenSource = new CancellationTokenSource(); _tokenSource = new CancellationTokenSource();
_loggers.Add(0, logger);
_commands = commands.ToList(); _commands = commands.ToList();
_translationLookup = translationLookup; _translationLookup = translationLookup;
_commandConfiguration = commandConfiguration; _commandConfiguration = commandConfiguration;
@ -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,8 @@ namespace IW4MAdmin.Application
public async Task ExecuteEvent(GameEvent newEvent) public async Task ExecuteEvent(GameEvent newEvent)
{ {
#if DEBUG == true ProcessingEvents.TryAdd(newEvent.Id, newEvent);
Logger.WriteDebug($"Entering event process for {newEvent.Id}");
#endif
// the event has failed already // the event has failed already
if (newEvent.Failed) if (newEvent.Failed)
{ {
@ -122,22 +130,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 +153,58 @@ namespace IW4MAdmin.Application
catch (NetworkException ex) catch (NetworkException ex)
{ {
newEvent.FailReason = EventFailReason.Exception; newEvent.FailReason = EventFailReason.Exception;
Logger.WriteError(ex.Message); using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
Logger.WriteDebug(ex.GetExceptionInfo()); {
_logger.LogError(ex, ex.Message);
}
} }
catch (ServerException ex) catch (ServerException ex)
{ {
newEvent.FailReason = EventFailReason.Exception; newEvent.FailReason = EventFailReason.Exception;
Logger.WriteWarning(ex.Message); using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
{
_logger.LogError(ex, ex.Message);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
newEvent.FailReason = EventFailReason.Exception; newEvent.FailReason = EventFailReason.Exception;
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner)); Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner));
Logger.WriteDebug(ex.GetExceptionInfo()); using (LogContext.PushProperty("Server", newEvent.Owner?.ToString()))
{
_logger.LogError(ex, "Unexpected exception");
}
}
skip:
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null)
{
var correlatedEvents =
ProcessingEvents.Values.Where(ev =>
ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id)
.ToList();
await Task.WhenAll(correlatedEvents.Select(ev =>
ev.WaitAsync(Utilities.DefaultCommandTimeout, CancellationToken)));
newEvent.Output.AddRange(correlatedEvents.SelectMany(ev => ev.Output));
foreach (var correlatedEvent in correlatedEvents)
{
ProcessingEvents.Remove(correlatedEvent.Id, out _);
}
}
// we don't want to remove events that are correlated to command
if (ProcessingEvents.Values.ToList()?.Count(gameEvent => gameEvent.CorrelationId == newEvent.CorrelationId) == 1)
{
ProcessingEvents.Remove(newEvent.Id, out _);
} }
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()
@ -190,15 +220,15 @@ namespace IW4MAdmin.Application
public async Task UpdateServerStates() public async Task UpdateServerStates()
{ {
// store the server hash code and task for it // store the server hash code and task for it
var runningUpdateTasks = new Dictionary<long, Task>(); var runningUpdateTasks = new Dictionary<long, (Task task, CancellationTokenSource tokenSource, DateTime startTime)>();
while (!_tokenSource.IsCancellationRequested) while (!_tokenSource.IsCancellationRequested)
{ {
// select the server ids that have completed the update task // select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks var serverTasksToRemove = runningUpdateTasks
.Where(ut => ut.Value.Status == TaskStatus.RanToCompletion || .Where(ut => ut.Value.task.Status == TaskStatus.RanToCompletion ||
ut.Value.Status == TaskStatus.Canceled || ut.Value.task.Status == TaskStatus.Canceled || // we want to cancel if a task takes longer than 5 minutes
ut.Value.Status == TaskStatus.Faulted) ut.Value.task.Status == TaskStatus.Faulted || DateTime.Now - ut.Value.startTime > TimeSpan.FromMinutes(5))
.Select(ut => ut.Key) .Select(ut => ut.Key)
.ToList(); .ToList();
@ -209,9 +239,14 @@ namespace IW4MAdmin.Application
IsInitialized = true; IsInitialized = true;
} }
// remove the update tasks as they have completd // remove the update tasks as they have completed
foreach (long serverId in serverTasksToRemove) foreach (var serverId in serverTasksToRemove)
{ {
if (!runningUpdateTasks[serverId].tokenSource.Token.IsCancellationRequested)
{
runningUpdateTasks[serverId].tokenSource.Cancel();
}
runningUpdateTasks.Remove(serverId); runningUpdateTasks.Remove(serverId);
} }
@ -219,36 +254,29 @@ namespace IW4MAdmin.Application
var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList(); var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint))) foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
{ {
runningUpdateTasks.Add(server.EndPoint, Task.Run(async () => var tokenSource = new CancellationTokenSource();
runningUpdateTasks.Add(server.EndPoint, (Task.Run(async () =>
{ {
try try
{ {
await server.ProcessUpdatesAsync(_tokenSource.Token); await server.ProcessUpdatesAsync(_tokenSource.Token).WithWaitCancellation(runningUpdateTasks[server.EndPoint].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
{ {
server.IsInitialized = true; server.IsInitialized = true;
} }
})); }, tokenSource.Token), tokenSource, DateTime.Now));
} }
#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);
@ -270,6 +298,15 @@ namespace IW4MAdmin.Application
IsRunning = true; IsRunning = true;
ExternalIPAddress = await Utilities.GetExternalIP(); ExternalIPAddress = await Utilities.GetExternalIP();
#region DATABASE
_logger.LogInformation("Beginning database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]);
await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
_logger.LogInformation("Finished database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
#endregion
#region PLUGINS #region PLUGINS
foreach (var plugin in Plugins) foreach (var plugin in Plugins)
{ {
@ -277,18 +314,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 +338,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<DefaultSettings>("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 +375,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 +408,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,26 +430,18 @@ namespace IW4MAdmin.Application
} }
} }
if (config.Servers.Length == 0) if (_appConfig.Servers.Length == 0)
{ {
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid"); throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
} }
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252"); Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(_appConfig.CustomParserEncoding) ? _appConfig.CustomParserEncoding : "windows-1252");
#endregion #endregion
#region DATABASE
using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString,
GetApplicationSettings().Configuration()?.DatabaseProvider))
{
await new ContextSeed(db).Seed();
}
#endregion
#region COMMANDS #region COMMANDS
if (ClientSvc.GetOwners().Result.Count > 0) if (await ClientSvc.HasOwnerAsync(_tokenSource.Token))
{ {
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand)); _commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
} }
@ -438,8 +463,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 +474,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 +495,7 @@ namespace IW4MAdmin.Application
} }
#endregion #endregion
Console.WriteLine(_translationLookup["MANAGER_COMMUNICATION_INFO"]);
await InitializeServers(); await InitializeServers();
} }
@ -484,16 +511,20 @@ 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 = EventType.Start,
Data = $"{ServerInstance.GameName} started", Data = $"{ServerInstance.GameName} started",
Owner = ServerInstance Owner = ServerInstance
}; };
@ -504,13 +535,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 +574,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()
@ -577,11 +596,6 @@ namespace IW4MAdmin.Application
return ClientSvc; return ClientSvc;
} }
public AliasService GetAliasService()
{
return AliasSvc;
}
public PenaltyService GetPenaltyService() public PenaltyService GetPenaltyService()
{ {
return PenaltySvc; return PenaltySvc;
@ -604,7 +618,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,49 @@
{
"Serilog": {
"Using": [
"Serilog.Sinks.File"
],
"MinimumLevel": {
"Default": "Information",
"Override": {
"System": "Warning",
"Microsoft": "Warning"
}
},
"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

@ -0,0 +1,15 @@
using System.Collections.Generic;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Configuration
{
public class ScriptPluginConfiguration : Dictionary<string, Dictionary<string, object>>, IBaseConfiguration
{
public string Name() => nameof(ScriptPluginConfiguration);
public IBaseConfiguration Generate()
{
return new ScriptPluginConfiguration();
}
}
}

View File

@ -1,4 +1,4 @@
{ {
"AutoMessagePeriod": 60, "AutoMessagePeriod": 60,
"AutoMessages": [ "AutoMessages": [
"This server uses ^5IW4M Admin v{{VERSION}} ^7get it at ^5raidmax.org/IW4MAdmin", "This server uses ^5IW4M Admin v{{VERSION}} ^7get it at ^5raidmax.org/IW4MAdmin",
@ -16,7 +16,13 @@
"Keep grenade launcher use to a minimum", "Keep grenade launcher use to a minimum",
"Balance teams at ALL times" "Balance teams at ALL times"
], ],
"DisallowedClientNames": [ "Unknown Soldier", "VickNet", "UnknownSoldier", "CHEATER", "Play77" ], "DisallowedClientNames": [
"Unknown Soldier",
"VickNet",
"UnknownSoldier",
"CHEATER",
"Play77"
],
"QuickMessages": [ "QuickMessages": [
{ {
"Game": "IW4", "Game": "IW4",
@ -163,6 +169,18 @@
"Alias": "Asylum", "Alias": "Asylum",
"Name": "mp_asylum" "Name": "mp_asylum"
}, },
{
"Alias": "Banzai",
"Name": "mp_kwai"
},
{
"Alias": "Battery",
"Name": "mp_drum"
},
{
"Alias": "Breach",
"Name": "mp_bgate"
},
{ {
"Alias": "Castle", "Alias": "Castle",
"Name": "mp_castle" "Name": "mp_castle"
@ -171,6 +189,10 @@
"Alias": "Cliffside", "Alias": "Cliffside",
"Name": "mp_shrine" "Name": "mp_shrine"
}, },
{
"Alias": "Corrosion",
"Name": "mp_stalingrad"
},
{ {
"Alias": "Courtyard", "Alias": "Courtyard",
"Name": "mp_courtyard" "Name": "mp_courtyard"
@ -184,60 +206,52 @@
"Name": "mp_downfall" "Name": "mp_downfall"
}, },
{ {
"Alias": "Hanger", "Alias": "Hangar",
"Name": "mp_hangar" "Name": "mp_hangar"
}, },
{
"Alias": "Makin",
"Name": "mp_makin"
},
{
"Alias": "Outskirts",
"Name": "mp_outskirts"
},
{
"Alias": "Roundhouse",
"Name": "mp_roundhouse"
},
{
"Alias": "Upheaval",
"Name": "mp_suburban"
},
{ {
"Alias": "Knee Deep", "Alias": "Knee Deep",
"Name": "mp_kneedeep" "Name": "mp_kneedeep"
}, },
{
"Alias": "Makin",
"Name": "mp_makin"
},
{
"Alias": "Makin Day",
"Name": "mp_makin_day"
},
{ {
"Alias": "Nightfire", "Alias": "Nightfire",
"Name": "mp_nachtfeuer" "Name": "mp_nachtfeuer"
}, },
{
"Alias": "Outskirts",
"Name": "mp_outskirts"
},
{
"Alias": "Revolution",
"Name": "mp_vodka"
},
{
"Alias": "Roundhouse",
"Name": "mp_roundhouse"
},
{
"Alias": "Seelow",
"Name": "mp_seelow"
},
{ {
"Alias": "Station", "Alias": "Station",
"Name": "mp_subway" "Name": "mp_subway"
}, },
{
"Alias": "Banzai",
"Name": "mp_kwai"
},
{
"Alias": "Corrosion",
"Name": "mp_stalingrad"
},
{ {
"Alias": "Sub Pens", "Alias": "Sub Pens",
"Name": "mp_docks" "Name": "mp_docks"
}, },
{ {
"Alias": "Battery", "Alias": "Upheaval",
"Name": "mp_drum" "Name": "mp_suburban"
},
{
"Alias": "Breach",
"Name": "mp_bgate"
},
{
"Alias": "Revolution",
"Name": "mp_vodka"
} }
] ]
}, },
@ -248,222 +262,178 @@
"Alias": "Rust", "Alias": "Rust",
"Name": "mp_rust" "Name": "mp_rust"
}, },
{ {
"Alias": "Terminal", "Alias": "Terminal",
"Name": "mp_terminal" "Name": "mp_terminal"
}, },
{ {
"Alias": "Crash", "Alias": "Crash",
"Name": "mp_crash" "Name": "mp_crash"
}, },
{ {
"Alias": "Afghan", "Alias": "Afghan",
"Name": "mp_afghan" "Name": "mp_afghan"
}, },
{ {
"Alias": "Derail", "Alias": "Derail",
"Name": "mp_derail" "Name": "mp_derail"
}, },
{ {
"Alias": "Estate", "Alias": "Estate",
"Name": "mp_estate" "Name": "mp_estate"
}, },
{ {
"Alias": "Favela", "Alias": "Favela",
"Name": "mp_favela" "Name": "mp_favela"
}, },
{ {
"Alias": "Highrise", "Alias": "Highrise",
"Name": "mp_highrise" "Name": "mp_highrise"
}, },
{ {
"Alias": "Invasion", "Alias": "Invasion",
"Name": "mp_invasion" "Name": "mp_invasion"
}, },
{ {
"Alias": "Karachi", "Alias": "Karachi",
"Name": "mp_checkpoint" "Name": "mp_checkpoint"
}, },
{ {
"Alias": "Quarry", "Alias": "Quarry",
"Name": "mp_quarry" "Name": "mp_quarry"
}, },
{ {
"Alias": "Rundown", "Alias": "Rundown",
"Name": "mp_rundown" "Name": "mp_rundown"
}, },
{ {
"Alias": "Scrapyard", "Alias": "Scrapyard",
"Name": "mp_boneyard" "Name": "mp_boneyard"
}, },
{ {
"Alias": "Skidrow", "Alias": "Skidrow",
"Name": "mp_nightshift" "Name": "mp_nightshift"
}, },
{ {
"Alias": "Sub Base", "Alias": "Sub Base",
"Name": "mp_subbase" "Name": "mp_subbase"
}, },
{ {
"Alias": "Underpass", "Alias": "Underpass",
"Name": "mp_underpass" "Name": "mp_underpass"
}, },
{ {
"Alias": "Wasteland", "Alias": "Wasteland",
"Name": "mp_brecourt" "Name": "mp_brecourt"
}, },
{ {
"Alias": "Overgrown", "Alias": "Overgrown",
"Name": "mp_overgrown" "Name": "mp_overgrown"
}, },
{ {
"Alias": "Strike", "Alias": "Strike",
"Name": "mp_strike" "Name": "mp_strike"
}, },
{ {
"Alias": "Vacant", "Alias": "Vacant",
"Name": "mp_vacant" "Name": "mp_vacant"
}, },
{ {
"Alias": "Carnival", "Alias": "Carnival",
"Name": "mp_abandon" "Name": "mp_abandon"
}, },
{ {
"Alias": "Trailer Park", "Alias": "Trailer Park",
"Name": "mp_trailerpark" "Name": "mp_trailerpark"
}, },
{ {
"Alias": "Fuel", "Alias": "Fuel",
"Name": "mp_fuel2" "Name": "mp_fuel2"
}, },
{ {
"Alias": "Storm", "Alias": "Storm",
"Name": "mp_storm" "Name": "mp_storm"
}, },
{ {
"Alias": "Bailout", "Alias": "Bailout",
"Name": "mp_complex" "Name": "mp_complex"
}, },
{ {
"Alias": "Salvage", "Alias": "Salvage",
"Name": "mp_compact" "Name": "mp_compact"
}, },
{ {
"Alias": "Nuketown", "Alias": "Nuketown",
"Name": "mp_nuked" "Name": "mp_nuked"
}, },
{ {
"Alias": "Test map", "Alias": "Test map",
"Name": "iw4_credits" "Name": "iw4_credits"
}, },
{ {
"Alias": "Killhouse", "Alias": "Killhouse",
"Name": "mp_killhouse" "Name": "mp_killhouse"
}, },
{ {
"Alias": "Bog", "Alias": "Bog",
"Name": "mp_bog_sh" "Name": "mp_bog_sh"
}, },
{ {
"Alias": "Freighter", "Alias": "Freighter",
"Name": "mp_cargoship_sh" "Name": "mp_cargoship_sh"
}, },
{ {
"Alias": "Cargoship", "Alias": "Cargoship",
"Name": "mp_cargoship" "Name": "mp_cargoship"
}, },
{ {
"Alias": "Shipment", "Alias": "Shipment",
"Name": "mp_shipment" "Name": "mp_shipment"
}, },
{ {
"Alias": "Shipment - Long", "Alias": "Shipment - Long",
"Name": "mp_shipment_long" "Name": "mp_shipment_long"
}, },
{ {
"Alias": "Rust - Long", "Alias": "Rust - Long",
"Name": "mp_rust_long" "Name": "mp_rust_long"
}, },
{ {
"Alias": "Firing Range", "Alias": "Firing Range",
"Name": "mp_firingrange" "Name": "mp_firingrange"
}, },
{ {
"Alias": "Chemical Plant", "Alias": "Chemical Plant",
"Name": "mp_storm_spring" "Name": "mp_storm_spring"
}, },
{ {
"Alias": "Tropical Favela", "Alias": "Tropical Favela",
"Name": "mp_fav_tropical" "Name": "mp_fav_tropical"
}, },
{ {
"Alias": "Tropical Estate", "Alias": "Tropical Estate",
"Name": "mp_estate_tropical" "Name": "mp_estate_tropical"
}, },
{ {
"Alias": "Tropical Crash", "Alias": "Tropical Crash",
"Name": "mp_crash_tropical" "Name": "mp_crash_tropical"
}, },
{ {
"Alias": "Forgotten City", "Alias": "Forgotten City",
"Name": "mp_bloc_sh" "Name": "mp_bloc_sh"
}, },
{ {
"Alias": "Crossfire", "Alias": "Crossfire",
"Name": "mp_cross_fire" "Name": "mp_cross_fire"
}, },
{ {
"Alias": "Bloc", "Alias": "Bloc",
"Name": "mp_bloc" "Name": "mp_bloc"
}, },
{ {
"Alias": "Oilrig", "Alias": "Oilrig",
"Name": "oilrig" "Name": "oilrig"
}, },
{ {
"Name": "Village", "Name": "Village",
"Alias": "co_hunted" "Alias": "co_hunted"
@ -519,7 +489,7 @@
}, },
{ {
"Alias": "Havana", "Alias": "Havana",
"Name": "mp_cairo" "Name": "mp_cairo"
}, },
{ {
"Alias": "Hazard", "Alias": "Hazard",
@ -725,6 +695,10 @@
{ {
"Alias": "Terminal", "Alias": "Terminal",
"Name": "mp_terminal_cls" "Name": "mp_terminal_cls"
},
{
"Alias": "Rust",
"Name": "mp_rust"
} }
] ]
}, },
@ -884,6 +858,596 @@
"Name": "zm_transit" "Name": "zm_transit"
} }
] ]
},
{
"Game": "IW6",
"Maps": [
{
"Alias": "Prision Break",
"Name": "mp_prisonbreak"
},
{
"Alias": "Octane",
"Name": "mp_dart"
},
{
"Alias": "Tremor",
"Name": "mp_lonestar"
},
{
"Alias": "Freight",
"Name": "mp_frag"
},
{
"Alias": "Whiteout",
"Name": "mp_snow"
},
{
"Alias": "Stormfront",
"Name": "mp_fahrenheit"
},
{
"Alias": "Siege",
"Name": "mp_hashima"
},
{
"Alias": "Warhawk",
"Name": "mp_warhawk"
},
{
"Alias": "Sovereign",
"Name": "mp_sovereign"
},
{
"Alias": "Overload",
"Name": "mp_zebra"
},
{
"Alias": "Stonehaven",
"Name": "mp_skeleton"
},
{
"Alias": "Chasm",
"Name": "mp_chasm"
},
{
"Alias": "Flooded",
"Name": "mp_flooded"
},
{
"Alias": "Strikezone",
"Name": "mp_strikezone"
},
{
"Alias": "Free Fall",
"Name": "mp_descent_new"
},
{
"Alias": "Unearthed",
"Name": "mp_dome_ns"
},
{
"Alias": "Collision",
"Name": "mp_ca_impact"
},
{
"Alias": "Behemoth",
"Name": "mp_ca_behemoth"
},
{
"Alias": "Ruins",
"Name": "mp_battery3"
},
{
"Alias": "Pharaoh",
"Name": "mp_dig"
},
{
"Alias": "Favela",
"Name": "mp_favela_iw6"
},
{
"Alias": "Mutiny",
"Name": "mp_pirate"
},
{
"Alias": "Departed",
"Name": "mp_zulu"
},
{
"Alias": "Dynasty",
"Name": "mp_conflict"
},
{
"Alias": "Goldrush",
"Name": "mp_mine"
},
{
"Alias": "Showtime",
"Name": "mp_shipment_ns"
},
{
"Alias": "Subzero",
"Name": "mp_zerosub"
},
{
"Alias": "Ignition",
"Name": "mp_boneyard_ns"
},
{
"Alias": "Containment",
"Name": "mp_ca_red_river"
},
{
"Alias": "Bayview",
"Name": "mp_ca_rumble"
},
{
"Alias": "Fog",
"Name": "mp_swamp"
},
{
"Alias": "Point of Contact",
"Name": "mp_alien_town"
},
{
"Alias": "Nightfall",
"Name": "mp_alien_armory"
},
{
"Alias": "Mayday",
"Name": "mp_alien_beacon"
},
{
"Alias": "Awakening",
"Name": "mp_alien_dlc3"
},
{
"Alias": "Exodus",
"Name": "mp_alien_last"
}
]
},
{
"Game": "SHG1",
"Maps": [
{
"Alias": "Ascend",
"Name": "mp_refraction"
},
{
"Alias": "Bio Lab",
"Name": "mp_lab2"
},
{
"Alias": "Comeback",
"Name": "mp_comeback"
},
{
"Alias": "Defender",
"Name": "mp_laser2"
},
{
"Alias": "Detroit",
"Name": "mp_detroit"
},
{
"Alias": "Greenband",
"Name": "mp_greenband"
},
{
"Alias": "Horizon",
"Name": "mp_levity"
},
{
"Alias": "Instinct",
"Name": "mp_instinct"
},
{
"Alias": "Recovery",
"Name": "mp_recovery"
},
{
"Alias": "Retreat",
"Name": "mp_venus"
},
{
"Alias": "Riot",
"Name": "mp_prison"
},
{
"Alias": "Solar",
"Name": "mp_solar"
},
{
"Alias": "Terrace",
"Name": "mp_terrace"
},
{
"Alias": "Atlas Gorge",
"Name": "mp_dam"
},
{
"Alias": "Chop Shop",
"Name": "mp_spark"
},
{
"Alias": "Climate",
"Name": "mp_climate_3"
},
{
"Alias": "Compound",
"Name": "mp_sector17"
},
{
"Alias": "Core",
"Name": "mp_lost"
},
{
"Alias": "Drift",
"Name": "mp_torqued"
},
{
"Alias": "Fracture",
"Name": "mp_fracture"
},
{
"Alias": "Kremlin",
"Name": "mp_kremlin"
},
{
"Alias": "Overload",
"Name": "mp_lair"
},
{
"Alias": "Parliament",
"Name": "mp_bigben2"
},
{
"Alias": "Perplex",
"Name": "mp_perplex_1"
},
{
"Alias": "Quarantine",
"Name": "mp_liberty"
},
{
"Alias": "Sideshow",
"Name": "mp_clowntown3"
},
{
"Alias": "Site 244",
"Name": "mp_blackbox"
},
{
"Alias": "Skyrise",
"Name": "mp_highrise2"
},
{
"Alias": "Swarn",
"Name": "mp_seoul2"
},
{
"Alias": "Urban",
"Name": "mp_urban"
}
]
},
{
"Game": "CSGO",
"Maps": [
{
"Name": "ar_baggage",
"Alias": "Baggage"
},
{
"Name": "ar_dizzy",
"Alias": "Dizzy"
},
{
"Name": "ar_lunacy",
"Alias": "Lunacy"
},
{
"Name": "ar_monastery",
"Alias": "Monastery"
},
{
"Name": "ar_shoots",
"Alias": "Shoots"
},
{
"Name": "cs_agency",
"Alias": "Agency"
},
{
"Name": "cs_assault",
"Alias": "Assault"
},
{
"Name": "cs_italy",
"Alias": "Italy"
},
{
"Name": "cs_militia",
"Alias": "Militia"
},
{
"Name": "cs_office",
"Alias": "Office"
},
{
"Name": "de_ancient",
"Alias": "Ancient"
},
{
"Name": "de_bank",
"Alias": "Bank"
},
{
"Name": "de_cache",
"Alias": "Cache"
},
{
"Name": "de_calavera",
"Alias": "Calavera"
},
{
"Name": "de_canals",
"Alias": "Canals"
},
{
"Name": "de_cbble",
"Alias": "Cobblestone"
},
{
"Name": "de_dust2",
"Alias": "Dust II"
},
{
"Name": "de_grind",
"Alias": "Grind"
},
{
"Name": "de_inferno",
"Alias": "Inferno"
},
{
"Name": "de_lake",
"Alias": "Lake"
},
{
"Name": "de_mirage",
"Alias": "Mirage"
},
{
"Name": "de_mocha",
"Alias": "Mocha"
},
{
"Name": "de_nuke",
"Alias": "Nuke"
},
{
"Name": "de_overpass",
"Alias": "Overpass"
},
{
"Name": "de_pitstop",
"Alias": "Pitstop"
},
{
"Name": "de_safehouse",
"Alias": "Safehouse"
},
{
"Name": "de_shortdust",
"Alias": "Shortdust"
},
{
"Name": "de_shortnuke",
"Alias": "Shortnuke"
},
{
"Name": "de_stmarc",
"Alias": "St. Marc"
},
{
"Name": "de_sugarcane",
"Alias": "Sugarcane"
},
{
"Name": "de_train",
"Alias": "Train"
},
{
"Name": "de_vertigo",
"Alias": "Vertigo"
},
{
"Name": "dz_blacksite",
"Alias": "Blacksite"
},
{
"Name": "dz_frostbite",
"Alias": "Frostbite"
},
{
"Name": "dz_sirocco",
"Alias": "Sirocco"
}
]
} }
] ],
"GameStrings": {
"IW4": {
"torso_upper": "Upper Torso",
"torso_lower": "Lower Torso",
"right_leg_upper": "Upper Right Leg",
"right_leg_lower": "Lower Right Leg",
"right_hand": "Right Hand",
"right_foot": "Right Foot",
"right_arm_upper": "Upper Right Arm",
"right_arm_lower": "Lower Right Arm",
"left_leg_upper": "Upper Left Leg",
"left_leg_lower": "Lower Left Leg",
"left_hand": "Left Hand",
"left_foot": "Left Foot",
"left_arm_upper": "Upper Left Arm",
"left_arm_lower": "Lower Left Arm",
"acog": "ACOG Sight",
"eotech": "Holographic Sight",
"fmj": "FMJ",
"gl": "Grenade Launcher",
"heartbeat": "Heartbeat Sensor",
"reflex": "Red Dot Sight",
"rof": "Rapid Fire",
"thermal": "Thermal",
"xmags": "Extended Mags",
"m4": "M4A1",
"m40a3": "M40A3",
"ak47": "AK-47",
"ak47classic": "AK-47 Classic",
"fn2000": "F2000",
"masada": "ACR",
"famas": "FAMAS",
"fal": "FAL",
"scar": "SCAR-H",
"tavor": "TAR-21",
"m16": "M16A4",
"mp5k": "MP5K",
"ump45": "UMP45",
"kriss": "Vector",
"uzi": "Mini-Uzi",
"rpd": "RPD",
"sa80": "L86 LSW",
"mg4": "MG4",
"aug": "AUG HBAR",
"cheytac": "Intervention",
"barrett": "Barrett .50cal",
"wa2000": "WA2000",
"m21": "M21 EBR",
"pp2000": "PP2000",
"glock": "G18",
"beretta": "M93 Raffica",
"tmp": "TMP",
"spas12": "SPAS-12",
"aa12": "AA-12",
"model1887": "Model 1887",
"usp": "USP .45",
"coltanaconda": ".44 Magnum",
"deserteagle": "Desert Eagle",
"deserteaglegold": "Desert Eagle Gold",
"at4": "AT4-HS",
"m79": "Thumper",
"rpg": "RPG-7",
"concussion": "Stun",
"throwingknife": "Throwing Knife",
"ffar": "Airstrike",
"pavelow": "Pave Low",
"cobra": "Attack Helicopter",
"ac130": "AC-130",
"remotemissile": "Predator Missile",
"artillery": "Precision Airstrike",
"player": "",
"attach": ""
},
"IW3": {
"torso_upper": "Upper Torso",
"torso_lower": "Lower Torso",
"right_leg_upper": "Upper Right Leg",
"right_leg_lower": "Lower Right Leg",
"right_hand": "Right Hand",
"right_foot": "Right Foot",
"right_arm_upper": "Upper Right Arm",
"right_arm_lower": "Lower Right Arm",
"left_leg_upper": "Upper Left Leg",
"left_leg_lower": "Lower Left Leg",
"left_hand": "Left Hand",
"left_foot": "Left Foot",
"left_arm_upper": "Upper Left Arm",
"left_arm_lower": "Lower Left Arm",
"acog": "ACOG Sight",
"gl": "Grenade Launcher",
"reflex": "Red Dot Sight",
"grip": "Grip",
"m4": "M4 Carbine",
"m40a3": "M40A3",
"ak47": "AK-47",
"ak74u": "AK-74u",
"rpg": "RPG-7",
"deserteagle": "Desert Eagle",
"deserteaglegold": "Desert Eagle Gold",
"m16": "M16A4",
"g36c": "G36C",
"uzi": "Mini-Uzi",
"m60e4": "M60E4",
"mp5": "MP5",
"barrett": "Barrett .50cal",
"mp44": "MP44",
"remington700": "R700",
"rpd": "RDP",
"saw": " M249 SAW",
"usp": "USP .45",
"winchester1200": "W1200",
"concussion": "Stun",
"melee": "Knife",
"Frag" : "Grenade",
"airstrike": "Airstrike",
"helicopter": "Attack Helicopter",
"player": "",
"attach": ""
},
"T4": {
"torso_upper": "Upper Torso",
"torso_lower": "Lower Torso",
"right_leg_upper": "Upper Right Leg",
"right_leg_lower": "Lower Right Leg",
"right_hand": "Right Hand",
"right_foot": "Right Foot",
"right_arm_upper": "Upper Right Arm",
"right_arm_lower": "Lower Right Arm",
"left_leg_upper": "Upper Left Leg",
"left_leg_lower": "Lower Left Leg",
"left_hand": "Left Hand",
"left_foot": "Left Foot",
"left_arm_upper": "Upper Left Arm",
"left_arm_lower": "Lower Left Arm",
"gl": "Rifle Grenade",
"bigammo": "Round Drum",
"scoped": "Sniper Scope",
"telescopic": "Telescopic Sight",
"aperture": "Aperture Sight",
"flash": "Flash Hider",
"silenced": "Silencer",
"molotov": "Molotov Cocktail",
"sticky": "N° 74 ST",
"m2": "M2 Flamethrower",
"artillery": "Artillery Strike",
"dog": "Attack Dogs",
"colt": "Colt M1911",
"357magnum": ".357 Magnum",
"walther": "Walther P38",
"tokarev": "Tokarev TT-33",
"shotgun": "M1897 Trench Gun",
"doublebarreledshotgun": "Double-Barreled Shotgun",
"mp40": "MP40",
"type100smg": "Type 100",
"ppsh": "PPSh-41",
"svt40": "SVT-40",
"gewehr43": "Gewehr 43",
"m1garand": "M1 Garand",
"stg44": "STG-44",
"m1carbine": "M1A1 Carbine",
"type99lmg": "Type 99",
"bar": "BAR",
"dp28": "DP-28",
"mg42": "MG42",
"fg42": "FG42",
"30cal": "Browning M1919",
"type99rifle": "Arisaka",
"mosinrifle": "Mosin-Nagant",
"ptrs41":"PTRS-41"
}
}
} }

View File

@ -5,7 +5,10 @@ using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Data.Models;
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
{ {
@ -14,6 +17,8 @@ namespace IW4MAdmin.Application.EventParsers
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations; private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
private readonly Dictionary<ParserRegex, GameEvent.EventType> _regexMap;
private readonly Dictionary<string, GameEvent.EventType> _eventTypeMap;
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig) public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig)
{ {
@ -75,7 +80,28 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12); Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13); Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.MapChange.Pattern = @".*InitGame.*";
Configuration.MapEnd.Pattern = @".*(?:ExitLevel|ShutdownGame).*";
Configuration.Time.Pattern = @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )"; Configuration.Time.Pattern = @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )";
_regexMap = new Dictionary<ParserRegex, GameEvent.EventType>
{
{Configuration.Say, GameEvent.EventType.Say},
{Configuration.Kill, GameEvent.EventType.Kill},
{Configuration.MapChange, GameEvent.EventType.MapChange},
{Configuration.MapEnd, GameEvent.EventType.MapEnd}
};
_eventTypeMap = new Dictionary<string, GameEvent.EventType>
{
{"say", GameEvent.EventType.Say},
{"sayteam", GameEvent.EventType.Say},
{"K", GameEvent.EventType.Kill},
{"D", GameEvent.EventType.Damage},
{"J", GameEvent.EventType.PreConnect},
{"Q", GameEvent.EventType.PreDisconnect},
};
} }
public IEventParserConfiguration Configuration { get; set; } public IEventParserConfiguration Configuration { get; set; }
@ -88,47 +114,79 @@ namespace IW4MAdmin.Application.EventParsers
public string Name { get; set; } = "Call of Duty"; public string Name { get; set; } = "Call of Duty";
private (GameEvent.EventType type, string eventKey) GetEventTypeFromLine(string logLine)
{
var lineSplit = logLine.Split(';');
if (lineSplit.Length > 1)
{
var type = lineSplit[0];
return _eventTypeMap.ContainsKey(type) ? (_eventTypeMap[type], type): (GameEvent.EventType.Unknown, lineSplit[0]);
}
foreach (var (key, value) in _regexMap)
{
var result = key.PatternMatcher.Match(logLine);
if (result.Success)
{
return (value, null);
}
}
return (GameEvent.EventType.Unknown, null);
}
public virtual GameEvent GenerateGameEvent(string logLine) public virtual GameEvent GenerateGameEvent(string logLine)
{ {
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine); var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
int gameTime = 0; var gameTime = 0L;
if (timeMatch.Success) if (timeMatch.Success)
{ {
gameTime = timeMatch if (timeMatch.Values[0].Contains(":"))
.Values {
.Skip(2) gameTime = timeMatch
// this converts the timestamp into seconds passed .Values
.Select((_value, index) => int.Parse(_value.ToString()) * (index == 0 ? 60 : 1)) .Skip(2)
.Sum(); // this converts the timestamp into seconds passed
.Select((value, index) => long.Parse(value.ToString()) * (index == 0 ? 60 : 1))
.Sum();
}
else
{
gameTime = long.Parse(timeMatch.Values[0]);
}
// we want to strip the time from the log line // we want to strip the time from the log line
logLine = logLine.Substring(timeMatch.Values.First().Length); logLine = logLine.Substring(timeMatch.Values.First().Length).Trim();
} }
string[] lineSplit = logLine.Split(';'); var eventParseResult = GetEventTypeFromLine(logLine);
string eventType = lineSplit[0]; var eventType = eventParseResult.type;
_logger.LogDebug(logLine);
if (eventType == "say" || eventType == "sayteam") if (eventType == GameEvent.EventType.Say)
{ {
var matchResult = Configuration.Say.PatternMatcher.Match(logLine); var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
if (matchResult.Success) if (matchResult.Success)
{ {
string message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]] var message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.ToString()
.Replace("\x15", "") .Replace("\x15", "")
.Trim(); .Trim();
if (message.Length > 0) if (message.Length > 0)
{ {
string originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString(); var originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
string originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(); var originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]];
long originId = originIdString.IsBotGuid() ? var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() : originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); var clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix)) if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
{ {
@ -160,26 +218,26 @@ namespace IW4MAdmin.Application.EventParsers
} }
} }
if (eventType == "K") if (eventType == GameEvent.EventType.Kill)
{ {
var match = Configuration.Kill.PatternMatcher.Match(logLine); var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (match.Success) if (match.Success)
{ {
string originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString(); var originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
string targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString(); var targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
string originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(); var originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]];
string targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]].ToString(); var targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]];
long originId = originIdString.IsBotGuid() ? var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() : originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
long targetId = targetIdString.IsBotGuid() ? var targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() : targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
int originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); var originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]); var targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent() return new GameEvent()
{ {
@ -194,26 +252,26 @@ namespace IW4MAdmin.Application.EventParsers
} }
} }
if (eventType == "D") if (eventType == GameEvent.EventType.Damage)
{ {
var match = Configuration.Damage.PatternMatcher.Match(logLine); var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (match.Success) if (match.Success)
{ {
string originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString(); var originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
string targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString(); var targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
string originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(); var originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]];
string targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]].ToString(); var targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]];
long originId = originIdString.IsBotGuid() ? var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() : originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
long targetId = targetIdString.IsBotGuid() ? var targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() : targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID); targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
int originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]); var originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]); var targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent() return new GameEvent()
{ {
@ -228,16 +286,16 @@ namespace IW4MAdmin.Application.EventParsers
} }
} }
if (eventType == "J") if (eventType == GameEvent.EventType.PreConnect)
{ {
var match = Configuration.Join.PatternMatcher.Match(logLine); var match = Configuration.Join.PatternMatcher.Match(logLine);
if (match.Success) if (match.Success)
{ {
string originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString(); var originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
string originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(); var originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]];
long networkId = originIdString.IsBotGuid() ? var networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() : originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
@ -249,12 +307,13 @@ namespace IW4MAdmin.Application.EventParsers
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias()
{ {
Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine(), Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine(),
}, },
NetworkId = networkId, NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()), ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]),
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,
@ -263,16 +322,16 @@ namespace IW4MAdmin.Application.EventParsers
} }
} }
if (eventType == "Q") if (eventType == GameEvent.EventType.PreDisconnect)
{ {
var match = Configuration.Quit.PatternMatcher.Match(logLine); var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (match.Success) if (match.Success)
{ {
string originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString(); var originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
string originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(); var originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]];
long networkId = originIdString.IsBotGuid() ? var networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() : originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
@ -284,10 +343,10 @@ namespace IW4MAdmin.Application.EventParsers
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias()
{ {
Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine() Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].TrimNewLine()
}, },
NetworkId = networkId, NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()), ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]),
State = EFClient.ClientState.Disconnecting State = EFClient.ClientState.Disconnecting
}, },
RequiredEntity = GameEvent.EventRequiredEntity.None, RequiredEntity = GameEvent.EventRequiredEntity.None,
@ -298,7 +357,7 @@ namespace IW4MAdmin.Application.EventParsers
} }
} }
if (eventType.Contains("ExitLevel")) if (eventType == GameEvent.EventType.MapEnd)
{ {
return new GameEvent() return new GameEvent()
{ {
@ -312,9 +371,9 @@ namespace IW4MAdmin.Application.EventParsers
}; };
} }
if (eventType.Contains("InitGame")) if (eventType == GameEvent.EventType.MapChange)
{ {
string dump = eventType.Replace("InitGame: ", ""); var dump = logLine.Replace("InitGame: ", "");
return new GameEvent() return new GameEvent()
{ {
@ -329,26 +388,37 @@ namespace IW4MAdmin.Application.EventParsers
}; };
} }
if (_customEventRegistrations.ContainsKey(eventType)) if (eventParseResult.eventKey == null || !_customEventRegistrations.ContainsKey(eventParseResult.eventKey))
{ {
var eventModifier = _customEventRegistrations[eventType]; return new GameEvent()
try
{ {
return eventModifier.Item2(logLine, Configuration, new GameEvent() Type = GameEvent.EventType.Unknown,
{ Data = logLine,
Type = GameEvent.EventType.Other, Origin = Utilities.IW4MAdminClient(),
Data = logLine, Target = Utilities.IW4MAdminClient(),
Subtype = eventModifier.Item1, RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime, GameTime = gameTime,
Source = GameEvent.EventSource.Log Source = GameEvent.EventSource.Log
}); };
} }
catch (Exception e) var eventModifier = _customEventRegistrations[eventParseResult.eventKey];
try
{
return eventModifier.Item2(logLine, Configuration, new GameEvent()
{ {
_logger.WriteWarning($"Could not handle custom event generation - {e.GetExceptionInfo()}"); Type = GameEvent.EventType.Other,
} Data = logLine,
Subtype = eventModifier.Item1,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
});
}
catch (Exception e)
{
_logger.LogError(e, "Could not handle custom event generation");
} }
return new GameEvent() return new GameEvent()

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

@ -1,5 +1,6 @@
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Globalization; using System.Globalization;
using SharedLibraryCore;
namespace IW4MAdmin.Application.EventParsers namespace IW4MAdmin.Application.EventParsers
{ {
@ -17,6 +18,8 @@ namespace IW4MAdmin.Application.EventParsers
public ParserRegex Damage { get; set; } public ParserRegex Damage { get; set; }
public ParserRegex Action { get; set; } public ParserRegex Action { get; set; }
public ParserRegex Time { get; set; } public ParserRegex Time { get; set; }
public ParserRegex MapChange { get; set; }
public ParserRegex MapEnd { get; set; }
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber; public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public DynamicEventParserConfiguration(IParserRegexFactory parserRegexFactory) public DynamicEventParserConfiguration(IParserRegexFactory parserRegexFactory)
@ -28,6 +31,8 @@ namespace IW4MAdmin.Application.EventParsers
Damage = parserRegexFactory.CreateParserRegex(); Damage = parserRegexFactory.CreateParserRegex();
Action = parserRegexFactory.CreateParserRegex(); Action = parserRegexFactory.CreateParserRegex();
Time = parserRegexFactory.CreateParserRegex(); Time = parserRegexFactory.CreateParserRegex();
MapChange = parserRegexFactory.CreateParserRegex();
MapEnd = parserRegexFactory.CreateParserRegex();
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,7 +1,10 @@
using SharedLibraryCore; using System;
using Data.Abstractions;
using Data.Models.Server;
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 +14,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 +39,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>(), _serviceProvider.GetRequiredService<ILookupCache<EFServer>>());
} }
} }
} }

View File

@ -1,6 +1,11 @@
using IW4MAdmin.Application.RCon; using System;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Text; using System.Text;
using Integrations.Cod;
using Integrations.Source;
using Integrations.Source.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -9,16 +14,16 @@ namespace IW4MAdmin.Application.Factories
/// </summary> /// </summary>
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 IServiceProvider _serviceProvider;
/// <summary> /// <summary>
/// Base constructor /// Base constructor
/// </summary> /// </summary>
/// <param name="logger"></param> /// <param name="logger"></param>
public RConConnectionFactory(ILogger logger) public RConConnectionFactory(IServiceProvider serviceProvider)
{ {
_logger = logger; _serviceProvider = serviceProvider;
} }
/// <summary> /// <summary>
@ -28,9 +33,16 @@ namespace IW4MAdmin.Application.Factories
/// <param name="port">port of the server</param> /// <param name="port">port of the server</param>
/// <param name="password">rcon password of the server</param> /// <param name="password">rcon password of the server</param>
/// <returns></returns> /// <returns></returns>
public IRConConnection CreateConnection(string ipAddress, int port, string password) public IRConConnection CreateConnection(string ipAddress, int port, string password, string rconEngine)
{ {
return new RConConnection(ipAddress, port, password, _logger, gameEncoding); return rconEngine switch
{
"COD" => new CodRConConnection(ipAddress, port, password,
_serviceProvider.GetRequiredService<ILogger<CodRConConnection>>(), GameEncoding),
"Source" => new SourceRConConnection(_serviceProvider.GetRequiredService<ILogger<SourceRConConnection>>(),
_serviceProvider.GetRequiredService<IRConClientFactory>(), ipAddress, port, password),
_ => throw new ArgumentException($"No supported RCon engine available for '{rconEngine}'")
};
} }
} }
} }

View File

@ -6,7 +6,9 @@ using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using static SharedLibraryCore.Database.Models.EFClient; using Data.Models.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -15,26 +17,30 @@ 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<EFClient.Permission>(permission);
var argsArray = args.Select(_arg => new CommandArgument var argsArray = args.Select(_arg => new CommandArgument
{ {
Name = _arg.Item1, Name = _arg.Item1,
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");
} }
} }
@ -55,18 +63,20 @@ namespace IW4MAdmin.Application.Localization
{ {
var localizationContents = File.ReadAllText(filePath, Encoding.UTF8); var localizationContents = File.ReadAllText(filePath, Encoding.UTF8);
var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents); var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents);
if (eachLocalizationFile == null)
{
continue;
}
foreach (var item in eachLocalizationFile.LocalizationIndex.Set) foreach (var item in eachLocalizationFile.LocalizationIndex.Set)
{ {
if (!localizationDict.TryAdd(item.Key, item.Value)) if (!localizationDict.TryAdd(item.Key, item.Value))
{ {
Program.ServerManager.GetLogger(0).WriteError($"Could not add locale string {item.Key} to localization"); logger.LogError("Could not add locale string {key} to localization", item.Key);
} }
} }
} }
string localizationFile = $"{Path.Join(Utilities.OperatingDirectory, "Localization")}{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocale}-{currentLocale.ToUpper()}.json";
Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict) Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict)
{ {
LocalizationName = currentLocale, LocalizationName = currentLocale,

View File

@ -1,7 +1,6 @@
using IW4MAdmin.Application.API.Master; using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Factories; using IW4MAdmin.Application.Factories;
using IW4MAdmin.Application.Helpers;
using IW4MAdmin.Application.Meta; using IW4MAdmin.Application.Meta;
using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
@ -18,12 +17,23 @@ using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Repositories; using SharedLibraryCore.Repositories;
using SharedLibraryCore.Services; using SharedLibraryCore.Services;
using Stats.Dtos; using Stats.Dtos;
using StatsWeb;
using System; using System;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Data.Helpers;
using Integrations.Source.Extensions;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Localization;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
using IW4MAdmin.Plugins.Stats.Client;
using Stats.Client.Abstractions;
using Stats.Client;
using Stats.Helpers;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
@ -65,7 +75,10 @@ namespace IW4MAdmin.Application
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e) private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
{ {
ServerManager?.Stop(); ServerManager?.Stop();
await ApplicationTask; if (ApplicationTask != null)
{
await ApplicationTask;
}
} }
/// <summary> /// <summary>
@ -74,31 +87,40 @@ namespace IW4MAdmin.Application
/// <returns></returns> /// <returns></returns>
private static async Task LaunchAsync(string[] args) private static async Task LaunchAsync(string[] args)
{ {
restart: restart:
ITranslationLookup translationLookup = null; ITranslationLookup translationLookup = null;
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
Utilities.DefaultLogger = logger;
IServiceCollection services = null;
logger.LogInformation("Begin IW4MAdmin startup. Version is {version} {@args}", Version, args);
try try
{ {
// do any needed housekeeping file/folder migrations // do any needed housekeeping file/folder migrations
ConfigurationMigration.MoveConfigFolder10518(null); ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories(); ConfigurationMigration.CheckDirectories();
ConfigurationMigration.RemoveObsoletePlugins20210322();
var services = ConfigureServices(args); logger.LogDebug("Configuring services...");
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
string exitMessage = translationLookup == null ? "Press enter to exit..." : translationLookup["MANAGER_EXIT"]; ? "Failed to initialize IW4MAdmin"
: translationLookup["MANAGER_INIT_FAIL"];
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)
@ -110,7 +132,8 @@ namespace IW4MAdmin.Application
{ {
if (translationLookup != null) if (translationLookup != null)
{ {
Console.WriteLine(translationLookup[configException.Message].FormatExt(configException.ConfigurationFileName)); Console.WriteLine(translationLookup[configException.Message]
.FormatExt(configException.ConfigurationFileName));
} }
foreach (string error in configException.Errors) foreach (string error in configException.Errors)
@ -131,13 +154,16 @@ namespace IW4MAdmin.Application
try try
{ {
ApplicationTask = RunApplicationTasksAsync(); ApplicationTask = RunApplicationTasksAsync(logger, services);
await ApplicationTask; await ApplicationTask;
} }
catch (Exception e) catch (Exception e)
{ {
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"]; logger.LogCritical(e, "Failed to launch IW4MAdmin");
string failMessage = translationLookup == null
? "Failed to launch IW4MAdmin"
: translationLookup["MANAGER_INIT_FAIL"];
Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}"); Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}");
} }
@ -153,27 +179,30 @@ namespace IW4MAdmin.Application
/// runs the core application tasks /// runs the core application tasks
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private static async Task RunApplicationTasksAsync() private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
{ {
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ? var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront
WebfrontCore.Program.Init(ServerManager, serviceProvider, ServerManager.CancellationToken) : ? WebfrontCore.Program.Init(ServerManager, serviceProvider, services, ServerManager.CancellationToken)
Task.CompletedTask; : Task.CompletedTask;
// we want to run this one on a manual thread instead of letting the thread pool handle it, // we want to run this one on a manual thread instead of letting the thread pool handle it,
// because we can't exit early from waiting on console input, and it prevents us from restarting // because we can't exit early from waiting on console input, and it prevents us from restarting
var inputThread = new Thread(async () => await ReadConsoleInput()); var inputThread = new Thread(async () => await ReadConsoleInput(logger));
inputThread.Start(); inputThread.Start();
var tasks = new[] var tasks = new[]
{ {
ServerManager.Start(), ServerManager.Start(),
webfrontTask, webfrontTask,
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.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
} }
@ -181,11 +210,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;
} }
@ -218,24 +247,129 @@ namespace IW4MAdmin.Application
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ } {
}
} }
private static IServiceCollection HandlePluginRegistration(ApplicationConfiguration appConfig,
IServiceCollection serviceCollection,
IMasterApi masterApi)
{
var defaultLogger = BuildDefaultLogger<Program>(appConfig);
var pluginServiceProvider = new ServiceCollection()
.AddBaseLogger(appConfig)
.AddSingleton(appConfig)
.AddSingleton(masterApi)
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
.AddSingleton<IPluginImporter, PluginImporter>()
.BuildServiceProvider();
var pluginImporter = pluginServiceProvider.GetRequiredService<IPluginImporter>();
// we need to register the rest client with regular collection
serviceCollection.AddSingleton(masterApi);
// register the native commands
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
.Where(_command => _command.BaseType == typeof(Command)))
{
defaultLogger.LogDebug("Registered native command type {name}", commandType.Name);
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
// register the plugin implementations
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in plugins)
{
defaultLogger.LogDebug("Registered plugin type {name}", pluginType.FullName);
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
}
// register the plugin commands
foreach (var commandType in commands)
{
defaultLogger.LogDebug("Registered plugin command type {name}", commandType.FullName);
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
foreach (var configurationType in configurations)
{
defaultLogger.LogDebug("Registered plugin config type {name}", configurationType.Name);
var configInstance = (IBaseConfiguration) Activator.CreateInstance(configurationType);
var handlerType = typeof(BaseConfigurationHandler<>).MakeGenericType(configurationType);
var handlerInstance = Activator.CreateInstance(handlerType, new[] {configInstance.Name()});
var genericInterfaceType = typeof(IConfigurationHandler<>).MakeGenericType(configurationType);
serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
}
// register any script plugins
foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins())
{
serviceCollection.AddSingleton(scriptPlugin);
}
// register any eventable types
foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))
.Union(plugins.SelectMany(_asm => _asm.Assembly.GetTypes())
.Distinct()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
{
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
serviceCollection.AddSingleton(instance);
}
return serviceCollection;
}
/// <summary> /// <summary>
/// Configures the dependency injection services /// Configures the dependency injection services
/// </summary> /// </summary>
private static IServiceCollection ConfigureServices(string[] args) private static IServiceCollection ConfigureServices(string[] args)
{ {
var defaultLogger = new Logger("IW4MAdmin-Manager"); // setup the static resources (config/master api/translations)
var pluginImporter = new PluginImporter(defaultLogger);
var serviceCollection = new ServiceCollection(); var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection) var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
.AddSingleton(new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings") as IConfigurationHandler<ApplicationConfiguration>) var appConfig = appConfigHandler.Configuration();
.AddSingleton(new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as IConfigurationHandler<CommandConfiguration>) var masterUri = Utilities.IsDevelopment
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration() ?? new ApplicationConfiguration()) ? new Uri("http://127.0.0.1:8080")
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>().Configuration() ?? new CommandConfiguration()) : appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
.AddSingleton<ILogger>(_serviceProvider => defaultLogger) 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();
}
// register override level names
foreach (var (key, value) in appConfig.OverridePermissionLevelNames)
{
if (!Utilities.PermissionLevelOverrides.ContainsKey(key))
{
Utilities.PermissionLevelOverrides.Add(key, value);
}
}
// build the dependency list
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
serviceCollection
.AddBaseLogger(appConfig)
.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection)
.AddSingleton<IConfigurationHandler<DefaultSettings>, BaseConfigurationHandler<DefaultSettings>>()
.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<IPluginImporter, PluginImporter>()
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>() .AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
.AddSingleton<IRConConnectionFactory, RConConnectionFactory>() .AddSingleton<IRConConnectionFactory, RConConnectionFactory>()
@ -248,25 +382,32 @@ namespace IW4MAdmin.Application
.AddSingleton<IAuditInformationRepository, AuditInformationRepository>() .AddSingleton<IAuditInformationRepository, AuditInformationRepository>()
.AddSingleton<IEntityService<EFClient>, ClientService>() .AddSingleton<IEntityService<EFClient>, ClientService>()
.AddSingleton<IMetaService, MetaService>() .AddSingleton<IMetaService, MetaService>()
.AddSingleton<ClientService>()
.AddSingleton<PenaltyService>()
.AddSingleton<ChangeHistoryService>()
.AddSingleton<IMetaRegistration, MetaRegistration>() .AddSingleton<IMetaRegistration, MetaRegistration>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>, ReceivedPenaltyResourceQueryHelper>() .AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>, AdministeredPenaltyResourceQueryHelper>() .AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>,
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>, UpdatedAliasResourceQueryHelper>() ReceivedPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>,
AdministeredPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>,
UpdatedAliasResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>() .AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>() .AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton(_serviceProvider => .AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
{ .AddSingleton<IMasterCommunication, MasterCommunication>()
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<IManager, ApplicationManager>()
.AddSingleton(_serviceProvider => RestClient .AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>()
.For<IMasterApi>(Utilities.IsDevelopment ? new Uri("http://127.0.0.1:8080") : _serviceProvider .AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>()
.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration()?.MasterUrl ?? .AddSingleton<IClientStatisticCalculator, HitCalculator>()
new ApplicationConfiguration().MasterUrl)) .AddSingleton<IServerDistributionCalculator, ServerDistributionCalculator>()
.AddSingleton<IMasterCommunication, MasterCommunication>(); .AddSingleton<IWeaponNameParser, WeaponNameParser>()
.AddSingleton<IHitInfoBuilder, HitInfoBuilder>()
.AddSingleton(typeof(ILookupCache<>), typeof(LookupCache<>))
.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>))
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);
if (args.Contains("serialevents")) if (args.Contains("serialevents"))
{ {
@ -277,48 +418,18 @@ namespace IW4MAdmin.Application
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>(); serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
} }
// register the native commands serviceCollection.AddSource();
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
.Where(_command => _command.BaseType == typeof(Command)))
{
defaultLogger.WriteInfo($"Registered native command type {commandType.Name}");
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
// register the plugin implementations
var pluginImplementations = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in pluginImplementations.Item1)
{
defaultLogger.WriteInfo($"Registered plugin type {pluginType.FullName}");
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
}
// register the plugin commands
foreach (var commandType in pluginImplementations.Item2)
{
defaultLogger.WriteInfo($"Registered plugin command type {commandType.FullName}");
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
// register any script plugins
foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins())
{
serviceCollection.AddSingleton(scriptPlugin);
}
// register any eventable types
foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))
.Union(pluginImplementations
.Item1.SelectMany(_asm => _asm.Assembly.GetTypes())
.Distinct()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
{
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
serviceCollection.AddSingleton(instance);
}
return serviceCollection; 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,14 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database.Models; using Microsoft.Extensions.Logging;
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 +21,7 @@ namespace IW4MAdmin.Application.Meta
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
public AdministeredPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory) public AdministeredPenaltyResourceQueryHelper(ILogger<AdministeredPenaltyResourceQueryHelper> logger, IDatabaseContextFactory contextFactory)
{ {
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
@ -26,7 +29,7 @@ namespace IW4MAdmin.Application.Meta
public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query) public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{ {
using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var iqPenalties = ctx.Penalties.AsNoTracking() var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => query.ClientId == _penalty.PunisherId) .Where(_penalty => query.ClientId == _penalty.PunisherId)

View File

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

View File

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

View File

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

View File

@ -1,11 +1,8 @@
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using ILogger = Microsoft.Extensions.Logging.ILogger;
using System.Text.RegularExpressions;
namespace IW4MAdmin.Application.Migration namespace IW4MAdmin.Application.Migration
{ {
@ -56,7 +53,6 @@ namespace IW4MAdmin.Application.Migration
if (!Directory.Exists(configDirectory)) if (!Directory.Exists(configDirectory))
{ {
log?.WriteDebug($"Creating directory for configs {configDirectory}");
Directory.CreateDirectory(configDirectory); Directory.CreateDirectory(configDirectory);
} }
@ -66,7 +62,6 @@ namespace IW4MAdmin.Application.Migration
foreach (var configFile in configurationFiles) foreach (var configFile in configurationFiles)
{ {
log?.WriteDebug($"Moving config file {configFile}");
string destinationPath = Path.Join("Configuration", configFile); string destinationPath = Path.Join("Configuration", configFile);
if (!File.Exists(destinationPath)) if (!File.Exists(destinationPath))
{ {
@ -77,7 +72,6 @@ namespace IW4MAdmin.Application.Migration
if (!File.Exists(Path.Join("Database", "Database.db")) && if (!File.Exists(Path.Join("Database", "Database.db")) &&
File.Exists("Database.db")) File.Exists("Database.db"))
{ {
log?.WriteDebug("Moving database file");
File.Move("Database.db", Path.Join("Database", "Database.db")); File.Move("Database.db", Path.Join("Database", "Database.db"));
} }
} }
@ -91,5 +85,20 @@ namespace IW4MAdmin.Application.Migration
config.ManualLogPath = null; config.ManualLogPath = null;
} }
} }
public static void RemoveObsoletePlugins20210322()
{
var files = new[] {"StatsWeb.dll", "StatsWeb.Views.dll"};
foreach (var file in files)
{
var path = Path.Join(Utilities.OperatingDirectory, "Plugins", file);
if (File.Exists(path))
{
File.Delete(path);
}
}
}
} }
} }

View File

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

View File

@ -22,6 +22,11 @@ namespace IW4MAdmin.Application.Misc
Build(); Build();
} }
public BaseConfigurationHandler() : this(typeof(T).Name)
{
}
public string FileName { get; } public string FileName { get; }
public void Build() public void Build()

View File

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

View File

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

View File

@ -41,5 +41,11 @@ namespace IW4MAdmin.Application.Misc
/// indicates if running on windows /// indicates if running on windows
/// </summary> /// </summary>
public bool IsWindows { get; set; } = true; public bool IsWindows { get; set; } = true;
/// <summary>
/// indicates that the game does not log to the mods folder (when mod is loaded),
/// but rather always to the fs_basegame directory
/// </summary>
public bool IsOneLog { get; set; }
} }
} }

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,10 @@ 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 Data.Abstractions;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using Data.Models;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -20,14 +24,14 @@ 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>>();
_contextFactory = contextFactory; _contextFactory = contextFactory;
} }
public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client) public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client, EFMeta linkedMeta = null)
{ {
// this seems to happen if the client disconnects before they've had time to authenticate and be added // this seems to happen if the client disconnects before they've had time to authenticate and be added
if (client.ClientId < 1) if (client.ClientId < 1)
@ -35,7 +39,7 @@ namespace IW4MAdmin.Application.Misc
return; return;
} }
using var ctx = _contextFactory.CreateContext(); await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta var existingMeta = await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey) .Where(_meta => _meta.Key == metaKey)
@ -46,6 +50,7 @@ namespace IW4MAdmin.Application.Misc
{ {
existingMeta.Value = metaValue; existingMeta.Value = metaValue;
existingMeta.Updated = DateTime.UtcNow; existingMeta.Updated = DateTime.UtcNow;
existingMeta.LinkedMetaId = linkedMeta?.MetaId;
} }
else else
@ -55,16 +60,101 @@ namespace IW4MAdmin.Application.Misc
ClientId = client.ClientId, ClientId = client.ClientId,
Created = DateTime.UtcNow, Created = DateTime.UtcNow,
Key = metaKey, Key = metaKey,
Value = metaValue Value = metaValue,
LinkedMetaId = linkedMeta?.MetaId
}); });
} }
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
public async Task AddPersistentMeta(string metaKey, string metaValue)
{
await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.ToListAsync();
var matchValues = existingMeta
.Where(meta => meta.Value == metaValue)
.ToArray();
if (matchValues.Any())
{
foreach (var meta in matchValues)
{
_logger.LogDebug("Updating existing meta with key {key} and id {id}", meta.Key, meta.MetaId);
meta.Value = metaValue;
meta.Updated = DateTime.UtcNow;
}
await ctx.SaveChangesAsync();
}
else
{
_logger.LogDebug("Adding new meta with key {key}", metaKey);
ctx.EFMeta.Add(new EFMeta()
{
Created = DateTime.UtcNow,
Key = metaKey,
Value = metaValue
});
await ctx.SaveChangesAsync();
}
}
public async Task RemovePersistentMeta(string metaKey, EFClient client)
{
await using var context = _contextFactory.CreateContext();
var existingMeta = await context.EFMeta
.FirstOrDefaultAsync(meta => meta.Key == metaKey && meta.ClientId == client.ClientId);
if (existingMeta == null)
{
_logger.LogDebug("No meta with key {key} found for client id {id}", metaKey, client.ClientId);
return;
}
_logger.LogDebug("Removing meta for key {key} with id {id}", metaKey, existingMeta.MetaId);
context.EFMeta.Remove(existingMeta);
await context.SaveChangesAsync();
}
public async Task RemovePersistentMeta(string metaKey, string metaValue = null)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
var existingMeta = await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.ToListAsync();
if (metaValue == null)
{
_logger.LogDebug("Removing all meta for key {key} with ids [{ids}] ", metaKey, string.Join(", ", existingMeta.Select(meta => meta.MetaId)));
existingMeta.ForEach(meta => context.Remove(existingMeta));
await context.SaveChangesAsync();
return;
}
var foundMeta = existingMeta.FirstOrDefault(meta => meta.Value == metaValue);
if (foundMeta != null)
{
_logger.LogDebug("Removing meta for key {key} with id {id}", metaKey, foundMeta.MetaId);
context.Remove(foundMeta);
await context.SaveChangesAsync();
}
}
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client) public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
{ {
using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
return await ctx.EFMeta return await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey) .Where(_meta => _meta.Key == metaKey)
@ -74,11 +164,34 @@ namespace IW4MAdmin.Application.Misc
MetaId = _meta.MetaId, MetaId = _meta.MetaId,
Key = _meta.Key, Key = _meta.Key,
ClientId = _meta.ClientId, ClientId = _meta.ClientId,
Value = _meta.Value Value = _meta.Value,
LinkedMetaId = _meta.LinkedMetaId,
LinkedMeta = _meta.LinkedMetaId != null ? new EFMeta()
{
MetaId = _meta.LinkedMeta.MetaId,
Key = _meta.LinkedMeta.Key,
Value = _meta.LinkedMeta.Value
} : null
}) })
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
public async Task<IEnumerable<EFMeta>> GetPersistentMeta(string metaKey)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
return await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.Select(meta => new EFMeta
{
MetaId = meta.MetaId,
Key = meta.Key,
ClientId = meta.ClientId,
Value = meta.Value,
})
.ToListAsync();
}
public void AddRuntimeMeta<T, V>(MetaType metaKey, Func<T, Task<IEnumerable<V>>> metaAction) where T : PaginationRequest where V : IClientMeta public void AddRuntimeMeta<T, V>(MetaType metaKey, Func<T, Task<IEnumerable<V>>> metaAction) where T : PaginationRequest where V : IClientMeta
{ {
if (!_metaActions.ContainsKey(metaKey)) if (!_metaActions.ContainsKey(metaKey))

View File

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

View File

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

View File

@ -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,10 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models.Client;
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 +17,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, EFClient.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);
} }
@ -112,8 +118,30 @@ namespace IW4MAdmin.Application.Misc
}) })
.CatchClrExceptions()); .CatchClrExceptions());
_scriptEngine.Execute(script); try
{
_scriptEngine.Execute(script);
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} at {@locationInfo}",
nameof(Initialize), _fileName, ex.Location);
throw new PluginException($"A JavaScript parsing error occured while initializing script plugin");
}
catch (Exception e)
{
_logger.LogError(e,
"Encountered unexpected error while running {methodName} for script plugin {plugin}",
nameof(Initialize), _fileName);
throw new PluginException($"An unexpected error occured while initialization script plugin");
}
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization); _scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject(); dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
Author = pluginObject.author; Author = pluginObject.author;
@ -128,7 +156,7 @@ namespace IW4MAdmin.Application.Misc
{ {
foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory)) foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory))
{ {
manager.GetLogger(0).WriteDebug($"Adding plugin registered command \"{command.Name}\""); _logger.LogDebug("Adding plugin registered command {commandName}", command.Name);
manager.AddAdditionalCommand(command); manager.AddAdditionalCommand(command);
_registeredCommandNames.Add(command.Name); _registeredCommandNames.Add(command.Name);
} }
@ -140,6 +168,7 @@ namespace IW4MAdmin.Application.Misc
} }
} }
_scriptEngine.SetValue("_configHandler", new ScriptPluginConfigurationWrapper(Name, _scriptEngine));
await OnLoadAsync(manager); await OnLoadAsync(manager);
try try
@ -164,9 +193,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 +233,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 +270,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 +307,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 +322,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 +356,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,90 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using IW4MAdmin.Application.Configuration;
using Jint;
using Jint.Native;
using Newtonsoft.Json.Linq;
namespace IW4MAdmin.Application.Misc
{
public class ScriptPluginConfigurationWrapper
{
private readonly BaseConfigurationHandler<ScriptPluginConfiguration> _handler;
private readonly ScriptPluginConfiguration _config;
private readonly string _pluginName;
private readonly Engine _scriptEngine;
public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine)
{
_handler = new BaseConfigurationHandler<ScriptPluginConfiguration>("ScriptPluginSettings");
_config = _handler.Configuration() ??
(ScriptPluginConfiguration) new ScriptPluginConfiguration().Generate();
_pluginName = pluginName;
_scriptEngine = scriptEngine;
}
private static int? AsInteger(double d)
{
return int.TryParse(d.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : (int?) null;
}
public async Task SetValue(string key, object value)
{
var castValue = value;
if (value is double d)
{
castValue = AsInteger(d) ?? value;
}
if (value is object[] array && array.All(item => item is double d && AsInteger(d) != null))
{
castValue = array.Select(item => AsInteger((double)item)).ToArray();
}
if (!_config.ContainsKey(_pluginName))
{
_config.Add(_pluginName, new Dictionary<string, object>());
}
var plugin = _config[_pluginName];
if (plugin.ContainsKey(key))
{
plugin[key] = castValue;
}
else
{
plugin.Add(key, castValue);
}
_handler.Set(_config);
await _handler.Save();
}
public JsValue GetValue(string key)
{
if (!_config.ContainsKey(_pluginName))
{
return JsValue.Undefined;
}
if (!_config[_pluginName].ContainsKey(key))
{
return JsValue.Undefined;
}
var item = _config[_pluginName][key];
if (item is JArray array)
{
item = array.ToObject<List<dynamic>>();
}
return JsValue.FromObject(_scriptEngine, item);
}
}
}

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

@ -4,6 +4,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using System; using System;
using System.Net; using System.Net;
using Data.Models;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.GameEvent; using static SharedLibraryCore.GameEvent;

View File

@ -8,14 +8,20 @@ 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 Data.Models;
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()
@ -50,6 +56,7 @@ namespace IW4MAdmin.Application.RconParsers
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.AdditionalGroup, int.MaxValue);
Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *"; Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *";
Configuration.GametypeStatus.Pattern = ""; Configuration.GametypeStatus.Pattern = "";
@ -67,16 +74,33 @@ namespace IW4MAdmin.Application.RconParsers
public Game GameName { get; set; } = Game.COD; public Game GameName { get; set; } = Game.COD;
public bool CanGenerateLogPath { get; set; } = true; public bool CanGenerateLogPath { get; set; } = true;
public string Name { get; set; } = "Call of Duty"; public string Name { get; set; } = "Call of Duty";
public string RConEngine { get; set; } = "COD";
public bool IsOneLog { get; set; }
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command) public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command)
{ {
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command); var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
return response.Skip(1).ToArray(); return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
} }
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);
@ -107,7 +131,7 @@ namespace IW4MAdmin.Application.RconParsers
return new Dvar<T>() return new Dvar<T>()
{ {
Name = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarName]].Value, Name = dvarName,
Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)), Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)),
DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)), DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)),
LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)), LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)),
@ -115,58 +139,55 @@ namespace IW4MAdmin.Application.RconParsers
}; };
} }
public virtual async Task<(List<EFClient>, string, string)> GetStatusAsync(IRConConnection connection) public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection)
{ {
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS); var 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) return new StatusResponse
{ {
Console.WriteLine(line); Clients = ClientsFromStatus(response).ToArray(),
} Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus.Pattern),
#endif GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus.Pattern),
return (ClientsFromStatus(response), MapFromStatus(response), GameTypeFromStatus(response)); Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus.Pattern),
MaxClients = GetValueFromStatus<int?>(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus.Pattern)
};
} }
private string MapFromStatus(string[] response) private T GetValueFromStatus<T>(IEnumerable<string> response, ParserRegex.GroupType groupType, string groupPattern)
{ {
string map = null; if (string.IsNullOrEmpty(groupPattern))
{
return default;
}
string value = null;
foreach (var line in response) foreach (var line in response)
{ {
var regex = Regex.Match(line, Configuration.MapStatus.Pattern); var regex = Regex.Match(line, groupPattern);
if (regex.Success) if (regex.Success)
{ {
map = regex.Groups[Configuration.MapStatus.GroupMapping[ParserRegex.GroupType.RConStatusMap]].ToString(); value = regex.Groups[Configuration.MapStatus.GroupMapping[groupType]].ToString();
} }
} }
return map; if (value == null)
}
private string GameTypeFromStatus(string[] response)
{
if (string.IsNullOrWhiteSpace(Configuration.GametypeStatus.Pattern))
{ {
return null; return default;
} }
string gametype = null; if (typeof(T) == typeof(int?))
foreach (var line in response)
{ {
var regex = Regex.Match(line, Configuration.GametypeStatus.Pattern); return (T)Convert.ChangeType(int.Parse(value), Nullable.GetUnderlyingType(typeof(T)));
if (regex.Success)
{
gametype = regex.Groups[Configuration.GametypeStatus.GroupMapping[ParserRegex.GroupType.RConStatusGametype]].ToString();
}
} }
return gametype; return (T)Convert.ChangeType(value, typeof(T));
} }
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue) public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue)
{ {
string dvarString = (dvarValue is string str) string dvarString = (dvarValue is string str)
? $"{dvarName} \"{str}\"" ? $"{dvarName} \"{str}\""
: $"{dvarName} {dvarValue.ToString()}"; : $"{dvarName} {dvarValue}";
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0; return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0;
} }
@ -190,10 +211,21 @@ namespace IW4MAdmin.Application.RconParsers
if (match.Success) if (match.Success)
{ {
int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]); if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]] == "ZMBI")
int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]); {
_logger.LogDebug("Ignoring detected client {client} because they are zombie state", string.Join(",", match.Values));
continue;
}
var clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]);
var score = 0;
if (Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore] > 0)
{
score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]);
}
int ping = 999; var ping = 999;
// their state can be CNCT, ZMBI etc // their state can be CNCT, ZMBI etc
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3) if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3)
@ -202,13 +234,15 @@ namespace IW4MAdmin.Application.RconParsers
} }
long networkId; long networkId;
string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString;
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
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() || (ip == null && ping == 999) ?
name.GenerateGuidFromString() : name.GenerateGuidFromString() :
networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
} }
@ -218,8 +252,6 @@ namespace IW4MAdmin.Application.RconParsers
continue; continue;
} }
int? ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
var client = new EFClient() var client = new EFClient()
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias()
@ -234,6 +266,19 @@ namespace IW4MAdmin.Application.RconParsers
State = EFClient.ClientState.Connecting State = EFClient.ClientState.Connecting
}; };
client.SetAdditionalProperty("BotGuid", networkIdString);
if (Configuration.Status.GroupMapping.ContainsKey(ParserRegex.GroupType.AdditionalGroup))
{
var additionalGroupIndex =
Configuration.Status.GroupMapping[ParserRegex.GroupType.AdditionalGroup];
if (match.Values.Length > additionalGroupIndex)
{
client.SetAdditionalProperty("ConnectionClientId", match.Values[additionalGroupIndex]);
}
}
StatusPlayers.Add(client); StatusPlayers.Add(client);
} }
} }
@ -260,5 +305,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

@ -0,0 +1,16 @@
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.RConParsers
{
/// <summary>
/// empty implementation of the IW4RConParser
/// allows script plugins to generate dynamic RCon parsers
/// </summary>
internal sealed class DynamicRConParser : BaseRConParser
{
public DynamicRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory) : base(logger, parserRegexFactory)
{
}
}
}

View File

@ -1,9 +1,10 @@
using SharedLibraryCore.Interfaces; using System;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon; using SharedLibraryCore.RCon;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
namespace IW4MAdmin.Application.RconParsers namespace IW4MAdmin.Application.RConParsers
{ {
/// <summary> /// <summary>
/// generic implementation of the IRConParserConfiguration /// generic implementation of the IRConParserConfiguration
@ -15,6 +16,8 @@ namespace IW4MAdmin.Application.RconParsers
public ParserRegex Status { get; set; } public ParserRegex Status { get; set; }
public ParserRegex MapStatus { get; set; } public ParserRegex MapStatus { get; set; }
public ParserRegex GametypeStatus { get; set; } public ParserRegex GametypeStatus { get; set; }
public ParserRegex HostnameStatus { get; set; }
public ParserRegex MaxPlayersStatus { get; set; }
public ParserRegex Dvar { get; set; } public ParserRegex Dvar { get; set; }
public ParserRegex StatusHeader { get; set; } public ParserRegex StatusHeader { get; set; }
public string ServerNotRunningResponse { get; set; } public string ServerNotRunningResponse { get; set; }
@ -22,6 +25,9 @@ namespace IW4MAdmin.Application.RconParsers
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber; public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>();
public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>();
public int NoticeMaximumLines { get; set; } = 8;
public int NoticeMaxCharactersPerLine { get; set; } = 50;
public string NoticeLineSeparator { get; set; } = Environment.NewLine;
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory) public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
{ {
@ -30,6 +36,8 @@ namespace IW4MAdmin.Application.RconParsers
GametypeStatus = parserRegexFactory.CreateParserRegex(); GametypeStatus = parserRegexFactory.CreateParserRegex();
Dvar = parserRegexFactory.CreateParserRegex(); Dvar = parserRegexFactory.CreateParserRegex();
StatusHeader = parserRegexFactory.CreateParserRegex(); StatusHeader = parserRegexFactory.CreateParserRegex();
HostnameStatus = parserRegexFactory.CreateParserRegex();
MaxPlayersStatus = parserRegexFactory.CreateParserRegex();
} }
} }
} }

View File

@ -0,0 +1,15 @@
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.RConParsers
{
/// <inheritdoc cref="IStatusResponse"/>
public class StatusResponse : IStatusResponse
{
public string Map { get; set; }
public string GameType { get; set; }
public string Hostname { get; set; }
public int? MaxClients { get; set; }
public EFClient[] Clients { get; set; }
}
}

View File

@ -1,15 +0,0 @@
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.RconParsers
{
/// <summary>
/// empty implementation of the IW4RConParser
/// allows script plugins to generate dynamic RCon parsers
/// </summary>
sealed internal class DynamicRConParser : BaseRConParser
{
public DynamicRConParser(IParserRegexFactory parserRegexFactory) : base(parserRegexFactory)
{
}
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace Data.Abstractions
{
public interface IDataValueCache<T, V> where T : class
{
void SetCacheItem(Func<DbSet<T>, Task<V>> itemGetter, string keyName, TimeSpan? expirationTime = null);
Task<V> GetCacheItem(string keyName);
}
}

View File

@ -1,6 +1,6 @@
using SharedLibraryCore.Database; using Data.Context;
namespace SharedLibraryCore.Interfaces namespace Data.Abstractions
{ {
/// <summary> /// <summary>
/// describes the capabilities of the database context factory /// describes the capabilities of the database context factory

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Data.Abstractions
{
public interface ILookupCache<T> where T : class
{
Task InitializeAsync();
Task<T> AddAsync(T item);
Task<T> FirstAsync(Func<T, bool> query);
IEnumerable<T> GetAll();
}
}

View File

@ -1,4 +1,4 @@
namespace SharedLibraryCore.Interfaces namespace Data.Abstractions
{ {
/// <summary> /// <summary>
/// describes the capability of extending properties by name /// describes the capability of extending properties by name

View File

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Data.Abstractions
{
public interface IUniqueId
{
[NotMapped]
long Id { get; }
[NotMapped]
string Value { get; }
}
}

View File

@ -1,37 +1,35 @@
using Microsoft.EntityFrameworkCore; using System;
using SharedLibraryCore.Database.Models; using System.Threading;
using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using static SharedLibraryCore.Database.Models.EFClient; using Data.Abstractions;
using Data.Models;
using Data.Models.Client;
using Microsoft.EntityFrameworkCore;
namespace SharedLibraryCore.Database namespace Data.Context
{ {
public class ContextSeed public static class ContextSeed
{ {
private DatabaseContext context; public static async Task Seed(IDatabaseContextFactory contextFactory, CancellationToken token)
public ContextSeed(DatabaseContext ctx)
{ {
context = ctx; await using var context = contextFactory.CreateContext();
} var strategy = context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await context.Database.MigrateAsync(token);
});
public async Task Seed() if (!await context.AliasLinks.AnyAsync(token))
{
context.Database.Migrate();
if (context.AliasLinks.Count() == 0)
{ {
var link = new EFAliasLink(); var link = new EFAliasLink();
context.Clients.Add(new EFClient() context.Clients.Add(new EFClient()
{ {
ClientId = 1,
Active = false, Active = false,
Connections = 0, Connections = 0,
FirstConnection = DateTime.UtcNow, FirstConnection = DateTime.UtcNow,
LastConnection = DateTime.UtcNow, LastConnection = DateTime.UtcNow,
Level = Permission.Console, Level = EFClient.Permission.Console,
Masked = true, Masked = true,
NetworkId = 0, NetworkId = 0,
AliasLink = link, AliasLink = link,
@ -44,8 +42,8 @@ namespace SharedLibraryCore.Database
}, },
}); });
await context.SaveChangesAsync(); await context.SaveChangesAsync(token);
} }
} }
} }
} }

View File

@ -0,0 +1,135 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Threading;
using System.Threading.Tasks;
using Data.Extensions;
using Data.Models;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Client.Stats.Reference;
using Data.Models.Server;
namespace Data.Context
{
public abstract class DatabaseContext : DbContext
{
public DbSet<EFClient> Clients { get; set; }
public DbSet<EFAlias> Aliases { get; set; }
public DbSet<EFAliasLink> AliasLinks { get; set; }
public DbSet<EFPenalty> Penalties { get; set; }
public DbSet<EFMeta> EFMeta { get; set; }
public DbSet<EFChangeHistory> EFChangeHistory { get; set; }
#region STATS
public DbSet<Models.Vector3> Vector3s { get; set; }
public DbSet<EFACSnapshotVector3> SnapshotVector3s { get; set; }
public DbSet<EFACSnapshot> ACSnapshots { get; set; }
public DbSet<EFServer> Servers { get; set; }
public DbSet<EFClientKill> ClientKills { get; set; }
public DbSet<EFClientMessage> ClientMessages { get; set; }
public DbSet<EFServerStatistics> ServerStatistics { get; set; }
public DbSet<EFHitLocation> HitLocations { get; set; }
public DbSet<EFClientHitStatistic> HitStatistics { get; set; }
public DbSet<EFWeapon> Weapons { get; set; }
public DbSet<EFWeaponAttachment> WeaponAttachments { get; set; }
public DbSet<EFMap> Maps { get; set; }
#endregion
private void SetAuditColumns()
{
return;
}
public DatabaseContext()
{
if (!MigrationExtensions.IsMigration)
{
throw new InvalidOperationException();
}
}
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{
}
protected DatabaseContext(DbContextOptions options) : base(options)
{
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = default)
{
SetAuditColumns();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
public override int SaveChanges()
{
SetAuditColumns();
return base.SaveChanges();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// make network id unique
modelBuilder.Entity<EFClient>(entity => { entity.HasIndex(e => e.NetworkId).IsUnique(); });
modelBuilder.Entity<EFPenalty>(entity =>
{
entity.HasOne(p => p.Offender)
.WithMany(c => c.ReceivedPenalties)
.HasForeignKey(c => c.OffenderId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(p => p.Punisher)
.WithMany(p => p.AdministeredPenalties)
.HasForeignKey(c => c.PunisherId)
.OnDelete(DeleteBehavior.Restrict);
entity.Property(p => p.Expires)
.IsRequired(false);
});
modelBuilder.Entity<EFAliasLink>(entity =>
{
entity.HasMany(e => e.Children)
.WithOne(a => a.Link)
.HasForeignKey(k => k.LinkId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<EFAlias>(ent =>
{
ent.Property(a => a.IPAddress).IsRequired(false);
ent.HasIndex(a => a.IPAddress);
ent.Property(a => a.Name).HasMaxLength(24);
ent.HasIndex(a => a.Name);
ent.Property(_alias => _alias.SearchableName).HasMaxLength(24);
ent.HasIndex(_alias => _alias.SearchableName);
ent.HasIndex(_alias => new {_alias.Name, _alias.IPAddress}).IsUnique();
});
modelBuilder.Entity<EFMeta>(ent =>
{
ent.HasIndex(_meta => _meta.Key);
ent.HasIndex(_meta => _meta.LinkedMetaId);
ent.HasOne(_meta => _meta.LinkedMeta)
.WithMany()
.OnDelete(DeleteBehavior.SetNull);
});
// force full name for database conversion
modelBuilder.Entity<EFClient>().ToTable("EFClients");
modelBuilder.Entity<EFAlias>().ToTable("EFAlias");
modelBuilder.Entity<EFAliasLink>().ToTable("EFAliasLinks");
modelBuilder.Entity<EFPenalty>().ToTable("EFPenalties");
Models.Configuration.StatsModelConfiguration.Configure(modelBuilder);
base.OnModelCreating(modelBuilder);
}
}
}

30
Data/Data.csproj Normal file
View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Configurations>Debug;Release;Prerelease</Configurations>
<Platforms>AnyCPU</Platforms>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>RaidMax.IW4MAdmin.Data</PackageId>
<Title>RaidMax.IW4MAdmin.Data</Title>
<Authors />
<PackageVersion>1.0.3</PackageVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql" Version="4.1.7" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.2.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.10" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'">
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.10" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
namespace Data.Extensions
{
public static class MigrationExtensions
{
public static bool IsMigration => Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Migration";
}
}

View File

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Data.Helpers
{
public class DataValueCache<T, V> : IDataValueCache<T, V> where T : class
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
private readonly Dictionary<string, CacheState> _cacheStates = new Dictionary<string, CacheState>();
private const int DefaultExpireMinutes = 15;
private class CacheState
{
public string Key { get; set; }
public DateTime LastRetrieval { get; set; }
public TimeSpan ExpirationTime { get; set; }
public Func<DbSet<T>, Task<V>> Getter { get; set; }
public V Value { get; set; }
public bool IsExpired => (DateTime.Now - LastRetrieval.Add(ExpirationTime)).TotalSeconds > 0;
}
public DataValueCache(ILogger<DataValueCache<T, V>> logger, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_contextFactory = contextFactory;
}
public void SetCacheItem(Func<DbSet<T>, Task<V>> getter, string key, TimeSpan? expirationTime = null)
{
if (_cacheStates.ContainsKey(key))
{
_logger.LogDebug("Cache key {key} is already added", key);
return;
}
var state = new CacheState()
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_cacheStates.Add(key, state);
}
public async Task<V> GetCacheItem(string keyName)
{
if (!_cacheStates.ContainsKey(keyName))
{
throw new ArgumentException("No cache found for key {key}", keyName);
}
var state = _cacheStates[keyName];
if (state.IsExpired)
{
await RunCacheUpdate(state);
}
return state.Value;
}
private async Task RunCacheUpdate(CacheState state)
{
try
{
await using var context = _contextFactory.CreateContext(false);
var set = context.Set<T>();
var value = await state.Getter(set);
state.Value = value;
state.LastRetrieval = DateTime.Now;
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not get cached value for {key}", state.Key);
}
}
}
}

114
Data/Helpers/LookupCache.cs Normal file
View File

@ -0,0 +1,114 @@
using Data.Abstractions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Data.Helpers
{
public class LookupCache<T> : ILookupCache<T> where T : class, IUniqueId
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
private Dictionary<long, T> _cachedItems;
private readonly SemaphoreSlim _onOperation = new SemaphoreSlim(1, 1);
public LookupCache(ILogger<LookupCache<T>> logger, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_contextFactory = contextFactory;
}
public async Task<T> AddAsync(T item)
{
await _onOperation.WaitAsync();
T existingItem = null;
if (_cachedItems.ContainsKey(item.Id))
{
existingItem = _cachedItems[item.Id];
}
if (existingItem != null)
{
_logger.LogDebug("Cached item already added for {type} {id} {value}", typeof(T).Name, item.Id,
item.Value);
_onOperation.Release();
return existingItem;
}
try
{
_logger.LogDebug("Adding new {type} with {id} {value}", typeof(T).Name, item.Id, item.Value);
await using var context = _contextFactory.CreateContext();
context.Set<T>().Add(item);
await context.SaveChangesAsync();
_cachedItems.Add(item.Id, item);
return item;
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not add item to cache for {type}", typeof(T).Name);
throw new Exception("Could not add item to cache");
}
finally
{
if (_onOperation.CurrentCount == 0)
{
_onOperation.Release();
}
}
}
public async Task<T> FirstAsync(Func<T, bool> query)
{
await _onOperation.WaitAsync();
try
{
var cachedResult = _cachedItems.Values.Where(query);
if (cachedResult.Any())
{
return cachedResult.FirstOrDefault();
}
}
catch
{
}
finally
{
if (_onOperation.CurrentCount == 0)
{
_onOperation.Release(1);
}
}
return null;
}
public IEnumerable<T> GetAll()
{
return _cachedItems.Values;
}
public async Task InitializeAsync()
{
try
{
await using var context = _contextFactory.CreateContext();
_cachedItems = await context.Set<T>().ToDictionaryAsync(item => item.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not initialize caching for {cacheType}", typeof(T).Name);
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -5,12 +5,13 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database; using Data;
using Data.MigrationContext;
using System; using System;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180409183408_InitialCreate")] [Migration("20180409183408_InitialCreate")]
partial class InitialCreate partial class InitialCreate
{ {

View File

@ -4,7 +4,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class InitialCreate : Migration public partial class InitialCreate : Migration
{ {

View File

@ -5,12 +5,13 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database; using Data;
using Data.MigrationContext;
using System; using System;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180502195450_Update")] [Migration("20180502195450_Update")]
partial class Update partial class Update
{ {

View File

@ -2,7 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class Update : Migration public partial class Update : Migration
{ {

View File

@ -5,12 +5,13 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database; using Data;
using Data.MigrationContext;
using System; using System;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180516023249_AddEloField")] [Migration("20180516023249_AddEloField")]
partial class AddEloField partial class AddEloField
{ {

View File

@ -2,7 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddEloField : Migration public partial class AddEloField : Migration
{ {

View File

@ -5,12 +5,13 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database; using Data;
using Data.MigrationContext;
using System; using System;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180517223349_AddRollingKDR")] [Migration("20180517223349_AddRollingKDR")]
partial class AddRollingKDR partial class AddRollingKDR
{ {

View File

@ -2,7 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddRollingKDR : Migration public partial class AddRollingKDR : Migration
{ {

View File

@ -5,12 +5,12 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database; using Data.MigrationContext;
using System; using System;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180531212903_AddAutomatedOffenseAndRatingHistory")] [Migration("20180531212903_AddAutomatedOffenseAndRatingHistory")]
partial class AddAutomatedOffenseAndRatingHistory partial class AddAutomatedOffenseAndRatingHistory
{ {

View File

@ -4,7 +4,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddAutomatedOffenseAndRatingHistory : Migration public partial class AddAutomatedOffenseAndRatingHistory : Migration
{ {

View File

@ -5,12 +5,12 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database; using Data.MigrationContext;
using System; using System;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180601172317_AddActivityAmount")] [Migration("20180601172317_AddActivityAmount")]
partial class AddActivityAmount partial class AddActivityAmount
{ {

View File

@ -2,7 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddActivityAmount : Migration public partial class AddActivityAmount : Migration
{ {

View File

@ -5,12 +5,12 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database; using Data.MigrationContext;
using System; using System;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180602041758_AddClientMeta")] [Migration("20180602041758_AddClientMeta")]
partial class AddClientMeta partial class AddClientMeta
{ {

View File

@ -4,7 +4,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddClientMeta : Migration public partial class AddClientMeta : Migration
{ {

View File

@ -5,12 +5,12 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database; using Data.MigrationContext;
using System; using System;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180605191706_AddEFACSnapshots")] [Migration("20180605191706_AddEFACSnapshots")]
partial class AddEFACSnapshots partial class AddEFACSnapshots
{ {

View File

@ -4,7 +4,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddEFACSnapshots : Migration public partial class AddEFACSnapshots : Migration
{ {

View File

@ -5,12 +5,12 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database; using Data.MigrationContext;
using System; using System;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180614014303_IndexForEFAlias")] [Migration("20180614014303_IndexForEFAlias")]
partial class IndexForEFAlias partial class IndexForEFAlias
{ {

View File

@ -2,7 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class IndexForEFAlias : Migration public partial class IndexForEFAlias : Migration
{ {

View File

@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SharedLibraryCore.Database; using Data.MigrationContext;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180902035612_AddFractionAndIsKill")] [Migration("20180902035612_AddFractionAndIsKill")]
partial class AddFractionAndIsKill partial class AddFractionAndIsKill
{ {

View File

@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddFractionAndIsKill : Migration public partial class AddFractionAndIsKill : Migration
{ {

View File

@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SharedLibraryCore.Database; using Data.MigrationContext;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180904154622_AddVisibilityPercentage")] [Migration("20180904154622_AddVisibilityPercentage")]
partial class AddVisibilityPercentage partial class AddVisibilityPercentage
{ {

View File

@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddVisibilityPercentage : Migration public partial class AddVisibilityPercentage : Migration
{ {

View File

@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SharedLibraryCore.Database; using Data.MigrationContext;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180907020706_AddVision")] [Migration("20180907020706_AddVision")]
partial class AddVision partial class AddVision
{ {

View File

@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddVision : Migration public partial class AddVision : Migration
{ {

View File

@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SharedLibraryCore.Database; using Data.MigrationContext;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180908004053_AddWhenToRating")] [Migration("20180908004053_AddWhenToRating")]
partial class AddWhenToRating partial class AddWhenToRating
{ {

View File

@ -1,7 +1,7 @@
using System; using System;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddWhenToRating : Migration public partial class AddWhenToRating : Migration
{ {

View File

@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SharedLibraryCore.Database; using Data.MigrationContext;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180910221749_AddRatingIndexes")] [Migration("20180910221749_AddRatingIndexes")]
partial class AddRatingIndexes partial class AddRatingIndexes
{ {

View File

@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddRatingIndexes : Migration public partial class AddRatingIndexes : Migration
{ {

View File

@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SharedLibraryCore.Database; using Data.MigrationContext;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180911184224_AddEFAliasNameIndex")] [Migration("20180911184224_AddEFAliasNameIndex")]
partial class AddEFAliasNameIndex partial class AddEFAliasNameIndex
{ {

View File

@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddEFAliasNameIndex : Migration public partial class AddEFAliasNameIndex : Migration
{ {

View File

@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SharedLibraryCore.Database; using Data.MigrationContext;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180911190823_AddEFAliasNameMaxLength24")] [Migration("20180911190823_AddEFAliasNameMaxLength24")]
partial class AddEFAliasNameMaxLength24 partial class AddEFAliasNameMaxLength24
{ {

View File

@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddEFAliasNameMaxLength24 : Migration public partial class AddEFAliasNameMaxLength24 : Migration
{ {

View File

@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SharedLibraryCore.Database; using Data.MigrationContext;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180912015012_AddPreviousCurrentValueToEFChangeHistory")] [Migration("20180912015012_AddPreviousCurrentValueToEFChangeHistory")]
partial class AddPreviousCurrentValueToEFChangeHistory partial class AddPreviousCurrentValueToEFChangeHistory
{ {

View File

@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddPreviousCurrentValueToEFChangeHistory : Migration public partial class AddPreviousCurrentValueToEFChangeHistory : Migration
{ {

View File

@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SharedLibraryCore.Database; using Data.MigrationContext;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
[DbContext(typeof(DatabaseContext))] [DbContext(typeof(MySqlDatabaseContext))]
[Migration("20180915163111_AddIndexToMessageTimeSent")] [Migration("20180915163111_AddIndexToMessageTimeSent")]
partial class AddIndexToMessageTimeSent partial class AddIndexToMessageTimeSent
{ {

View File

@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
namespace SharedLibraryCore.Migrations namespace Data.Migrations.MySql
{ {
public partial class AddIndexToMessageTimeSent : Migration public partial class AddIndexToMessageTimeSent : Migration
{ {

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