Compare commits

...

192 Commits

Author SHA1 Message Date
12dfd8c558 more integration tweaks
add configurable flood protect interval for rcon
2022-02-08 12:03:55 -06:00
07f675eadc fix issue with plugin registration 2022-02-07 22:02:50 -06:00
576d7015fa increase poll rate for reasonable response times 2022-02-07 18:47:16 -06:00
b1a1aae6c0 initial framework for gsc + iw4madmin integration
improvements to script plugin capabilities and error feedback
2022-02-07 18:43:36 -06:00
a0f4ceccfe small optimizations 2022-02-02 16:21:08 -06:00
b7a76cc4a2 only send heartbeat when fully initialized 2022-02-01 18:31:55 -06:00
261da918c7 Allow either parser version or parser name to be used in server config block 2022-02-01 18:27:03 -06:00
2ed5e00bcb more profile loading optimizations 2022-02-01 18:20:29 -06:00
6ca94f8da8 only default to IPv4 when parsing
update postgres target version to 12.9
2022-02-01 14:27:16 -06:00
3b532cf1f7 don't try to load scoreboard if not on scoreboard page 2022-02-01 09:09:29 -06:00
40966ed74d modify update script on linux to set executable bit on itself after update 2022-02-01 09:04:40 -06:00
45eacabc28 actual fix now? 2022-01-31 17:56:43 -06:00
0b02b7627a fix again 2022-01-31 17:23:56 -06:00
fc3a24ca17 fix typo on pipeline 2022-01-31 17:00:24 -06:00
209cb6cdd0 use proper folder in post publish script 2022-01-31 16:47:51 -06:00
cfd4296f5c update webfront ip lookup for ssl connection 2022-01-31 16:37:44 -06:00
b275fbaced create update script for managing updates programatically
./UpdateIW4MAdmin.sh or ./UpdateIW4MAdmin.ps1
Co-authored-by: xerxes-at <xerxes-at@users.noreply.github.com>
2022-01-31 11:06:44 -06:00
b2a3625288 update IP lookup api 2022-01-31 08:16:12 -06:00
0d3e2cb0bc fix issue with writing config files 2022-01-29 13:30:48 -06:00
505a2c4c2d fix refactor issue 2022-01-28 17:28:49 -06:00
8730a3fab8 fix issue with certain penalties not linking 2022-01-28 15:33:21 -06:00
3539101a40 webfront profile loading optimizations 2022-01-28 14:33:08 -06:00
7ccdee7d1b disable some warnings 2022-01-28 09:37:04 -06:00
f4b160b735 small startup performance optimization 2022-01-28 09:35:01 -06:00
73036dc1c7 properly provide culture to welcome plugin ordinalize 2022-01-27 21:19:05 -06:00
6cfcce23cc tech debt 2022-01-27 21:18:35 -06:00
8649b0efe9 fix issue with configuration on new install 2022-01-27 13:37:38 -06:00
f554536b95 s This is a combination of 7 commits.
This is the 1st commit message:
2022-01-27 11:25:42 -06:00
11efc039b5 update for .net core SDK Azure 2022-01-27 09:35:16 -06:00
916ea4163b add additional fields to server api 2022-01-26 15:26:26 -06:00
0bed1c728a update .net version required in readme 2022-01-26 15:26:25 -06:00
7171b3753e Address some .NET 6 oddities and allow webfront startup without servers being monitored 2022-01-26 15:26:25 -06:00
a602e8caed Initial .net 6 upgrades 2022-01-26 15:26:25 -06:00
e4cb3abb20 order chat context messages from oldest to newest 2022-01-26 15:26:25 -06:00
686b297d32 hopeful topstats fixes 2022-01-26 15:20:10 -06:00
fb11bf54a6 scoreboard tweak 2022-01-26 15:20:10 -06:00
11d2b0da90 display "--" for no zscore 2022-01-26 15:20:10 -06:00
8bd0337168 scoreboard sort tweak 2022-01-26 15:20:10 -06:00
74b565ebae increase zscore precision for scoreboard.. last commit I promise 2022-01-26 15:20:10 -06:00
2b467d6ef9 fix missing null check in scoreboard. oops 2022-01-26 15:20:10 -06:00
e90355307d include cs go "estimated" score on scoreboard 2022-01-26 15:20:10 -06:00
d3962989b5 add sorting and zscore to scoreboard 2022-01-26 15:20:10 -06:00
16831aaccb remove incorrect project reference 2022-01-26 15:20:10 -06:00
032753236b fix misc webfront errors on first run after configuration 2022-01-26 15:20:10 -06:00
7fcb2202bd add server scoreboard functionality 2022-01-26 15:20:10 -06:00
7910fc73a3 increment shared library version 2022-01-26 15:20:10 -06:00
a8d581eab7 Update shared library to reference data library instead of separate nuget package 2022-01-26 15:20:10 -06:00
bd27977b1e improve connection resets in CSGO 2022-01-26 15:20:10 -06:00
092ca5f9bd Update plutonium t4 MP parser 2022-01-26 15:20:10 -06:00
3f0b1b892a Add Plutonium T4 Co-Op/Zombies support 2022-01-26 15:20:10 -06:00
c713fdacb0 update packages for previous release (re-release of previous) 2022-01-26 15:20:10 -06:00
f5854f8d03 hopefully fix issue with linked banned players 2022-01-26 15:20:10 -06:00
67be4f8e7f reduce some potential errors 2022-01-26 15:20:10 -06:00
9baad44ab4 update max name length to 34 for base kill/damage parser 2022-01-26 15:20:10 -06:00
76f5933074 fix color code issue 2022-01-26 15:20:10 -06:00
4cce336fb9 update custom callbacks to properly exit thread on disconnect 2022-01-26 15:20:10 -06:00
5d12ff471b work around for iw5/t6 not being able to parse multiple commands over rcon for mag command 2022-01-26 15:20:10 -06:00
5d7ac7498f update to show full gametype name on webfront 2022-01-26 15:20:10 -06:00
15cb114c15 implement map and gametype command 2022-01-26 15:20:10 -06:00
e739c91b52 add color code mapping for CSGO 2022-01-26 15:20:10 -06:00
17c9944eef fix concurrency issue with accent color setup 2022-01-26 15:20:10 -06:00
4a89744ee9 abstract engine color codes to use (Color::<Color>) format to make codes more.
see pt6 parser and configs for example usages
2022-01-26 15:20:10 -06:00
66010a2fa2 fix issue with caching implementation 2022-01-26 15:20:10 -06:00
ce3119425f try renable FTP publish 2022-01-26 15:20:10 -06:00
307ff3ddeb update help command to use per game commands 2022-01-26 15:20:10 -06:00
7f2fa390c7 fix plugin error formatting 2022-01-26 15:20:10 -06:00
a88b30562c update caching to use automatic timer instead of request based to prevent task cancellation 2022-01-26 15:20:10 -06:00
08bcd23cbc add default port and rcon password hint during setup 2022-01-26 15:20:10 -06:00
072571d341 add console log sink for critical errors 2022-01-26 15:20:10 -06:00
35e42516f1 update plugin error message format 2022-01-26 15:20:10 -06:00
2210ccea68 update webfront ip lookup to bypass api key restriction 2022-01-26 15:20:10 -06:00
08b93fcc10 Add Pluto IW5 Maps from r2385 (#220) 2022-01-26 15:20:10 -06:00
ab05b45016 fix issue with assigning correct server when processing command 2022-01-26 15:20:10 -06:00
825dd6f382 update country flag api 2022-01-26 15:20:10 -06:00
f99fdac4b0 remove javascript error log trying to load hljs from non config pages 2022-01-26 15:20:10 -06:00
f7897763e3 temporarily disable ftp release integration to bypass unknown Error: connect ETIMEDOUT *:21 (control socket) 2022-01-26 15:20:10 -06:00
5b95cdaca8 update welcome plugin to bypass api lookup limitation 2022-01-26 15:20:10 -06:00
c4e0c4c36a cleanup and enhance penalty handling 2022-01-26 15:20:10 -06:00
31d0dfc7d3 reduce timeout when master api is down 2022-01-26 15:20:10 -06:00
8f52714fb7 fix issue with detecting bans on accounts with new ips when implicit linking is disabled 2022-01-26 15:20:10 -06:00
e4153e0c2f post webfront url to master 2022-01-26 15:20:10 -06:00
8d0c48614f Merge pull request #219 from RaidMax/release/pre
Merge pre release into master
2021-10-19 20:52:49 -05:00
761d156209 Merge branch 'master' into release/pre 2021-10-19 20:45:05 -05:00
77f04058de merge default settings up 2021-10-19 20:40:40 -05:00
1317102d00 add script injection to the config to import custom webfront scripts (ie google tracking) 2021-10-19 20:17:10 -05:00
a2c7d92162 fix issue on about page with duplicate server names or inactive servers 2021-10-19 20:02:31 -05:00
b2afc410f2 improve about page layout 2021-10-16 13:30:26 -05:00
5b3420b97a default about page to enabled 2021-10-10 10:57:27 -05:00
74bb3da459 add option to toggle about page/make some checks on displayed rules 2021-10-10 10:44:18 -05:00
3916278422 Add about/community info guidelines/social page 2021-10-09 21:11:47 -05:00
a01543c89b deactivate penalties while unlinking an account if implicit account linking is disabled 2021-09-30 10:28:04 -05:00
694431d789 fix profile display with implicit linked accounts enabled 2021-09-18 22:31:56 -05:00
d5f978858d set sv_sayname on connection restore 2021-09-18 18:28:37 -05:00
e80753a4d3 make connection attempts for CoD configurable as "ServerConnectionAttempts" 2021-09-18 18:25:02 -05:00
d4fb75d07c add check to determine whether to include color codes when checking name length 2021-09-18 18:10:47 -05:00
e97119211f fix source issue on home page 2021-09-17 11:23:57 -05:00
87985b3e68 cap client name for new flow 2021-09-17 11:19:17 -05:00
33c63f01db add raw file editing to configuration page in webfront 2021-09-16 16:27:40 -05:00
68c1151191 add tooltip timestamp to max concurrent players 2021-09-14 18:12:20 -05:00
54e39fabb1 fix client history issue with empty database 2021-09-10 11:27:46 -05:00
a4f0726b32 Merge remote-tracking branch 'origin/release/pre' into release/pre 2021-09-06 11:37:30 -05:00
05e228633d fix searching name resulting in incorrect results 2021-09-06 11:37:15 -05:00
e267bd95da Update IW6x parser to automatically find the log file. (#216)
* Update ParserIW6x.js
2021-09-05 10:45:28 -05:00
c7fab5d36c removed commented code and show current alias for ip search 2021-09-05 10:43:48 -05:00
1f8b7cde3f test linking fix 2021-09-04 12:33:25 -05:00
c5f9a68102 implement client server connection tracking persistence 2021-08-31 18:21:40 -05:00
eff8a29a39 version css for webfront 2021-08-31 18:07:07 -05:00
0191c8b7a7 bugfix for edge case of linking alias to new account 2021-08-31 09:53:01 -05:00
fa6524c3b1 fix issue with display server with no saved player history 2021-08-31 08:44:15 -05:00
5b11196b29 bundle js by version so webfront updates don't need a cache refresh 2021-08-30 20:30:06 -05:00
3b7a22edef tweak player history hover format 2021-08-29 20:47:25 -05:00
deff4f2947 persist client count history data across reboots and allow for configurable timespan 2021-08-29 13:10:10 -05:00
02e5e78f67 update iw5 parser to work around filesytem dvar limitation 2021-08-28 17:56:41 -05:00
162006da29 use new cache signature 2021-08-27 21:05:30 -05:00
27e9ecfd9d support homepath in pluto t6 2021-08-27 20:47:06 -05:00
da301bef40 Exclude accidental dotnet bundle command comment 2021-08-26 17:37:01 -05:00
a815bcbff5 Add max concurrent players over 24 hours badge to home 2021-08-26 17:35:05 -05:00
19a49504b8 display "since last connection" as per server on top stats instead of last connection to any servers 2021-08-25 17:47:57 -05:00
3bb87dffb0 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2021-08-25 11:07:32 -05:00
02942e5c03 Add support for IW5 (#213) 2021-08-25 11:06:52 -05:00
8c5ff440db Updated T6 AC GSC (#214)
* PlutoT6 AC GSC Updated

PlutoT6's GSC modding capabilities changed, this allows us to bring the script on parity with the IW4x one. The following things changed:
*  Script no longer replaces stock GSC since custom GSC files are now supported.
* The Script now captures the last time the client used his attack button; this is used to detect trigger bots.
* Cleaned up the code a bit

* Create README.MD

Basic installation guide.
2021-08-25 11:06:46 -05:00
a0b7781e66 properly unban accounts associated with IP with toggle 2021-08-25 11:02:37 -05:00
596272a3de tweak linking behavior 2021-08-21 10:40:03 -05:00
b83ea57579 fix another thing 2021-08-16 18:28:00 -05:00
75f68b6385 remove other changes 2021-08-16 17:13:17 -05:00
d5e4d083c5 renable dotnet bundle cuz that was the real issue. 2021-08-16 17:02:47 -05:00
602ec66afe more pipeline test plz work 2021-08-16 16:53:58 -05:00
435b079b94 testing again for CLI Version 2021-08-16 16:46:18 -05:00
a4eec5981f specify explicit .net cli sdk version for pipeline 2021-08-16 13:50:22 -05:00
0b6e261dbb fix more issues with implicit link toggle 2021-08-16 13:20:54 -05:00
7e1221f467 fix small issue with new toggle 2021-08-14 20:43:20 -05:00
a6b0911af9 make implicit account linking a feature toggle 2021-08-14 17:55:28 -05:00
fa66381193 small fixes 2021-08-14 11:30:15 -05:00
67c2406325 fix issues with last release 2021-07-12 14:57:44 -05:00
e2ea5c6ce0 support hostnames for server config 2021-07-11 17:26:30 -05:00
5ef00d6dae tweak headshot detection for CSGO 2021-07-11 09:58:02 -05:00
5921098dce detect headshots for CSGO on advanced stats
track say_team events for CSGO
2021-07-10 21:37:51 -05:00
31ee71260a use default settings for maps and quick messages config (remove from IW4MAdminSettings) 2021-07-09 16:50:33 -05:00
ed8067a4a2 add offline messaging feature 2021-07-08 21:12:09 -05:00
e2116712e7 pass x-forwarded-for to properly log proxied login/logout 2021-07-05 16:08:13 -05:00
8b06da5783 use different api for country code/flag that support https 2021-07-02 10:04:56 -05:00
33a427bb8a add country flag and name to profile 2021-07-01 21:58:09 -05:00
c9d7a957dc add reset anticheat metric (!rsa) for issue #177 2021-07-01 13:12:19 -05:00
9c6ff6f353 use right game for estimated score 2021-07-01 13:06:31 -05:00
7444cb6472 actually fix steam id parsing 2021-07-01 10:14:58 -05:00
c7e5c9c8dd parse steam id properly for source games 2021-07-01 09:10:56 -05:00
0256fc35d2 add login/logout events to change tracker
default guest profile to minimum permissions
2021-06-30 21:13:25 -05:00
0019ed8dde fix run as command config not being honored properly 2021-06-30 18:10:45 -05:00
56aec53e72 fix bad key lookup in manager 2021-06-30 14:01:41 -05:00
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
8530444ffa Added T7 aliases (#186)
1. T7 aliases by @mikzyy
2. Highrise for IW5 which is still in beta
2021-03-23 13:14:11 -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
2512b9f251 Added iw6 aliases (#184) 2021-01-20 12:43:44 -06:00
405 changed files with 41983 additions and 6147 deletions

4
.gitignore vendored
View File

@ -245,4 +245,6 @@ launchSettings.json
/Tests/ApplicationTests/Files/replay.json
/GameLogServer/game_log_server_env
.idea/*
*.db
*.db
/Data/IW4MAdmin_Migration.db-shm
/Data/IW4MAdmin_Migration.db-wal

View File

@ -22,7 +22,7 @@ namespace IW4MAdmin.Application.API.Master
public int Uptime { get; set; }
/// <summary>
/// Specifices the version of the instance
/// Specifies the version of the instance
/// </summary>
[JsonProperty("version")]
[JsonConverter(typeof(BuildNumberJsonConverter))]
@ -33,5 +33,11 @@ namespace IW4MAdmin.Application.API.Master
/// </summary>
[JsonProperty("servers")]
public List<ApiServer> Servers { get; set; }
/// <summary>
/// Url IW4MAdmin is listening on
/// </summary>
[JsonProperty("webfront_url")]
public string WebfrontUrl { get; set; }
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2020.0.0.0</Version>
@ -24,14 +24,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-1632" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.10">
<PackageReference Include="Jint" Version="3.0.0-beta-2037" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.10" />
<PackageReference Include="RestEase" Version="1.5.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="RestEase" Version="1.5.5" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup>
<PropertyGroup>
@ -48,6 +48,8 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Integrations\Cod\Integrations.Cod.csproj" />
<ProjectReference Include="..\Integrations\Source\Integrations.Source.csproj" />
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>true</Private>
</ProjectReference>

View File

@ -1,7 +1,7 @@
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RconParsers;
using IW4MAdmin.Application.RConParsers;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
@ -15,6 +15,7 @@ using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text;
@ -26,6 +27,7 @@ using IW4MAdmin.Application.Migration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using SharedLibraryCore.Formatting;
using static SharedLibraryCore.GameEvent;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
@ -217,18 +219,20 @@ namespace IW4MAdmin.Application
return _commands;
}
public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList();
public async Task UpdateServerStates()
{
// 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)
{
// select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks
.Where(ut => ut.Value.Status == TaskStatus.RanToCompletion ||
ut.Value.Status == TaskStatus.Canceled ||
ut.Value.Status == TaskStatus.Faulted)
.Where(ut => ut.Value.task.Status == TaskStatus.RanToCompletion ||
ut.Value.task.Status == TaskStatus.Canceled || // we want to cancel if a task takes longer than 5 minutes
ut.Value.task.Status == TaskStatus.Faulted || DateTime.Now - ut.Value.startTime > TimeSpan.FromMinutes(5))
.Select(ut => ut.Key)
.ToList();
@ -239,9 +243,14 @@ namespace IW4MAdmin.Application
IsInitialized = true;
}
// remove the update tasks as they have completd
foreach (long serverId in serverTasksToRemove)
// remove the update tasks as they have completed
foreach (var serverId in serverTasksToRemove.Where(serverId => runningUpdateTasks.ContainsKey(serverId)))
{
if (!runningUpdateTasks[serverId].tokenSource.Token.IsCancellationRequested)
{
runningUpdateTasks[serverId].tokenSource.Cancel();
}
runningUpdateTasks.Remove(serverId);
}
@ -249,11 +258,16 @@ namespace IW4MAdmin.Application
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)))
{
runningUpdateTasks.Add(server.EndPoint, Task.Run(async () =>
var tokenSource = new CancellationTokenSource();
runningUpdateTasks.Add(server.EndPoint, (Task.Run(async () =>
{
try
{
await server.ProcessUpdatesAsync(_tokenSource.Token);
if (runningUpdateTasks.ContainsKey(server.EndPoint))
{
await server.ProcessUpdatesAsync(_tokenSource.Token)
.WithWaitCancellation(runningUpdateTasks[server.EndPoint].tokenSource.Token);
}
}
catch (Exception e)
@ -268,7 +282,7 @@ namespace IW4MAdmin.Application
{
server.IsInitialized = true;
}
}));
}, tokenSource.Token), tokenSource, DateTime.Now));
}
try
@ -341,15 +355,13 @@ namespace IW4MAdmin.Application
// copy over default config if it doesn't exist
if (!_appConfig.Servers?.Any() ?? true)
{
var defaultConfig = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings").Configuration();
//ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
//var newConfig = ConfigHandler.Configuration();
var defaultHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
await defaultHandler.BuildAsync();
var defaultConfig = defaultHandler.Configuration();
_appConfig.AutoMessages = defaultConfig.AutoMessages;
_appConfig.GlobalRules = defaultConfig.GlobalRules;
_appConfig.Maps = defaultConfig.Maps;
_appConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
_appConfig.QuickMessages = defaultConfig.QuickMessages;
//if (newConfig.Servers == null)
{
@ -390,12 +402,24 @@ namespace IW4MAdmin.Application
await ConfigHandler.Save();
}
#pragma warning disable 618
if (_appConfig.Maps != null)
{
_appConfig.Maps = null;
}
if (_appConfig.QuickMessages != null)
{
_appConfig.QuickMessages = null;
}
#pragma warning restore 618
var validator = new ApplicationConfigurationValidator();
var validationResult = validator.Validate(_appConfig);
if (!validationResult.IsValid)
{
throw new ConfigurationException("MANAGER_CONFIGURATION_ERROR")
throw new ConfigurationException(_translationLookup["MANAGER_CONFIGURATION_ERROR"])
{
Errors = validationResult.Errors.Select(_error => _error.ErrorMessage).ToArray(),
ConfigurationFileName = ConfigHandler.FileName
@ -404,7 +428,7 @@ namespace IW4MAdmin.Application
foreach (var serverConfig in _appConfig.Servers)
{
Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
ConfigurationMigration.ModifyLogPath020919(serverConfig);
if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null)
{
@ -432,6 +456,17 @@ namespace IW4MAdmin.Application
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(_appConfig.CustomParserEncoding) ? _appConfig.CustomParserEncoding : "windows-1252");
foreach (var parser in AdditionalRConParsers)
{
if (!parser.Configuration.ColorCodeMapping.ContainsKey(ColorCodes.Accent.ToString()))
{
parser.Configuration.ColorCodeMapping.Add(ColorCodes.Accent.ToString(),
parser.Configuration.ColorCodeMapping.TryGetValue(_appConfig.IngameAccentColorKey, out var colorCode)
? colorCode
: "");
}
}
#endregion
#region COMMANDS
@ -457,13 +492,17 @@ namespace IW4MAdmin.Application
// 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
cmdConfig.CommandPrefix = _appConfig.CommandPrefix;
cmdConfig.BroadcastCommandPrefix = _appConfig.BroadcastCommandPrefix;
cmdConfig.CommandPrefix = _appConfig?.CommandPrefix ?? "!";
cmdConfig.BroadcastCommandPrefix = _appConfig?.BroadcastCommandPrefix ?? "@";
foreach (var cmd in commandsToAddToConfig)
{
if (cmdConfig.Commands.ContainsKey(cmd.CommandConfigNameForType()))
{
continue;
}
cmdConfig.Commands.Add(cmd.CommandConfigNameForType(),
new CommandProperties()
new CommandProperties
{
Name = cmd.Name,
Alias = cmd.Alias,
@ -512,7 +551,7 @@ namespace IW4MAdmin.Application
_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.LogInformation("Finishing initialization and now monitoring [{Server}]", ServerInstance.Hostname);
}
// add the start event for this server
@ -585,6 +624,11 @@ namespace IW4MAdmin.Application
return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList();
}
public EFClient FindActiveClient(EFClient client) =>client.ClientNumber < 0 ?
GetActiveClients()
.FirstOrDefault(c => c.NetworkId == client.NetworkId) ?? client :
client;
public ClientService GetClientService()
{
return ClientSvc;

View File

@ -13,4 +13,5 @@ if not exist "%TargetDir%Plugins" (
md "%TargetDir%Plugins"
)
xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
del "%TargetDir%Plugins\SQLite*"

View File

@ -23,6 +23,7 @@ echo setting up default folders
if not exist "%PublishDir%\Configuration" md "%PublishDir%\Configuration"
move "%PublishDir%\DefaultSettings.json" "%PublishDir%\Configuration\"
if not exist "%PublishDir%\Lib\" md "%PublishDir%\Lib\"
del "%PublishDir%\Microsoft.CodeAnalysis*.dll" /F /Q
move "%PublishDir%\*.dll" "%PublishDir%\Lib\"
move "%PublishDir%\*.json" "%PublishDir%\Lib\"
move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes"
@ -30,16 +31,37 @@ move "%PublishDir%\ru" "%PublishDir%\Lib\ru"
move "%PublishDir%\de" "%PublishDir%\Lib\de"
move "%PublishDir%\pt" "%PublishDir%\Lib\pt"
move "%PublishDir%\es" "%PublishDir%\Lib\es"
rmdir /Q /S "%PublishDir%\cs"
rmdir /Q /S "%PublishDir%\fr"
rmdir /Q /S "%PublishDir%\it"
rmdir /Q /S "%PublishDir%\ja"
rmdir /Q /S "%PublishDir%\ko"
rmdir /Q /S "%PublishDir%\pl"
rmdir /Q /S "%PublishDir%\pt-BR"
rmdir /Q /S "%PublishDir%\tr"
rmdir /Q /S "%PublishDir%\zh-Hans"
rmdir /Q /S "%PublishDir%\zh-Hant"
if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs"
echo making start scripts
@(echo @echo off && echo @title IW4MAdmin && echo set DOTNET_CLI_TELEMETRY_OPTOUT=1 && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%PublishDir%\StartIW4MAdmin.cmd"
@(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%PublishDir%\StartIW4MAdmin.sh"
echo copying update scripts
copy "%SourceDir%\DeploymentFiles\UpdateIW4MAdmin.ps1" "%PublishDir%\UpdateIW4MAdmin.ps1"
copy "%SourceDir%\DeploymentFiles\UpdateIW4MAdmin.sh" "%PublishDir%\UpdateIW4MAdmin.sh"
echo moving front-end library dependencies
if not exist "%PublishDir%\wwwroot\font" mkdir "%PublishDir%\wwwroot\font"
move "WebfrontCore\wwwroot\lib\open-iconic\font\fonts\*.*" "%PublishDir%\wwwroot\font\"
if exist "%PublishDir%\wwwroot\lib" rd /s /q "%PublishDir%\wwwroot\lib"
if not exist "%PublishDir%\wwwroot\css" mkdir "%PublishDir%\wwwroot\css"
move "WebfrontCore\wwwroot\css\global.min.css" "%PublishDir%\wwwroot\css\global.min.css"
if not exist "%PublishDir%\wwwroot\js" mkdir "%PublishDir%\wwwroot\js"
move "%SourceDir%\WebfrontCore\wwwroot\js\global.min.js" "%PublishDir%\wwwroot\js\global.min.js"
if not exist "%PublishDir%\wwwroot\images" mkdir "%PublishDir%\wwwroot\images"
xcopy "%SourceDir%\WebfrontCore\wwwroot\images" "%PublishDir%\wwwroot\images" /E /H /C /I
echo setting permissions...
cacls "%PublishDir%" /t /e /p Everyone:F
cacls "%PublishDir%" /t /e /p Everyone:F

View File

@ -0,0 +1,59 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Finds player by name
/// </summary>
public class FindPlayerCommand : Command
{
public FindPlayerCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "find";
Description = _translationLookup["COMMANDS_FIND_DESC"];
Alias = "f";
Permission = EFClient.Permission.Administrator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument()
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
if (gameEvent.Data.Length < 3)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_FIND_MIN"]);
return;
}
var players = await gameEvent.Owner.Manager.GetClientService().FindClientsByIdentifier(gameEvent.Data);
if (!players.Any())
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_FIND_EMPTY"]);
return;
}
foreach (var client in players)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_FIND_FORMAT_V2"].FormatExt(client.Name,
client.ClientId, Utilities.ConvertLevelToColor((EFClient.Permission) client.LevelInt, client.Level),
client.IPAddress, (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture()));
}
}
}
}

View File

@ -0,0 +1,93 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints help information
/// </summary>
public class HelpCommand : Command
{
public HelpCommand(CommandConfiguration config, ITranslationLookup translationLookup) :
base(config, translationLookup)
{
Name = "help";
Description = translationLookup["COMMANDS_HELP_DESC"];
Alias = "h";
Permission = EFClient.Permission.User;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_COMMANDS"],
Required = false
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var searchTerm = gameEvent.Data.Trim();
var availableCommands = gameEvent.Owner.Manager.Commands.Distinct().Where(command =>
command.SupportedGames == null || !command.SupportedGames.Any() ||
command.SupportedGames.Contains(gameEvent.Owner.GameName))
.Where(command => gameEvent.Origin.Level >= command.Permission);
if (searchTerm.Length > 2)
{
var matchingCommand = availableCommands.FirstOrDefault(command =>
command.Name.Equals(searchTerm, StringComparison.InvariantCultureIgnoreCase) ||
command.Alias.Equals(searchTerm, StringComparison.InvariantCultureIgnoreCase));
if (matchingCommand != null)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_HELP_SEARCH_RESULT"]
.FormatExt(matchingCommand.Name, matchingCommand.Alias));
gameEvent.Origin.Tell(matchingCommand.Syntax);
}
else
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_HELP_NOTFOUND"]);
}
}
else
{
var commandStrings = availableCommands.Select((command, index) =>
new
{
response = $" {_translationLookup["COMMANDS_HELP_LIST_FORMAT"].FormatExt(command.Name)} ",
index
});
var helpResponse = new StringBuilder();
foreach (var item in commandStrings)
{
helpResponse.Append(item.response);
if (item.index == 0 || item.index % 4 != 0)
{
continue;
}
gameEvent.Origin.Tell(helpResponse.ToString());
helpResponse = new StringBuilder();
}
gameEvent.Origin.Tell(helpResponse.ToString());
gameEvent.Origin.Tell(_translationLookup["COMMANDS_HELP_MOREINFO"]);
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Lists all unmasked admins
/// </summary>
public class ListAdminsCommand : Command
{
public ListAdminsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "admins";
Description = _translationLookup["COMMANDS_ADMINS_DESC"];
Alias = "a";
Permission = EFClient.Permission.User;
RequiresTarget = false;
}
public static string OnlineAdmins(Server server, ITranslationLookup lookup)
{
var onlineAdmins = server.GetClientsAsList()
.Where(p => p.Level > EFClient.Permission.Flagged)
.Where(p => !p.Masked)
.Select(p =>
$"[(Color::Yellow){Utilities.ConvertLevelToColor(p.Level, p.ClientPermission.Name)}(Color::White)] {p.Name}")
.ToList();
return onlineAdmins.Any() ? string.Join(Environment.NewLine, onlineAdmins) : lookup["COMMANDS_ADMINS_NONE"];
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
foreach (var line in OnlineAdmins(gameEvent.Owner, _translationLookup).Split(Environment.NewLine))
{
var _ = gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix)
? gameEvent.Owner.Broadcast(line)
: gameEvent.Origin.Tell(line);
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Lists alises of specified client
/// </summary>
public class ListAliasesCommand : Command
{
public ListAliasesCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "alias";
Description = _translationLookup["COMMANDS_ALIAS_DESC"];
Alias = "known";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument()
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true,
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var message = new StringBuilder();
var names = new List<string>(gameEvent.Target.AliasLink.Children.Select(a => a.Name));
var ips = new List<string>(gameEvent.Target.AliasLink.Children.Select(a => a.IPAddress.ConvertIPtoString())
.Distinct());
gameEvent.Origin.Tell($"[(Color::Accent){gameEvent.Target}(Color::White)]");
message.Append($"{_translationLookup["COMMANDS_ALIAS_ALIASES"]}: ");
message.Append(string.Join(" | ", names));
gameEvent.Origin.Tell(message.ToString());
message.Clear();
message.Append($"{_translationLookup["COMMANDS_ALIAS_IPS"]}: ");
message.Append(string.Join(" | ", ips));
gameEvent.Origin.Tell(message.ToString());
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,37 @@
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// List online clients
/// </summary>
public class ListClientsCommand : Command
{
public ListClientsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "list";
Description = _translationLookup["COMMANDS_LIST_DESC"];
Alias = "l";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var clientList = gameEvent.Owner.GetClientsAsList()
.Select(client =>
$"[(Color::Accent){client.ClientPermission.Name}(Color::White){(string.IsNullOrEmpty(client.Tag) ? "" : $" {client.Tag}")}(Color::White)][(Color::Yellow)#{client.ClientNumber}(Color::White)] {client.Name}")
.ToArray();
gameEvent.Origin.Tell(clientList);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Lists the loaded plugins
/// </summary>
public class ListPluginsCommand : Command
{
private readonly IEnumerable<IPlugin> _plugins;
public ListPluginsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IEnumerable<IPlugin> plugins) : base(config, translationLookup)
{
Name = "plugins";
Description = _translationLookup["COMMANDS_PLUGINS_DESC"];
Alias = "p";
Permission = EFClient.Permission.Administrator;
RequiresTarget = false;
_plugins = plugins;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_PLUGINS_LOADED"]);
foreach (var plugin in _plugins.Where(plugin => !plugin.IsParser))
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_LIST_PLUGINS_FORMAT"]
.FormatExt(plugin.Name, plugin.Version, plugin.Author));
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// List all reports on the server
/// </summary>
public class ListReportsCommand : Command
{
public ListReportsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "reports";
Description = _translationLookup["COMMANDS_REPORTS_DESC"];
Alias = "reps";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_CLEAR"],
Required = false
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
if (gameEvent.Data != null && gameEvent.Data.ToLower().Contains(_translationLookup["COMMANDS_ARGS_CLEAR"]))
{
gameEvent.Owner.Reports = new List<Report>();
gameEvent.Origin.Tell(_translationLookup["COMMANDS_REPORTS_CLEAR_SUCCESS"]);
return Task.CompletedTask;
}
if (gameEvent.Owner.Reports.Count < 1)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_REPORTS_NONE"]);
return Task.CompletedTask;
}
foreach (var report in gameEvent.Owner.Reports)
{
gameEvent.Origin.Tell(
$"(Color::Accent){report.Origin.Name}(Color::White) -> (Color::Red){report.Target.Name}(Color::White): {report.Reason}");
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,128 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Data.Models.Client;
using IW4MAdmin.Application.Extensions;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Commands
{
public class MapAndGameTypeCommand : Command
{
private const string ArgumentRegexPattern = "(?:\"([^\"]+)\"|([^\\s]+)) (?:\"([^\"]+)\"|([^\\s]+))";
private readonly ILogger _logger;
private readonly DefaultSettings _defaultSettings;
public MapAndGameTypeCommand(ILogger<MapAndGameTypeCommand> logger, CommandConfiguration config,
DefaultSettings defaultSettings, ITranslationLookup layout) : base(config, layout)
{
Name = "mapandgametype";
Description = _translationLookup["COMMANDS_MAG_DESCRIPTION"];
Alias = "mag";
Permission = EFClient.Permission.Administrator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMADS_MAG_ARG_1"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMADS_MAG_ARG_2"],
Required = true
}
};
_logger = logger;
_defaultSettings = defaultSettings;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var match = Regex.Match(gameEvent.Data.Trim(), ArgumentRegexPattern,
RegexOptions.Compiled | RegexOptions.IgnoreCase);
if (!match.Success)
{
gameEvent.Origin.Tell(Syntax);
return;
}
string map;
string gametype;
if (match.Groups.Count > 3)
{
map = match.Groups[2].ToString();
gametype = match.Groups[4].ToString();
}
else
{
map = match.Groups[1].ToString();
gametype = match.Groups[3].ToString();
}
var matchingMaps = gameEvent.Owner.FindMap(map);
var matchingGametypes = _defaultSettings.FindGametype(gametype, gameEvent.Owner.GameName);
if (matchingMaps.Count > 1)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_MAG_MULTIPLE_MAPS"]);
foreach (var matchingMap in matchingMaps)
{
gameEvent.Origin.Tell(
$"[(Color::Yellow){matchingMap.Alias}(Color::White)] [(Color::Yellow){matchingMap.Name}(Color::White)]");
}
return;
}
if (matchingGametypes.Count > 1)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_MAG_MULTIPLE_GAMETYPES"]);
foreach (var matchingGametype in matchingGametypes)
{
gameEvent.Origin.Tell(
$"[(Color::Yellow){matchingGametype.Alias}(Color::White)] [(Color::Yellow){matchingGametype.Name}(Color::White)]");
}
return;
}
map = matchingMaps.FirstOrDefault()?.Name ?? map;
gametype = matchingGametypes.FirstOrDefault()?.Name ?? gametype;
var hasMatchingGametype = matchingGametypes.Any();
_logger.LogDebug("Changing map to {Map} and gametype {Gametype}", map, gametype);
await gameEvent.Owner.SetDvarAsync("g_gametype", gametype);
gameEvent.Owner.Broadcast(_translationLookup["COMMANDS_MAP_SUCCESS"].FormatExt(map));
await Task.Delay(gameEvent.Owner.Manager.GetApplicationSettings().Configuration().MapChangeDelaySeconds);
switch (gameEvent.Owner.GameName)
{
case Server.Game.IW5:
await gameEvent.Owner.ExecuteCommandAsync(
$"load_dsr {(hasMatchingGametype ? gametype.ToUpper() + "_default" : gametype)}");
await gameEvent.Owner.ExecuteCommandAsync($"map {map}");
break;
case Server.Game.T6:
await gameEvent.Owner.ExecuteCommandAsync($"exec {gametype}.cfg");
await gameEvent.Owner.ExecuteCommandAsync($"map {map}");
break;
default:
await gameEvent.Owner.ExecuteCommandAsync($"map {map}");
break;
}
}
}
}

View File

@ -0,0 +1,78 @@
using System;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Misc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Commands
{
public class OfflineMessageCommand : Command
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
private const short MaxLength = 1024;
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout)
{
Name = "offlinemessage";
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
Alias = "om";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
_contextFactory = contextFactory;
_logger = logger;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
if (gameEvent.Data.Length > MaxLength)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
return;
}
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
return;
}
if (gameEvent.Target.IsIngame)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"].FormatExt(gameEvent.Target.Name));
return;
}
await using var context = _contextFactory.CreateContext(enableTracking: false);
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
var newMessage = new EFInboxMessage()
{
SourceClientId = gameEvent.Origin.ClientId,
DestinationClientId = gameEvent.Target.ClientId,
ServerId = server.Id,
Message = gameEvent.Data,
};
try
{
context.Set<EFInboxMessage>().Add(newMessage);
await context.SaveChangesAsync();
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SUCCESS"]);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not save offline message {@Message}", newMessage);
throw;
}
}
}
}

View File

@ -0,0 +1,45 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Sends a private message to another player
/// </summary>
public class PrivateMessageCommand : Command
{
public PrivateMessageCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{
Name = "privatemessage";
Description = _translationLookup["COMMANDS_PM_DESC"];
Alias = "pm";
Permission = EFClient.Permission.User;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_MESSAGE"],
Required = true
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Target.Tell(_translationLookup["COMMANDS_PRIVATE_MESSAGE_FORMAT"].FormatExt(gameEvent.Origin.Name, gameEvent.Data));
gameEvent.Origin.Tell(_translationLookup["COMMANDS_PRIVATE_MESSAGE_RESULT"]
.FormatExt(gameEvent.Target.Name, gameEvent.Data));
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,79 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using EFClient = Data.Models.Client.EFClient;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Commands
{
public class ReadMessageCommand : Command
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
public ReadMessageCommand(CommandConfiguration config, ITranslationLookup layout,
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout)
{
Name = "readmessage";
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
Alias = "rm";
Permission = EFClient.Permission.Flagged;
_contextFactory = contextFactory;
_logger = logger;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
try
{
await using var context = _contextFactory.CreateContext();
var inboxItems = await context.InboxMessages
.Include(message => message.SourceClient)
.ThenInclude(client => client.CurrentAlias)
.Where(message => message.DestinationClientId == gameEvent.Origin.ClientId)
.Where(message => !message.IsDelivered)
.ToListAsync();
if (!inboxItems.Any())
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_READ_MESSAGE_NONE"]);
return;
}
var index = 1;
foreach (var inboxItem in inboxItems)
{
await gameEvent.Origin.Tell(_translationLookup["COMMANDS_READ_MESSAGE_SUCCESS"]
.FormatExt($"{index}/{inboxItems.Count}", inboxItem.SourceClient.CurrentAlias.Name))
.WaitAsync();
foreach (var messageFragment in inboxItem.Message.FragmentMessageForDisplay())
{
await gameEvent.Origin.Tell(messageFragment).WaitAsync();
}
index++;
}
inboxItems.ForEach(item => { item.IsDelivered = true; });
context.UpdateRange(inboxItems);
await context.SaveChangesAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "Could not retrieve offline messages for {Client}", gameEvent.Origin.ToString());
throw;
}
}
}
}

View File

@ -0,0 +1,77 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Report client for given reason
/// </summary>
public class ReportClientCommand : Command
{
public ReportClientCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "report";
Description = _translationLookup["COMMANDS_REPORT_DESC"];
Alias = "rep";
Permission = EFClient.Permission.User;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_REASON"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent commandEvent)
{
if (commandEvent.Data.ToLower().Contains("camp"))
{
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_CAMP"]);
return;
}
var success = false;
switch ((await commandEvent.Target.Report(commandEvent.Data, commandEvent.Origin)
.WaitAsync(Utilities.DefaultCommandTimeout, commandEvent.Owner.Manager.CancellationToken)).FailReason)
{
case GameEvent.EventFailReason.None:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_SUCCESS"]);
success = true;
break;
case GameEvent.EventFailReason.Exception:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_DUPLICATE"]);
break;
case GameEvent.EventFailReason.Permission:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL"]
.FormatExt(commandEvent.Target.Name));
break;
case GameEvent.EventFailReason.Invalid:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_SELF"]);
break;
case GameEvent.EventFailReason.Throttle:
commandEvent.Origin.Tell(_translationLookup["COMMANDS_REPORT_FAIL_TOOMANY"]);
break;
}
if (success)
{
commandEvent.Owner.ToAdmins(
$"(Color::Accent){commandEvent.Origin.Name}(Color::White) -> (Color::Red){commandEvent.Target.Name}(Color::White): {commandEvent.Data}");
}
}
}
}

View File

@ -0,0 +1,46 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints out a message to all clients on all servers
/// </summary>
public class SayAllCommand : Command
{
public SayAllCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "sayall";
Description = _translationLookup["COMMANDS_SAY_ALL_DESC"];
Alias = "sa";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_MESSAGE"],
Required = true
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var message = $"(Color::Accent){gameEvent.Origin.Name}(Color::White) - (Color::Red){gameEvent.Data}";
foreach (var server in gameEvent.Owner.Manager.GetServers())
{
server.Broadcast(message, gameEvent.Origin);
}
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SAY_SUCCESS"]);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,42 @@
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using EFClient = Data.Models.Client.EFClient;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints out a message to all clients on the server
/// </summary>
public class SayCommand : Command
{
public SayCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "say";
Description = _translationLookup["COMMANDS_SAY_DESC"];
Alias = "s";
Permission = EFClient.Permission.Moderator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_MESSAGE"],
Required = true
}
};
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Owner.Broadcast(
_translationLookup["COMMANDS_SAY_FORMAT"].FormatExt(gameEvent.Origin.Name, gameEvent.Data),
gameEvent.Origin);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_SAY_SUCCESS"]);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,38 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands
{
/// <summary>
/// Prints client information
/// </summary>
public class WhoAmICommand : Command
{
public WhoAmICommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "whoami";
Description = _translationLookup["COMMANDS_WHO_DESC"];
Alias = "who";
Permission = EFClient.Permission.User;
RequiresTarget = false;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var you =
"[(Color::Yellow)#{{clientNumber}}(Color::White)] [(Color::Yellow)@{{clientId}}(Color::White)] [{{networkId}}] [{{ip}}] [(Color::Cyan){{level}}(Color::White){{tag}}(Color::White)] {{name}}"
.FormatExt(gameEvent.Origin.ClientNumber,
gameEvent.Origin.ClientId, gameEvent.Origin.GuidString,
gameEvent.Origin.IPAddressString, gameEvent.Origin.ClientPermission.Name,
string.IsNullOrEmpty(gameEvent.Origin.Tag) ? "" : $" {gameEvent.Origin.Tag}",
gameEvent.Origin.Name);
gameEvent.Origin.Tell(you);
return Task.CompletedTask;
}
}
}

View File

@ -3,7 +3,13 @@
"Using": [
"Serilog.Sinks.File"
],
"MinimumLevel": "Information",
"MinimumLevel": {
"Default": "Information",
"Override": {
"System": "Warning",
"Microsoft": "Warning"
}
},
"WriteTo": [
{
"Name": "File",
@ -12,6 +18,13 @@
"rollingInterval": "Day",
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}",
"RestrictedToMinimumLevel": "Fatal"
}
}
],
"Enrich": [

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

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,8 @@ namespace IW4MAdmin.Application.EventParsers
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations;
private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig;
private readonly Dictionary<ParserRegex, GameEvent.EventType> _regexMap;
private readonly Dictionary<string, GameEvent.EventType> _eventTypeMap;
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig)
{
@ -48,7 +50,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -63,7 +65,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,32})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -78,7 +80,28 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
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]+ )";
_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; }
@ -91,6 +114,28 @@ namespace IW4MAdmin.Application.EventParsers
public string Name { get; set; } = "Call of Duty";
private (GameEvent.EventType type, string eventKey) GetEventTypeFromLine(string logLine)
{
var lineSplit = logLine.Split(';');
if (lineSplit.Length > 1)
{
var type = lineSplit[0];
return _eventTypeMap.ContainsKey(type) ? (_eventTypeMap[type], type): (GameEvent.EventType.Unknown, lineSplit[0]);
}
foreach (var (key, value) in _regexMap)
{
var result = key.PatternMatcher.Match(logLine);
if (result.Success)
{
return (value, null);
}
}
return (GameEvent.EventType.Unknown, null);
}
public virtual GameEvent GenerateGameEvent(string logLine)
{
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
@ -104,7 +149,7 @@ namespace IW4MAdmin.Application.EventParsers
.Values
.Skip(2)
// this converts the timestamp into seconds passed
.Select((_value, index) => long.Parse(_value.ToString()) * (index == 0 ? 60 : 1))
.Select((value, index) => long.Parse(value.ToString()) * (index == 0 ? 60 : 1))
.Sum();
}
@ -114,33 +159,34 @@ namespace IW4MAdmin.Application.EventParsers
}
// 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(';');
string eventType = lineSplit[0];
var eventParseResult = GetEventTypeFromLine(logLine);
var eventType = eventParseResult.type;
_logger.LogDebug(logLine);
if (eventType == "say" || eventType == "sayteam")
if (eventType == GameEvent.EventType.Say)
{
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
if (matchResult.Success)
{
string message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.ToString()
var message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.Replace("\x15", "")
.Trim();
if (message.Length > 0)
{
string originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
var originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]];
long originId = originIdString.IsBotGuid() ?
var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
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))
{
@ -172,26 +218,26 @@ namespace IW4MAdmin.Application.EventParsers
}
}
if (eventType == "K")
if (eventType == GameEvent.EventType.Kill)
{
var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (match.Success)
{
string originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
string originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
string targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
var originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
var originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]];
var targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]];
long originId = originIdString.IsBotGuid() ?
var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
long targetId = targetIdString.IsBotGuid() ?
var targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
int originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
var originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
@ -206,26 +252,26 @@ namespace IW4MAdmin.Application.EventParsers
}
}
if (eventType == "D")
if (eventType == GameEvent.EventType.Damage)
{
var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (match.Success)
{
string originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
string originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
string targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
var originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]];
var originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]];
var targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]];
long originId = originIdString.IsBotGuid() ?
var originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
long targetId = targetIdString.IsBotGuid() ?
var targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
int originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
var originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
var targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
@ -240,16 +286,16 @@ namespace IW4MAdmin.Application.EventParsers
}
}
if (eventType == "J")
if (eventType == GameEvent.EventType.PreConnect)
{
var match = Configuration.Join.PatternMatcher.Match(logLine);
if (match.Success)
{
string originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
var originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]];
long networkId = originIdString.IsBotGuid() ?
var networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
@ -261,10 +307,10 @@ namespace IW4MAdmin.Application.EventParsers
{
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,
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,
},
Extra = originIdString,
@ -276,16 +322,16 @@ namespace IW4MAdmin.Application.EventParsers
}
}
if (eventType == "Q")
if (eventType == GameEvent.EventType.PreDisconnect)
{
var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (match.Success)
{
string originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
var originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]];
var originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]];
long networkId = originIdString.IsBotGuid() ?
var networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
@ -297,10 +343,10 @@ namespace IW4MAdmin.Application.EventParsers
{
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,
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
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
@ -311,7 +357,7 @@ namespace IW4MAdmin.Application.EventParsers
}
}
if (eventType.Contains("ExitLevel") || eventType.Contains("ShutdownGame"))
if (eventType == GameEvent.EventType.MapEnd)
{
return new GameEvent()
{
@ -325,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()
{
@ -342,26 +388,37 @@ namespace IW4MAdmin.Application.EventParsers
};
}
if (_customEventRegistrations.ContainsKey(eventType))
if (eventParseResult.eventKey == null || !_customEventRegistrations.ContainsKey(eventParseResult.eventKey))
{
var eventModifier = _customEventRegistrations[eventType];
try
return new GameEvent()
{
return eventModifier.Item2(logLine, Configuration, new GameEvent()
{
Type = GameEvent.EventType.Other,
Data = logLine,
Subtype = eventModifier.Item1,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
});
}
Type = GameEvent.EventType.Unknown,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
catch (Exception e)
var eventModifier = _customEventRegistrations[eventParseResult.eventKey];
try
{
return eventModifier.Item2(logLine, Configuration, new GameEvent()
{
_logger.LogError(e, $"Could not handle custom event generation");
}
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()

View File

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

View File

@ -1,6 +1,10 @@
using IW4MAdmin.Application.Misc;
using System;
using System.Collections.Generic;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces;
using System.Linq;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Extensions
{
@ -13,9 +17,19 @@ namespace IW4MAdmin.Application.Extensions
/// <returns></returns>
public static string CommandConfigNameForType(this IManagerCommand command)
{
return command.GetType() == typeof(ScriptCommand) ?
$"{char.ToUpper(command.Name[0])}{command.Name.Substring(1)}Command" :
command.GetType().Name;
return command.GetType() == typeof(ScriptCommand)
? $"{char.ToUpper(command.Name[0])}{command.Name.Substring(1)}Command"
: command.GetType().Name;
}
public static IList<Map> FindMap(this Server server, string mapName) => server.Maps.Where(map =>
map.Name.Equals(mapName, StringComparison.InvariantCultureIgnoreCase) ||
map.Alias.Equals(mapName, StringComparison.InvariantCultureIgnoreCase)).ToList();
public static IList<Gametype> FindGametype(this DefaultSettings settings, string gameType, Server.Game? game = null) =>
settings.Gametypes?.Where(gt => game == null || gt.Game == game)
.SelectMany(gt => gt.Gametypes).Where(gt =>
gt.Alias.Contains(gameType, StringComparison.CurrentCultureIgnoreCase) ||
gt.Name.Contains(gameType, StringComparison.CurrentCultureIgnoreCase)).ToList();
}
}

View File

@ -78,8 +78,10 @@ namespace IW4MAdmin.Application.Extensions
case "mysql":
var appendTimeout = !appConfig.ConnectionString.Contains("default command timeout",
StringComparison.InvariantCultureIgnoreCase);
var connectionString =
appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : "");
services.AddSingleton(sp => (DbContextOptions) new DbContextOptionsBuilder<MySqlDatabaseContext>()
.UseMySql(appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : ""),
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString),
mysqlOptions => mysqlOptions.EnableRetryOnFailure())
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options);
return services;
@ -92,7 +94,7 @@ namespace IW4MAdmin.Application.Extensions
postgresqlOptions =>
{
postgresqlOptions.EnableRetryOnFailure();
postgresqlOptions.SetPostgresVersion(new Version("9.4"));
postgresqlOptions.SetPostgresVersion(new Version("12.9"));
})
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options);
return services;
@ -101,4 +103,4 @@ namespace IW4MAdmin.Application.Extensions
}
}
}
}
}

View File

@ -1,4 +1,5 @@
using IW4MAdmin.Application.Misc;
using System.Threading.Tasks;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Factories
@ -17,7 +18,17 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns>
public IConfigurationHandler<T> GetConfigurationHandler<T>(string name) where T : IBaseConfiguration
{
return new BaseConfigurationHandler<T>(name);
var handler = new BaseConfigurationHandler<T>(name);
handler.BuildAsync().Wait();
return handler;
}
/// <inheritdoc/>
public async Task<IConfigurationHandler<T>> GetConfigurationHandlerAsync<T>(string name) where T : IBaseConfiguration
{
var handler = new BaseConfigurationHandler<T>(name);
await handler.BuildAsync();
return handler;
}
}
}

View File

@ -22,7 +22,7 @@ namespace IW4MAdmin.Application.Factories
/// </summary>
/// <param name="translationLookup"></param>
/// <param name="rconConnectionFactory"></param>
public GameServerInstanceFactory(ITranslationLookup translationLookup,
public GameServerInstanceFactory(ITranslationLookup translationLookup,
IMetaService metaService,
IServiceProvider serviceProvider)
{
@ -39,7 +39,10 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns>
public Server CreateServer(ServerConfiguration config, IManager manager)
{
return new IW4MServer(config, _translationLookup, _metaService, _serviceProvider, _serviceProvider.GetRequiredService<IClientNoticeMessageFormatter>(), _serviceProvider.GetRequiredService<ILookupCache<EFServer>>());
return new IW4MServer(config,
_serviceProvider.GetRequiredService<CommandConfiguration>(), _translationLookup, _metaService,
_serviceProvider, _serviceProvider.GetRequiredService<IClientNoticeMessageFormatter>(),
_serviceProvider.GetRequiredService<ILookupCache<EFServer>>());
}
}
}
}

View File

@ -1,7 +1,13 @@
using IW4MAdmin.Application.RCon;
using System;
using System.Net;
using SharedLibraryCore.Interfaces;
using System.Text;
using Integrations.Cod;
using Integrations.Source;
using Integrations.Source.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Factories
{
@ -10,28 +16,31 @@ namespace IW4MAdmin.Application.Factories
/// </summary>
internal class RConConnectionFactory : IRConConnectionFactory
{
private static readonly Encoding gameEncoding = Encoding.GetEncoding("windows-1252");
private readonly ILogger<RConConnection> _logger;
private static readonly Encoding GameEncoding = Encoding.GetEncoding("windows-1252");
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// Base constructor
/// </summary>
/// <param name="logger"></param>
public RConConnectionFactory(ILogger<RConConnection> logger)
public RConConnectionFactory(IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
/// <summary>
/// creates a new rcon connection instance
/// </summary>
/// <param name="ipAddress">ip address of the server</param>
/// <param name="port">port of the server</param>
/// <param name="password">rcon password of the server</param>
/// <returns></returns>
public IRConConnection CreateConnection(string ipAddress, int port, string password)
/// <inheritdoc/>
public IRConConnection CreateConnection(IPEndPoint ipEndpoint, string password, string rconEngine)
{
return new RConConnection(ipAddress, port, password, _logger, gameEncoding);
return rconEngine switch
{
"COD" => new CodRConConnection(ipEndpoint, password,
_serviceProvider.GetRequiredService<ILogger<CodRConConnection>>(), GameEncoding,
_serviceProvider.GetRequiredService<ApplicationConfiguration>()?.ServerConnectionAttempts ?? 6),
"Source" => new SourceRConConnection(
_serviceProvider.GetRequiredService<ILogger<SourceRConConnection>>(),
_serviceProvider.GetRequiredService<IRConClientFactory>(), ipEndpoint, password),
_ => throw new ArgumentException($"No supported RCon engine available for '{rconEngine}'")
};
}
}
}
}

View File

@ -6,6 +6,7 @@ using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Models.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -30,7 +31,7 @@ namespace IW4MAdmin.Application.Factories
/// <inheritdoc/>
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission,
bool isTargetRequired, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction)
bool isTargetRequired, IEnumerable<(string, bool)> args, Func<GameEvent, Task> executeAction, Server.Game[] supportedGames)
{
var permissionEnum = Enum.Parse<EFClient.Permission>(permission);
var argsArray = args.Select(_arg => new CommandArgument
@ -40,7 +41,7 @@ namespace IW4MAdmin.Application.Factories
}).ToArray();
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction,
_config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>());
_config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>(), supportedGames);
}
}
}

View File

@ -13,6 +13,7 @@ namespace IW4MAdmin.Application
{
private readonly EventLog _eventLog;
private readonly ILogger _logger;
private readonly IEventPublisher _eventPublisher;
private static readonly GameEvent.EventType[] overrideEvents = new[]
{
GameEvent.EventType.Connect,
@ -21,10 +22,11 @@ namespace IW4MAdmin.Application
GameEvent.EventType.Stop
};
public GameEventHandler(ILogger<GameEventHandler> logger)
public GameEventHandler(ILogger<GameEventHandler> logger, IEventPublisher eventPublisher)
{
_eventLog = new EventLog();
_logger = logger;
_eventPublisher = eventPublisher;
}
public void HandleEvent(IManager manager, GameEvent gameEvent)
@ -32,6 +34,7 @@ namespace IW4MAdmin.Application
if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
{
EventApi.OnGameEvent(gameEvent);
_eventPublisher.Publish(gameEvent);
Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent));
}
else

View File

@ -11,6 +11,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
@ -22,6 +24,8 @@ using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models;
using Data.Models.Server;
using IW4MAdmin.Application.Commands;
using Microsoft.EntityFrameworkCore;
using static Data.Models.Client.EFClient;
namespace IW4MAdmin
@ -39,25 +43,30 @@ namespace IW4MAdmin
private readonly IServiceProvider _serviceProvider;
private readonly IClientNoticeMessageFormatter _messageFormatter;
private readonly ILookupCache<EFServer> _serverCache;
private readonly CommandConfiguration _commandConfiguration;
public IW4MServer(
ServerConfiguration serverConfiguration,
CommandConfiguration commandConfiguration,
ITranslationLookup lookup,
IMetaService metaService,
IServiceProvider serviceProvider,
IClientNoticeMessageFormatter messageFormatter,
ILookupCache<EFServer> serverCache) : base(serviceProvider.GetRequiredService<ILogger<Server>>(),
#pragma warning disable CS0612
serviceProvider.GetRequiredService<SharedLibraryCore.Interfaces.ILogger>(),
#pragma warning restore CS0612
serverConfiguration,
serviceProvider.GetRequiredService<IManager>(),
serviceProvider.GetRequiredService<IRConConnectionFactory>(),
serviceProvider.GetRequiredService<IGameLogReaderFactory>())
serviceProvider.GetRequiredService<IGameLogReaderFactory>(), serviceProvider)
{
_translationLookup = lookup;
_metaService = metaService;
_serviceProvider = serviceProvider;
_messageFormatter = messageFormatter;
_serverCache = serverCache;
_commandConfiguration = commandConfiguration;
}
public override async Task<EFClient> OnClientConnected(EFClient clientFromLog)
@ -74,6 +83,8 @@ namespace IW4MAdmin
client = await Manager.GetClientService().Create(clientFromLog);
}
client.CopyAdditionalProperties(clientFromLog);
// this is only a temporary version until the IPAddress is transmitted
client.CurrentAlias = new EFAlias()
{
@ -156,7 +167,7 @@ namespace IW4MAdmin
{
try
{
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration());
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
}
catch (CommandException e)
@ -235,11 +246,11 @@ namespace IW4MAdmin
try
{
await (plugin.OnEventAsync(gameEvent, this)).WithWaitCancellation(tokenSource.Token);
await plugin.OnEventAsync(gameEvent, this).WithWaitCancellation(tokenSource.Token);
}
catch (Exception ex)
{
Console.WriteLine(loc["SERVER_PLUGIN_ERROR"]);
Console.WriteLine(loc["SERVER_PLUGIN_ERROR"].FormatExt(plugin.Name, ex.GetType().Name));
ServerLogger.LogError(ex, "Could not execute {methodName} for plugin {plugin}",
nameof(plugin.OnEventAsync), plugin.Name);
}
@ -303,6 +314,11 @@ namespace IW4MAdmin
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
}
if (!string.IsNullOrEmpty(CustomSayName))
{
await this.SetDvarAsync("sv_sayname", CustomSayName);
}
Throttled = false;
}
@ -336,7 +352,26 @@ namespace IW4MAdmin
E.Origin.Tag = clientTag.LinkedMeta.Value;
}
await E.Origin.OnJoin(E.Origin.IPAddress);
try
{
var factory = _serviceProvider.GetRequiredService<IDatabaseContextFactory>();
await using var context = factory.CreateContext(enableTracking: false);
var messageCount = await context.InboxMessages
.CountAsync(msg => msg.DestinationClientId == E.Origin.ClientId && !msg.IsDelivered);
if (messageCount > 0)
{
E.Origin.Tell(_translationLookup["SERVER_JOIN_OFFLINE_MESSAGES"]);
}
}
catch (Exception ex)
{
ServerLogger.LogError(ex, "Could not get offline message count for {Client}", E.Origin.ToString());
throw;
}
await E.Origin.OnJoin(E.Origin.IPAddress, Manager.GetApplicationSettings().Configuration().EnableImplicitAccountLinking);
}
}
@ -402,7 +437,7 @@ namespace IW4MAdmin
if (E.Origin.Level > Permission.Moderator)
{
E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
E.Origin.Tell(loc["SERVER_REPORT_COUNT_V2"].FormatExt(E.Owner.Reports.Count));
}
}
@ -436,7 +471,7 @@ namespace IW4MAdmin
Link = E.Target.AliasLink
};
var addedPenalty = await Manager.GetPenaltyService().Create(newPenalty);
await Manager.GetPenaltyService().Create(newPenalty);
E.Target.SetLevel(Permission.Flagged, E.Origin);
}
@ -588,7 +623,7 @@ namespace IW4MAdmin
{
try
{
message = Manager.GetApplicationSettings().Configuration()
message = _serviceProvider.GetRequiredService<DefaultSettings>()
.QuickMessages
.First(_qm => _qm.Game == GameName)
.Messages[E.Data.Substring(1)];
@ -694,11 +729,11 @@ namespace IW4MAdmin
private async Task OnClientUpdate(EFClient origin)
{
var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(origin));
var client = Manager.GetActiveClients().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
if (client == null)
{
ServerLogger.LogWarning("{origin} expected to exist in client list for update, but they do not", origin.ToString());
ServerLogger.LogWarning("{Origin} expected to exist in client list for update, but they do not", origin.ToString());
return;
}
@ -712,7 +747,7 @@ namespace IW4MAdmin
{
try
{
await client.OnJoin(origin.IPAddress);
await client.OnJoin(origin.IPAddress, Manager.GetApplicationSettings().Configuration().EnableImplicitAccountLinking);
}
catch (Exception e)
@ -724,11 +759,11 @@ namespace IW4MAdmin
}
}
else if ((client.IPAddress != null && client.State == ClientState.Disconnecting) ||
else if (client.IPAddress != null && client.State == ClientState.Disconnecting ||
client.Level == Permission.Banned)
{
ServerLogger.LogWarning("{client} state is Unknown (probably kicked), but they are still connected. trying to kick again...", origin.ToString());
await client.CanConnect(client.IPAddress);
ServerLogger.LogWarning("{Client} state is Unknown (probably kicked), but they are still connected. trying to kick again...", origin.ToString());
await client.CanConnect(client.IPAddress, Manager.GetApplicationSettings().Configuration().EnableImplicitAccountLinking);
}
}
@ -739,11 +774,11 @@ namespace IW4MAdmin
/// array index 2 = updated clients
/// </summary>
/// <returns></returns>
async Task<IList<EFClient>[]> PollPlayersAsync()
async Task<List<EFClient>[]> PollPlayersAsync()
{
var currentClients = GetClientsAsList();
var statusResponse = (await this.GetStatusAsync());
var polledClients = statusResponse.Item1.AsEnumerable();
var polledClients = statusResponse.Clients.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
{
@ -753,10 +788,12 @@ namespace IW4MAdmin
var connectingClients = polledClients.Except(currentClients);
var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients);
UpdateMap(statusResponse.Item2);
UpdateGametype(statusResponse.Item3);
UpdateMap(statusResponse.Map);
UpdateGametype(statusResponse.GameType);
UpdateHostname(statusResponse.Hostname);
UpdateMaxPlayers(statusResponse.MaxClients);
return new List<EFClient>[]
return new []
{
connectingClients.ToList(),
disconnectingClients.ToList(),
@ -764,8 +801,10 @@ namespace IW4MAdmin
};
}
private async Task<long> GetIdForServer(Server server)
public override async Task<long> GetIdForServer(Server server = null)
{
server ??= this;
if ($"{server.IP}:{server.Port.ToString()}" == "66.150.121.184:28965")
{
return 886229536;
@ -803,6 +842,36 @@ namespace IW4MAdmin
}
}
private void UpdateHostname(string hostname)
{
if (string.IsNullOrEmpty(hostname) || Hostname == hostname)
{
return;
}
using(LogContext.PushProperty("Server", ToString()))
{
ServerLogger.LogDebug("Updating hostname to {HostName}", hostname);
}
Hostname = hostname;
}
private void UpdateMaxPlayers(int? maxPlayers)
{
if (maxPlayers == null || maxPlayers == MaxClients)
{
return;
}
using(LogContext.PushProperty("Server", ToString()))
{
ServerLogger.LogDebug("Updating max clients to {MaxPlayers}", maxPlayers);
}
MaxClients = maxPlayers.Value;
}
private async Task ShutdownInternal()
{
foreach (var client in GetClientsAsList())
@ -942,10 +1011,13 @@ namespace IW4MAdmin
LastMessage = DateTime.Now - start;
lastCount = DateTime.Now;
var appConfig = _serviceProvider.GetService<ApplicationConfiguration>();
// update the player history
if ((lastCount - playerCountStart).TotalMinutes >= PlayerHistory.UpdateInterval)
if (lastCount - playerCountStart >= appConfig.ServerDataCollectionInterval)
{
while (ClientHistory.Count > ((60 / PlayerHistory.UpdateInterval) * 12)) // 12 times a hour for 12 hours
var maxItems = Math.Ceiling(appConfig.MaxClientHistoryTime.TotalMinutes /
appConfig.ServerDataCollectionInterval.TotalMinutes);
while ( ClientHistory.Count > maxItems)
{
ClientHistory.Dequeue();
}
@ -1002,27 +1074,45 @@ namespace IW4MAdmin
public async Task Initialize()
{
try
{
ResolvedIpEndPoint =
new IPEndPoint(
(await Dns.GetHostAddressesAsync(IP)).First(address =>
address.AddressFamily == AddressFamily.InterNetwork), Port);
}
catch (Exception ex)
{
ServerLogger.LogWarning(ex, "Could not resolve hostname or IP for RCon connection {IP}:{Port}", IP, Port);
ResolvedIpEndPoint = new IPEndPoint(IPAddress.Parse(IP), Port);
}
RconParser = Manager.AdditionalRConParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.RConParserVersion);
.FirstOrDefault(parser =>
parser.Version == ServerConfig.RConParserVersion ||
parser.Name == ServerConfig.RConParserVersion);
EventParser = Manager.AdditionalEventParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion);
.FirstOrDefault(parser =>
parser.Version == ServerConfig.EventParserVersion ||
parser.Name == ServerConfig.RConParserVersion);
RconParser = RconParser ?? Manager.AdditionalRConParsers[0];
EventParser = EventParser ?? Manager.AdditionalEventParsers[0];
RconParser ??= Manager.AdditionalRConParsers[0];
EventParser ??= Manager.AdditionalEventParsers[0];
RemoteConnection = RConConnectionFactory.CreateConnection(ResolvedIpEndPoint, Password, RconParser.RConEngine);
RemoteConnection.SetConfiguration(RconParser);
var version = await this.GetMappedDvarValueOrDefaultAsync<string>("version");
Version = version.Value;
GameName = Utilities.GetGame(version?.Value ?? RconParser.Version);
GameName = Utilities.GetGame(version.Value ?? RconParser.Version);
if (GameName == Game.UKN)
{
GameName = RconParser.GameName;
}
if (version?.Value?.Length != 0)
if (version.Value?.Length != 0)
{
var matchedRconParser = Manager.AdditionalRConParsers.FirstOrDefault(_parser => _parser.Version == version.Value);
RconParser.Configuration = matchedRconParser != null ? matchedRconParser.Configuration : RconParser.Configuration;
@ -1043,8 +1133,9 @@ namespace IW4MAdmin
string mapname = (await this.GetMappedDvarValueOrDefaultAsync<string>("mapname", infoResponse: infoResponse)).Value;
int maxplayers = (await this.GetMappedDvarValueOrDefaultAsync<int>("sv_maxclients", infoResponse: infoResponse)).Value;
string gametype = (await this.GetMappedDvarValueOrDefaultAsync<string>("g_gametype", "gametype", infoResponse)).Value;
var basepath = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basepath"));
var basegame = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basegame"));
var basepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basepath");
var basegame = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basegame");
var homepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_homepath");
var game = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_game", infoResponse: infoResponse));
var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log");
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync");
@ -1080,8 +1171,14 @@ namespace IW4MAdmin
{
Manager.GetApplicationSettings().Configuration().ContactUri = Website;
}
var defaultConfig = _serviceProvider.GetRequiredService<DefaultSettings>();
var gameMaps = defaultConfig?.Maps?.FirstOrDefault(map => map.Game == GameName);
InitializeMaps();
if (gameMaps != null)
{
Maps.AddRange(gameMaps.Maps);
}
WorkingDirectory = basepath.Value;
this.Hostname = hostname;
@ -1139,15 +1236,16 @@ namespace IW4MAdmin
{
BaseGameDirectory = basegame.Value,
BasePathDirectory = basepath.Value,
HomePathDirectory = homepath.Value,
GameDirectory = EventParser.Configuration.GameDirectory ?? "",
ModDirectory = game.Value ?? "",
LogFile = logfile.Value,
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
IsOneLog = RconParser.IsOneLog
};
LogPath = GenerateLogPath(logInfo);
ServerLogger.LogInformation("Game log information {@logInfo}", logInfo);
if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null)
{
Console.WriteLine(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
@ -1172,7 +1270,7 @@ namespace IW4MAdmin
public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl)
{
var logUri = new Uri(logPath);
var logUri = new Uri(logPath, UriKind.Absolute);
if (string.IsNullOrEmpty(gameLogServerUrl))
{
@ -1188,21 +1286,31 @@ namespace IW4MAdmin
public static string GenerateLogPath(LogPathGeneratorInfo logInfo)
{
string logPath;
string workingDirectory = logInfo.BasePathDirectory;
var workingDirectory = logInfo.BasePathDirectory;
bool IsValidGamePath (string path)
{
var baseGameIsDirectory = !string.IsNullOrWhiteSpace(path) &&
path.IndexOfAny(Utilities.DirectorySeparatorChars) != -1;
bool baseGameIsDirectory = !string.IsNullOrWhiteSpace(logInfo.BaseGameDirectory) &&
logInfo.BaseGameDirectory.IndexOfAny(Utilities.DirectorySeparatorChars) != -1;
var baseGameIsRelative = path.FixDirectoryCharacters()
.Equals(logInfo.GameDirectory.FixDirectoryCharacters(), StringComparison.InvariantCultureIgnoreCase);
bool baseGameIsRelative = logInfo.BaseGameDirectory.FixDirectoryCharacters()
.Equals(logInfo.GameDirectory.FixDirectoryCharacters(), StringComparison.InvariantCultureIgnoreCase);
return baseGameIsDirectory && !baseGameIsRelative;
}
// we want to see if base game is provided and it 'looks' like a directory
if (baseGameIsDirectory && !baseGameIsRelative)
if (IsValidGamePath(logInfo.HomePathDirectory))
{
workingDirectory = logInfo.HomePathDirectory;
}
else if (IsValidGamePath(logInfo.BaseGameDirectory))
{
workingDirectory = logInfo.BaseGameDirectory;
}
if (string.IsNullOrWhiteSpace(logInfo.ModDirectory))
if (string.IsNullOrWhiteSpace(logInfo.ModDirectory) || logInfo.IsOneLog)
{
logPath = Path.Combine(workingDirectory, logInfo.GameDirectory, logInfo.LogFile);
}
@ -1224,12 +1332,9 @@ namespace IW4MAdmin
public override async Task Warn(string reason, EFClient targetClient, EFClient targetOrigin)
{
// ensure player gets warned if command not performed on them in game
targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var activeClient = Manager.FindActiveClient(targetClient);
var newPenalty = new EFPenalty()
var newPenalty = new EFPenalty
{
Type = EFPenalty.PenaltyType.Warning,
Expires = DateTime.UtcNow,
@ -1239,31 +1344,28 @@ namespace IW4MAdmin
Link = targetClient.AliasLink
};
ServerLogger.LogDebug("Creating warn penalty for {targetClient}", targetClient.ToString());
ServerLogger.LogDebug("Creating warn penalty for {TargetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame)
if (activeClient.IsIngame)
{
if (targetClient.Warnings >= 4)
if (activeClient.Warnings >= 4)
{
targetClient.Kick(loc["SERVER_WARNLIMT_REACHED"], Utilities.IW4MAdminClient(this));
activeClient.Kick(loc["SERVER_WARNLIMT_REACHED"], Utilities.IW4MAdminClient(this));
return;
}
// todo: move to translation sheet
string message = $"^1{loc["SERVER_WARNING"]} ^7[^3{targetClient.Warnings}^7]: ^3{targetClient.Name}^7, {reason}";
targetClient.CurrentServer.Broadcast(message);
var message = loc["COMMANDS_WARNING_FORMAT_V2"]
.FormatExt(activeClient.Warnings, activeClient.Name, reason);
activeClient.CurrentServer.Broadcast(message);
}
}
public override async Task Kick(string reason, EFClient targetClient, EFClient originClient, EFPenalty previousPenalty)
{
targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var activeClient = Manager.FindActiveClient(targetClient);
var newPenalty = new EFPenalty()
var newPenalty = new EFPenalty
{
Type = EFPenalty.PenaltyType.Kick,
Expires = DateTime.UtcNow,
@ -1273,69 +1375,64 @@ namespace IW4MAdmin
Link = targetClient.AliasLink
};
ServerLogger.LogDebug("Creating kick penalty for {targetClient}", targetClient.ToString());
ServerLogger.LogDebug("Creating kick penalty for {TargetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame)
if (activeClient.IsIngame)
{
var e = new GameEvent()
var gameEvent = new GameEvent
{
Type = GameEvent.EventType.PreDisconnect,
Origin = targetClient,
Origin = activeClient,
Owner = this
};
Manager.AddEvent(e);
Manager.AddEvent(gameEvent);
var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
targetClient.ClientNumber,
activeClient.TemporalClientNumber,
_messageFormatter.BuildFormattedMessage(RconParser.Configuration,
newPenalty,
previousPenalty));
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
}
public override async Task TempBan(string Reason, TimeSpan length, EFClient targetClient, EFClient originClient)
public override async Task TempBan(string reason, TimeSpan length, EFClient targetClient, EFClient originClient)
{
// ensure player gets kicked if command not performed on them in the same server
targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var activeClient = Manager.FindActiveClient(targetClient);
var newPenalty = new EFPenalty()
var newPenalty = new EFPenalty
{
Type = EFPenalty.PenaltyType.TempBan,
Expires = DateTime.UtcNow + length,
Offender = targetClient,
Offense = Reason,
Offense = reason,
Punisher = originClient,
Link = targetClient.AliasLink
};
ServerLogger.LogDebug("Creating tempban penalty for {targetClient}", targetClient.ToString());
ServerLogger.LogDebug("Creating tempban penalty for {TargetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame)
if (activeClient.IsIngame)
{
var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
targetClient.ClientNumber,
activeClient.TemporalClientNumber,
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
ServerLogger.LogDebug("Executing tempban kick command for {targetClient}", targetClient.ToString());
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
}
public override async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
{
// ensure player gets kicked if command not performed on them in the same server
targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var activeClient = Manager.FindActiveClient(targetClient);
EFPenalty newPenalty = new EFPenalty()
var newPenalty = new EFPenalty
{
Type = EFPenalty.PenaltyType.Ban,
Expires = null,
@ -1346,46 +1443,47 @@ namespace IW4MAdmin
IsEvadedOffense = isEvade
};
ServerLogger.LogDebug("Creating ban penalty for {targetClient}", targetClient.ToString());
targetClient.SetLevel(Permission.Banned, originClient);
ServerLogger.LogDebug("Creating ban penalty for {TargetClient}", targetClient.ToString());
activeClient.SetLevel(Permission.Banned, originClient);
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame)
if (activeClient.IsIngame)
{
ServerLogger.LogDebug("Attempting to kicking newly banned client {targetClient}", targetClient.ToString());
ServerLogger.LogDebug("Attempting to kicking newly banned client {ActiveClient}", activeClient.ToString());
var formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
targetClient.ClientNumber,
activeClient.TemporalClientNumber,
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString);
await activeClient.CurrentServer.ExecuteCommandAsync(formattedString);
}
}
override public async Task Unban(string reason, EFClient Target, EFClient Origin)
public override async Task Unban(string reason, EFClient targetClient, EFClient originClient)
{
var unbanPenalty = new EFPenalty()
var unbanPenalty = new EFPenalty
{
Type = EFPenalty.PenaltyType.Unban,
Expires = DateTime.Now,
Offender = Target,
Offender = targetClient,
Offense = reason,
Punisher = Origin,
Punisher = originClient,
When = DateTime.UtcNow,
Active = true,
Link = Target.AliasLink
Link = targetClient.AliasLink
};
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", Target.ToString());
Target.SetLevel(Permission.User, Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId);
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString());
targetClient.SetLevel(Permission.User, originClient);
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId);
await Manager.GetPenaltyService().Create(unbanPenalty);
}
override public void InitializeTokens()
public override void InitializeTokens()
{
Manager.GetMessageTokens().Add(new MessageToken("TOTALPLAYERS", (Server s) => Task.Run(async () => (await Manager.GetClientService().GetTotalClientsAsync()).ToString())));
Manager.GetMessageTokens().Add(new MessageToken("VERSION", (Server s) => Task.FromResult(Application.Program.Version.ToString())));
Manager.GetMessageTokens().Add(new MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.NextMapCommand.GetNextMap(s, _translationLookup)));
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(SharedLibraryCore.Commands.ListAdminsCommand.OnlineAdmins(s, _translationLookup))));
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(ListAdminsCommand.OnlineAdmins(s, _translationLookup))));
}
}
}

View File

@ -19,11 +19,13 @@ using SharedLibraryCore.Services;
using Stats.Dtos;
using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
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;
@ -32,16 +34,17 @@ using IW4MAdmin.Plugins.Stats.Client.Abstractions;
using IW4MAdmin.Plugins.Stats.Client;
using Stats.Client.Abstractions;
using Stats.Client;
using Stats.Config;
using Stats.Helpers;
namespace IW4MAdmin.Application
{
public class Program
{
public static BuildNumber Version { get; private set; } = BuildNumber.Parse(Utilities.GetVersionAsString());
public static ApplicationManager ServerManager;
private static Task ApplicationTask;
private static ServiceProvider serviceProvider;
public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString());
private static ApplicationManager _serverManager;
private static Task _applicationTask;
private static ServiceProvider _serviceProvider;
/// <summary>
/// entrypoint of the application
@ -54,7 +57,7 @@ namespace IW4MAdmin.Application
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray;
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
Console.CancelKeyPress += OnCancelKey;
Console.WriteLine("=====================================================");
Console.WriteLine(" IW4MAdmin");
@ -73,10 +76,10 @@ namespace IW4MAdmin.Application
/// <param name="e"></param>
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
{
ServerManager?.Stop();
if (ApplicationTask != null)
_serverManager?.Stop();
if (_applicationTask != null)
{
await ApplicationTask;
await _applicationTask;
}
}
@ -90,9 +93,8 @@ namespace IW4MAdmin.Application
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);
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version} {@Args}", Version, args);
try
{
// do any needed housekeeping file/folder migrations
@ -100,22 +102,29 @@ namespace IW4MAdmin.Application
ConfigurationMigration.CheckDirectories();
ConfigurationMigration.RemoveObsoletePlugins20210322();
logger.LogDebug("Configuring services...");
services = ConfigureServices(args);
serviceProvider = services.BuildServiceProvider();
var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>();
ServerManager = (ApplicationManager) serviceProvider.GetRequiredService<IManager>();
translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>();
var services = await ConfigureServices(args);
_serviceProvider = services.BuildServiceProvider();
var versionChecker = _serviceProvider.GetRequiredService<IMasterCommunication>();
_serverManager = (ApplicationManager) _serviceProvider.GetRequiredService<IManager>();
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
await versionChecker.CheckVersion();
await ServerManager.Init();
_applicationTask = RunApplicationTasksAsync(logger, services);
var tasks = new[]
{
versionChecker.CheckVersion(),
_serverManager.Init(),
_applicationTask
};
await Task.WhenAll(tasks);
}
catch (Exception e)
{
string failMessage = translationLookup == null
var failMessage = translationLookup == null
? "Failed to initialize IW4MAdmin"
: translationLookup["MANAGER_INIT_FAIL"];
string exitMessage = translationLookup == null
var exitMessage = translationLookup == null
? "Press enter to exit..."
: translationLookup["MANAGER_EXIT"];
@ -135,7 +144,7 @@ namespace IW4MAdmin.Application
.FormatExt(configException.ConfigurationFileName));
}
foreach (string error in configException.Errors)
foreach (var error in configException.Errors)
{
Console.WriteLine(error);
}
@ -151,27 +160,12 @@ namespace IW4MAdmin.Application
return;
}
try
{
ApplicationTask = RunApplicationTasksAsync(logger, services);
await ApplicationTask;
}
catch (Exception e)
{
logger.LogCritical(e, "Failed to launch IW4MAdmin");
string failMessage = translationLookup == null
? "Failed to launch IW4MAdmin"
: translationLookup["MANAGER_INIT_FAIL"];
Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}");
}
if (ServerManager.IsRestartRequested)
if (_serverManager.IsRestartRequested)
{
goto restart;
}
serviceProvider.Dispose();
await _serviceProvider.DisposeAsync();
}
/// <summary>
@ -180,21 +174,26 @@ namespace IW4MAdmin.Application
/// <returns></returns>
private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
{
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront
? WebfrontCore.Program.Init(ServerManager, serviceProvider, services, ServerManager.CancellationToken)
var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront
? WebfrontCore.Program.Init(_serverManager, _serviceProvider, services, _serverManager.CancellationToken)
: Task.CompletedTask;
var collectionService = _serviceProvider.GetRequiredService<IServerDataCollector>();
// we want to run this one on a manual thread instead of letting the thread pool handle it,
// because we can't exit early from waiting on console input, and it prevents us from restarting
var inputThread = new Thread(async () => await ReadConsoleInput(logger));
async void ReadInput() => await ReadConsoleInput(logger);
var inputThread = new Thread(ReadInput);
inputThread.Start();
var tasks = new[]
{
ServerManager.Start(),
webfrontTask,
serviceProvider.GetRequiredService<IMasterCommunication>()
.RunUploadStatus(ServerManager.CancellationToken)
_serverManager.Start(),
_serviceProvider.GetRequiredService<IMasterCommunication>()
.RunUploadStatus(_serverManager.CancellationToken),
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
};
logger.LogDebug("Starting webfront and input tasks");
@ -203,8 +202,7 @@ namespace IW4MAdmin.Application
logger.LogInformation("Shutdown completed successfully");
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
}
/// <summary>
/// reads input from the console and executes entered commands on the default server
/// </summary>
@ -217,32 +215,41 @@ namespace IW4MAdmin.Application
return;
}
string lastCommand;
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
EFClient origin = null;
try
{
while (!ServerManager.CancellationToken.IsCancellationRequested)
while (!_serverManager.CancellationToken.IsCancellationRequested)
{
lastCommand = await Console.In.ReadLineAsync();
if (lastCommand?.Length > 0)
if (!_serverManager.IsInitialized)
{
if (lastCommand?.Length > 0)
{
GameEvent E = new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = lastCommand,
Origin = Origin,
Owner = ServerManager.Servers[0]
};
ServerManager.AddEvent(E);
await E.WaitAsync(Utilities.DefaultCommandTimeout, ServerManager.CancellationToken);
Console.Write('>');
}
await Task.Delay(1000);
continue;
}
var lastCommand = await Console.In.ReadLineAsync();
if (lastCommand == null)
{
continue;
}
if (!lastCommand.Any())
{
continue;
}
var gameEvent = new GameEvent
{
Type = GameEvent.EventType.Command,
Data = lastCommand,
Origin = origin ??= Utilities.IW4MAdminClient(_serverManager.Servers.FirstOrDefault()),
Owner = _serverManager.Servers[0]
};
_serverManager.AddEvent(gameEvent);
await gameEvent.WaitAsync(Utilities.DefaultCommandTimeout, _serverManager.CancellationToken);
Console.Write('>');
}
}
catch (OperationCanceledException)
@ -270,9 +277,10 @@ namespace IW4MAdmin.Application
// register the native commands
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
.Where(_command => _command.BaseType == typeof(Command)))
.Concat(typeof(Program).Assembly.GetTypes().Where(type => type.Namespace == "IW4MAdmin.Application.Commands"))
.Where(command => command.BaseType == typeof(Command)))
{
defaultLogger.LogDebug("Registered native command type {name}", commandType.Name);
defaultLogger.LogDebug("Registered native command type {Name}", commandType.Name);
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
@ -280,40 +288,40 @@ namespace IW4MAdmin.Application
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in plugins)
{
defaultLogger.LogDebug("Registered plugin type {name}", pluginType.FullName);
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);
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);
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 handlerInstance = Activator.CreateInstance(handlerType, configInstance.Name());
var genericInterfaceType = typeof(IConfigurationHandler<>).MakeGenericType(configurationType);
serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
}
// register any script plugins
foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins())
foreach (var func in pluginImporter.DiscoverScriptPlugins())
{
serviceCollection.AddSingleton(scriptPlugin);
serviceCollection.AddSingleton(sp => func(sp));
}
// 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())
.Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType))
.Union(plugins.SelectMany(asm => asm.Assembly.GetTypes())
.Distinct()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
.Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType))))
{
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
serviceCollection.AddSingleton(instance);
@ -326,23 +334,39 @@ namespace IW4MAdmin.Application
/// <summary>
/// Configures the dependency injection services
/// </summary>
private static IServiceCollection ConfigureServices(string[] args)
private static async Task<IServiceCollection> ConfigureServices(string[] args)
{
// todo: this is a quick fix
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
// setup the static resources (config/master api/translations)
var serviceCollection = new ServiceCollection();
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
await appConfigHandler.BuildAsync();
var defaultConfigHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
await defaultConfigHandler.BuildAsync();
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration");
await commandConfigHandler.BuildAsync();
var statsCommandHandler = new BaseConfigurationHandler<StatsConfiguration>();
await statsCommandHandler.BuildAsync();
var defaultConfig = defaultConfigHandler.Configuration();
var appConfig = appConfigHandler.Configuration();
var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080")
: appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
var masterRestClient = RestClient.For<IMasterApi>(masterUri);
var httpClient = new HttpClient
{
BaseAddress = masterUri,
Timeout = TimeSpan.FromSeconds(15)
};
var masterRestClient = RestClient.For<IMasterApi>(httpClient);
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
if (appConfig == null)
{
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
appConfigHandler.Set(appConfig);
appConfigHandler.Save();
await appConfigHandler.Save();
}
// register override level names
@ -359,15 +383,15 @@ namespace IW4MAdmin.Application
serviceCollection
.AddBaseLogger(appConfig)
.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection)
.AddSingleton(defaultConfig)
.AddSingleton<IServiceCollection>(serviceCollection)
.AddSingleton<IConfigurationHandler<DefaultSettings>, BaseConfigurationHandler<DefaultSettings>>()
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
.AddSingleton(
new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as
IConfigurationHandler<CommandConfiguration>)
.AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler)
.AddSingleton(appConfig)
.AddSingleton(_serviceProvider =>
_serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
.AddSingleton(statsCommandHandler.Configuration() ?? new StatsConfiguration())
.AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
.Configuration() ?? new CommandConfiguration())
.AddSingleton<IPluginImporter, PluginImporter>()
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
@ -393,11 +417,14 @@ namespace IW4MAdmin.Application
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>,
UpdatedAliasResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>, ConnectionsResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
.AddSingleton<IMasterCommunication, MasterCommunication>()
.AddSingleton<IManager, ApplicationManager>()
#pragma warning disable CS0612
.AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>()
#pragma warning restore CS0612
.AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>()
.AddSingleton<IClientStatisticCalculator, HitCalculator>()
.AddSingleton<IServerDistributionCalculator, ServerDistributionCalculator>()
@ -405,6 +432,10 @@ namespace IW4MAdmin.Application
.AddSingleton<IHitInfoBuilder, HitInfoBuilder>()
.AddSingleton(typeof(ILookupCache<>), typeof(LookupCache<>))
.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>))
.AddSingleton<IServerDataViewer, ServerDataViewer>()
.AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IEventPublisher, EventPublisher>()
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);
@ -417,6 +448,8 @@ namespace IW4MAdmin.Application
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
}
serviceCollection.AddSource();
return serviceCollection;
}
@ -429,4 +462,4 @@ namespace IW4MAdmin.Application
return collection.GetRequiredService<ILogger<T>>();
}
}
}
}

View File

@ -0,0 +1,60 @@
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Meta
{
public class
ConnectionsResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
public ConnectionsResourceQueryHelper(ILogger<ConnectionsResourceQueryHelper> logger,
IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
_logger = logger;
}
public async Task<ResourceQueryHelperResult<ConnectionHistoryResponse>> QueryResource(
ClientPaginationRequest query)
{
_logger.LogDebug("{Class} {@Request}", nameof(ConnectionsResourceQueryHelper), query);
await using var context = _contextFactory.CreateContext(enableTracking: false);
var iqConnections = context.ConnectionHistory.AsNoTracking()
.Where(history => query.ClientId == history.ClientId)
.Where(history => history.CreatedDateTime < query.Before)
.OrderByDescending(history => history.CreatedDateTime);
var connections = await iqConnections.Select(history => new ConnectionHistoryResponse
{
MetaId = history.ClientConnectionId,
ClientId = history.ClientId,
Type = MetaType.ConnectionHistory,
ShouldDisplay = true,
When = history.CreatedDateTime,
ServerName = history.Server.HostName,
ConnectionType = history.ConnectionType
})
.ToListAsync();
_logger.LogDebug("{Class} retrieved {Number} items", nameof(ConnectionsResourceQueryHelper),
connections.Count);
return new ResourceQueryHelperResult<ConnectionHistoryResponse>
{
Results = connections
};
}
}
}

View File

@ -20,11 +20,14 @@ namespace IW4MAdmin.Application.Meta
private readonly IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> _receivedPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> _administeredPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> _updatedAliasHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>
_connectionHistoryHelper;
public MetaRegistration(ILogger<MetaRegistration> logger, IMetaService metaService, ITranslationLookup transLookup, IEntityService<EFClient> clientEntityService,
IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> receivedPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> administeredPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper)
IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper,
IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse> connectionHistoryHelper)
{
_logger = logger;
_transLookup = transLookup;
@ -33,6 +36,7 @@ namespace IW4MAdmin.Application.Meta
_receivedPenaltyHelper = receivedPenaltyHelper;
_administeredPenaltyHelper = administeredPenaltyHelper;
_updatedAliasHelper = updatedAliasHelper;
_connectionHistoryHelper = connectionHistoryHelper;
}
public void Register()
@ -41,6 +45,7 @@ namespace IW4MAdmin.Application.Meta
_metaService.AddRuntimeMeta<ClientPaginationRequest, ReceivedPenaltyResponse>(MetaType.ReceivedPenalty, GetReceivedPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, AdministeredPenaltyResponse>(MetaType.Penalized, GetAdministeredPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, UpdatedAliasResponse>(MetaType.AliasUpdate, GetUpdatedAliasMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, ConnectionHistoryResponse>(MetaType.ConnectionHistory, GetConnectionHistoryMeta);
}
private async Task<IEnumerable<InformationResponse>> GetProfileMeta(ClientPaginationRequest request)
@ -163,5 +168,11 @@ namespace IW4MAdmin.Application.Meta
var aliases = await _updatedAliasHelper.QueryResource(request);
return aliases.Results;
}
private async Task<IEnumerable<ConnectionHistoryResponse>> GetConnectionHistoryMeta(ClientPaginationRequest request)
{
var connections = await _connectionHistoryHelper.QueryResource(request);
return connections.Results;
}
}
}

View File

@ -1,10 +1,12 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
@ -21,11 +23,14 @@ namespace IW4MAdmin.Application.Meta
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ApplicationConfiguration _appConfig;
public ReceivedPenaltyResourceQueryHelper(ILogger<ReceivedPenaltyResourceQueryHelper> logger, IDatabaseContextFactory contextFactory)
public ReceivedPenaltyResourceQueryHelper(ILogger<ReceivedPenaltyResourceQueryHelper> logger,
IDatabaseContextFactory contextFactory, ApplicationConfiguration appConfig)
{
_contextFactory = contextFactory;
_logger = logger;
_appConfig = appConfig;
}
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query)
@ -35,15 +40,40 @@ namespace IW4MAdmin.Application.Meta
var linkId = await ctx.Clients.AsNoTracking()
.Where(_client => _client.ClientId == query.ClientId)
.Select(_client => _client.AliasLinkId)
.Select(_client => new {_client.AliasLinkId, _client.CurrentAliasId })
.FirstOrDefaultAsync();
var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => _penalty.OffenderId == query.ClientId || (linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId))
.Where(_penalty => _penalty.When < query.Before)
.OrderByDescending(_penalty => _penalty.When);
.Where(_penalty => _penalty.OffenderId == query.ClientId ||
linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId.AliasLinkId);
var penalties = await iqPenalties
IQueryable<EFPenalty> iqIpLinkedPenalties = null;
if (!_appConfig.EnableImplicitAccountLinking)
{
var usedIps = await ctx.Aliases.AsNoTracking()
.Where(alias => (alias.LinkId == linkId.AliasLinkId || alias.AliasId == linkId.CurrentAliasId) && alias.IPAddress != null)
.Select(alias => alias.IPAddress).ToListAsync();
var aliasedIds = await ctx.Aliases.AsNoTracking().Where(alias => usedIps.Contains(alias.IPAddress))
.Select(alias => alias.LinkId)
.ToListAsync();
iqIpLinkedPenalties = ctx.Penalties.AsNoTracking()
.Where(penalty =>
linkedPenaltyType.Contains(penalty.Type) && aliasedIds.Contains(penalty.LinkId));
}
var iqAllPenalties = iqPenalties;
if (iqIpLinkedPenalties != null)
{
iqAllPenalties = iqPenalties.Union(iqIpLinkedPenalties);
}
var penalties = await iqAllPenalties
.Where(_penalty => _penalty.When < query.Before)
.OrderByDescending(_penalty => _penalty.When)
.Take(query.Count)
.Select(_penalty => new ReceivedPenaltyResponse()
{

View File

@ -4,8 +4,6 @@ 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
{

View File

@ -1,10 +1,13 @@
using Newtonsoft.Json;
using SharedLibraryCore;
using SharedLibraryCore;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace IW4MAdmin.Application.Misc
{
@ -14,27 +17,39 @@ namespace IW4MAdmin.Application.Misc
/// <typeparam name="T">base configuration type</typeparam>
public class BaseConfigurationHandler<T> : IConfigurationHandler<T> where T : IBaseConfiguration
{
T _configuration;
private T _configuration;
private readonly SemaphoreSlim _onSaving;
private readonly JsonSerializerOptions _serializerOptions;
public BaseConfigurationHandler(string fn)
public BaseConfigurationHandler(string fileName)
{
FileName = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{fn}.json");
Build();
_serializerOptions = new JsonSerializerOptions
{
WriteIndented = true,
};
_serializerOptions.Converters.Add(new JsonStringEnumConverter());
_onSaving = new SemaphoreSlim(1, 1);
FileName = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{fileName}.json");
}
public BaseConfigurationHandler() : this(typeof(T).Name)
{
}
~BaseConfigurationHandler()
{
_onSaving.Dispose();
}
public string FileName { get; }
public void Build()
public async Task BuildAsync()
{
try
{
var configContent = File.ReadAllText(FileName);
_configuration = JsonConvert.DeserializeObject<T>(configContent);
await using var fileStream = File.OpenRead(FileName);
_configuration = await JsonSerializer.DeserializeAsync<T>(fileStream, _serializerOptions);
}
catch (FileNotFoundException)
@ -52,16 +67,23 @@ namespace IW4MAdmin.Application.Misc
}
}
public Task Save()
public async Task Save()
{
var settings = new JsonSerializerSettings()
try
{
Formatting = Formatting.Indented
};
settings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
await _onSaving.WaitAsync();
var appConfigJSON = JsonConvert.SerializeObject(_configuration, settings);
return File.WriteAllTextAsync(FileName, appConfigJSON);
await using var fileStream = File.Create(FileName);
await JsonSerializer.SerializeAsync(fileStream, _configuration, _serializerOptions);
}
finally
{
if (_onSaving.CurrentCount == 0)
{
_onSaving.Release(1);
}
}
}
public T Configuration()

View File

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

View File

@ -19,6 +19,12 @@ namespace IW4MAdmin.Application.Misc
/// </summary>
public string BasePathDirectory { get; set; } = "";
/// <summary>
/// directory for local storage
/// <remarks>fs_homepath</remarks>
/// </summary>
public string HomePathDirectory { get; set; } = "";
/// <summary>
/// overide game directory
/// <remarks>plugin driven</remarks>
@ -41,5 +47,11 @@ namespace IW4MAdmin.Application.Misc
/// indicates if running on windows
/// </summary>
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

@ -26,7 +26,8 @@ namespace IW4MAdmin.Application.Misc
private readonly ApplicationConfiguration _appConfig;
private readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99");
private readonly int _apiVersion = 1;
private bool firstHeartBeat = true;
private bool _firstHeartBeat = true;
private static readonly TimeSpan Interval = TimeSpan.FromSeconds(30);
public MasterCommunication(ILogger<MasterCommunication> logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager)
{
@ -93,53 +94,24 @@ namespace IW4MAdmin.Application.Misc
public async Task RunUploadStatus(CancellationToken token)
{
// todo: clean up this logic
bool connected;
while (!token.IsCancellationRequested)
{
try
{
await UploadStatus();
}
catch (System.Net.Http.HttpRequestException e)
{
_logger.LogWarning(e, "Could not send heartbeat");
}
catch (AggregateException e)
{
_logger.LogWarning(e, "Could not send heartbeat");
var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(ApiException));
foreach (var ex in exceptions)
if (_manager.IsRunning)
{
if (((ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
await UploadStatus();
}
}
catch (ApiException e)
catch (Exception ex)
{
_logger.LogWarning(e, "Could not send heartbeat");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
_logger.LogWarning(ex, "Could not send heartbeat");
}
catch (Exception e)
{
_logger.LogWarning(e, "Could not send heartbeat");
}
try
{
await Task.Delay(30000, token);
await Task.Delay(Interval, token);
}
catch
@ -151,7 +123,7 @@ namespace IW4MAdmin.Application.Misc
private async Task UploadStatus()
{
if (firstHeartBeat)
if (_firstHeartBeat)
{
var token = await _apiInstance.Authenticate(new AuthenticationId
{
@ -179,12 +151,13 @@ namespace IW4MAdmin.Application.Misc
Id = s.EndPoint,
Port = (short)s.Port,
IPAddress = s.IP
}).ToList()
}).ToList(),
WebfrontUrl = _appConfig.WebfrontUrl
};
Response<ResultMessage> response = null;
if (firstHeartBeat)
if (_firstHeartBeat)
{
response = await _apiInstance.AddInstance(instance);
}
@ -192,7 +165,7 @@ namespace IW4MAdmin.Application.Misc
else
{
response = await _apiInstance.UpdateInstance(instance.Id, instance);
firstHeartBeat = false;
_firstHeartBeat = false;
}
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)

View File

@ -207,42 +207,30 @@ namespace IW4MAdmin.Application.Misc
public async Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request)
{
var meta = new List<IClientMeta>();
var metas = await Task.WhenAll(_metaActions.Where(kvp => kvp.Key != MetaType.Information)
.Select(async kvp => await kvp.Value[0](request)));
foreach (var (type, actions) in _metaActions)
{
// information is not listed chronologically
if (type != MetaType.Information)
{
var metaItems = await actions[0](request);
meta.AddRange(metaItems);
}
}
return meta.OrderByDescending(_meta => _meta.When)
return metas.SelectMany(m => (IEnumerable<IClientMeta>)m)
.OrderByDescending(m => m.When)
.Take(request.Count)
.ToList();
}
public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType) where T : IClientMeta
{
IEnumerable<T> meta;
if (metaType == MetaType.Information)
{
var allMeta = new List<T>();
foreach (var individualMetaRegistration in _metaActions[metaType])
{
allMeta.AddRange(await individualMetaRegistration(request));
}
var completedMeta = await Task.WhenAll(_metaActions[metaType].Select(async individualMetaRegistration =>
(IEnumerable<T>)await individualMetaRegistration(request)));
allMeta.AddRange(completedMeta.SelectMany(meta => meta));
return ProcessInformationMeta(allMeta);
}
else
{
meta = await _metaActions[metaType][0](request) as IEnumerable<T>;
}
var meta = await _metaActions[metaType][0](request) as IEnumerable<T>;
return meta;
}

View File

@ -6,6 +6,7 @@ using SharedLibraryCore.Interfaces;
using System.Linq;
using SharedLibraryCore;
using IW4MAdmin.Application.API.Master;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -37,26 +38,26 @@ namespace IW4MAdmin.Application.Misc
/// discovers all the script plugins in the plugins dir
/// </summary>
/// <returns></returns>
public IEnumerable<IPlugin> DiscoverScriptPlugins()
public IEnumerable<Func<IServiceProvider, IPlugin>> DiscoverScriptPlugins()
{
string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
var pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
if (Directory.Exists(pluginDir))
if (!Directory.Exists(pluginDir))
{
var scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts());
_logger.LogDebug("Discovered {count} potential script plugins", scriptPluginFiles.Count());
if (scriptPluginFiles.Count() > 0)
{
foreach (string fileName in scriptPluginFiles)
{
_logger.LogDebug("Discovered script plugin {fileName}", fileName);
var plugin = new ScriptPlugin(_logger, fileName);
yield return plugin;
}
}
return Enumerable.Empty<Func<IServiceProvider, IPlugin>>();
}
var scriptPluginFiles =
Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts()).ToList();
_logger.LogDebug("Discovered {count} potential script plugins", scriptPluginFiles.Count);
return scriptPluginFiles.Select<string, Func<IServiceProvider, IPlugin>>(fileName => serviceProvider =>
{
_logger.LogDebug("Discovered script plugin {fileName}", fileName);
return new ScriptPlugin(_logger,
serviceProvider.GetRequiredService<IScriptPluginTimerHelper>(), fileName);
}).ToList();
}
/// <summary>
@ -83,19 +84,47 @@ namespace IW4MAdmin.Application.Misc
.GroupBy(_assembly => _assembly.FullName).Select(_assembly => _assembly.OrderByDescending(_assembly => _assembly.GetName().Version).First());
pluginTypes = assemblies
.SelectMany(_asm => _asm.GetTypes())
.SelectMany(_asm =>
{
try
{
return _asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null);
_logger.LogDebug("Discovered {count} plugin implementations", pluginTypes.Count());
commandTypes = assemblies
.SelectMany(_asm => _asm.GetTypes())
.SelectMany(_asm =>{
try
{
return _asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command));
_logger.LogDebug("Discovered {count} plugin commands", commandTypes.Count());
configurationTypes = assemblies
.SelectMany(asm => asm.GetTypes())
.SelectMany(asm => {
try
{
return asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(asmType =>
asmType.IsClass && asmType.GetInterface(nameof(IBaseConfiguration), false) != null);

View File

@ -6,7 +6,6 @@ using System;
using System.Threading.Tasks;
using Data.Models.Client;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Database.Models.EFClient;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
@ -16,14 +15,15 @@ namespace IW4MAdmin.Application.Misc
/// </summary>
public class ScriptCommand : Command
{
private readonly Action<GameEvent> _executeAction;
private readonly Func<GameEvent, Task> _executeAction;
private readonly ILogger _logger;
public ScriptCommand(string name, string alias, string description, bool isTargetRequired, EFClient.Permission permission,
CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout, ILogger<ScriptCommand> logger)
public ScriptCommand(string name, string alias, string description, bool isTargetRequired,
EFClient.Permission permission,
CommandArgument[] args, Func<GameEvent, Task> executeAction, CommandConfiguration config,
ITranslationLookup layout, ILogger<ScriptCommand> logger, Server.Game[] supportedGames)
: base(config, layout)
{
_executeAction = executeAction;
_logger = logger;
Name = name;
@ -32,6 +32,7 @@ namespace IW4MAdmin.Application.Misc
RequiresTarget = isTargetRequired;
Permission = permission;
Arguments = args;
SupportedGames = supportedGames;
}
public override async Task ExecuteAsync(GameEvent e)
@ -43,7 +44,7 @@ namespace IW4MAdmin.Application.Misc
try
{
await Task.Run(() => _executeAction(e));
await _executeAction(e);
}
catch (Exception ex)
{

View File

@ -13,6 +13,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Logging;
using Serilog.Context;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -36,29 +37,31 @@ namespace IW4MAdmin.Application.Misc
/// </summary>
public bool IsParser { get; private set; }
public FileSystemWatcher Watcher { get; private set; }
public FileSystemWatcher Watcher { get; }
private Engine _scriptEngine;
private readonly string _fileName;
private readonly SemaphoreSlim _onProcessing;
private bool successfullyLoaded;
private readonly SemaphoreSlim _onProcessing = new(1, 1);
private bool _successfullyLoaded;
private readonly List<string> _registeredCommandNames;
private readonly ILogger _logger;
private readonly IScriptPluginTimerHelper _timerHelper;
public ScriptPlugin(ILogger logger, string filename, string workingDirectory = null)
public ScriptPlugin(ILogger logger, IScriptPluginTimerHelper timerHelper, string filename, string workingDirectory = null)
{
_logger = logger;
_fileName = filename;
Watcher = new FileSystemWatcher()
Watcher = new FileSystemWatcher
{
Path = workingDirectory == null ? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}" : workingDirectory,
Path = workingDirectory ?? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}",
NotifyFilter = NotifyFilters.Size,
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
};
Watcher.EnableRaisingEvents = true;
_onProcessing = new SemaphoreSlim(1, 1);
_registeredCommandNames = new List<string>();
_timerHelper = timerHelper;
_timerHelper.SetDependency(_onProcessing);
}
~ScriptPlugin()
@ -67,12 +70,13 @@ namespace IW4MAdmin.Application.Misc
_onProcessing.Dispose();
}
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory, IScriptPluginServiceResolver serviceResolver)
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory,
IScriptPluginServiceResolver serviceResolver)
{
await _onProcessing.WaitAsync();
try
{
await _onProcessing.WaitAsync();
// for some reason we get an event trigger when the file is not finished being modified.
// this must have been a change in .NET CORE 3.x
// so if the new file is empty we can't process it yet
@ -81,26 +85,27 @@ namespace IW4MAdmin.Application.Misc
return;
}
bool firstRun = _scriptEngine == null;
var firstRun = _scriptEngine == null;
// it's been loaded before so we need to call the unload event
if (!firstRun)
{
await OnUnloadAsync();
foreach (string commandName in _registeredCommandNames)
foreach (var commandName in _registeredCommandNames)
{
_logger.LogDebug("Removing plugin registered command {command}", commandName);
_logger.LogDebug("Removing plugin registered command {Command}", commandName);
manager.RemoveCommandByName(commandName);
}
_registeredCommandNames.Clear();
}
successfullyLoaded = false;
_successfullyLoaded = false;
string script;
using (var stream = new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
await using (var stream =
new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (var reader = new StreamReader(stream, Encoding.Default))
{
@ -110,45 +115,33 @@ namespace IW4MAdmin.Application.Misc
_scriptEngine = new Engine(cfg =>
cfg.AllowClr(new[]
{
typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly,
typeof(Utilities).Assembly,
typeof(Encoding).Assembly
})
.CatchClrExceptions());
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");
}
{
typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly,
typeof(Utilities).Assembly,
typeof(Encoding).Assembly
})
.CatchClrExceptions()
.AddObjectConverter(new PermissionLevelToStringConverter()));
_scriptEngine.Execute(script);
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
dynamic pluginObject = _scriptEngine.Evaluate("plugin").ToObject();
Author = pluginObject.author;
Name = pluginObject.name;
Version = (float)pluginObject.version;
var commands = _scriptEngine.GetValue("commands");
var commands = JsValue.Undefined;
try
{
commands = _scriptEngine.Evaluate("commands");
}
catch (JavaScriptException)
{
// ignore because commands aren't defined;
}
if (commands != JsValue.Undefined)
{
@ -156,7 +149,7 @@ namespace IW4MAdmin.Application.Misc
{
foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory))
{
_logger.LogDebug("Adding plugin registered command {commandName}", command.Name);
_logger.LogDebug("Adding plugin registered command {CommandName}", command.Name);
manager.AddAdditionalCommand(command);
_registeredCommandNames.Add(command.Name);
}
@ -164,52 +157,63 @@ namespace IW4MAdmin.Application.Misc
catch (RuntimeBinderException e)
{
throw new PluginException($"Not all required fields were found: {e.Message}") { PluginFile = _fileName };
throw new PluginException($"Not all required fields were found: {e.Message}")
{ PluginFile = _fileName };
}
}
await OnLoadAsync(manager);
try
{
if (pluginObject.isParser)
{
await OnLoadAsync(manager);
IsParser = true;
IEventParser eventParser = (IEventParser)_scriptEngine.GetValue("eventParser").ToObject();
IRConParser rconParser = (IRConParser)_scriptEngine.GetValue("rconParser").ToObject();
var eventParser = (IEventParser)_scriptEngine.Evaluate("eventParser").ToObject();
var rconParser = (IRConParser)_scriptEngine.Evaluate("rconParser").ToObject();
manager.AdditionalEventParsers.Add(eventParser);
manager.AdditionalRConParsers.Add(rconParser);
}
}
catch (RuntimeBinderException) { }
catch (RuntimeBinderException)
{
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine);
await configWrapper.InitializeAsync();
_scriptEngine.SetValue("_configHandler", configWrapper);
await OnLoadAsync(manager);
}
if (!firstRun)
{
await OnLoadAsync(manager);
}
successfullyLoaded = true;
_successfullyLoaded = true;
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} initialization {@locationInfo}",
nameof(OnLoadAsync), _fileName, ex.Location);
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
nameof(Initialize), Path.GetFileName(_fileName), ex.Location);
throw new PluginException("An error occured while initializing script plugin");
}
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo}",
nameof(Initialize), _fileName, jsEx.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");
}
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
nameof(OnLoadAsync), Path.GetFileName(_fileName));
throw new PluginException("An error occured while executing action for script plugin");
}
finally
{
if (_onProcessing.CurrentCount == 0)
@ -219,42 +223,41 @@ namespace IW4MAdmin.Application.Misc
}
}
public async Task OnEventAsync(GameEvent E, Server S)
public async Task OnEventAsync(GameEvent gameEvent, Server server)
{
if (successfullyLoaded)
if (_successfullyLoaded)
{
await _onProcessing.WaitAsync();
try
{
_scriptEngine.SetValue("_gameEvent", E);
_scriptEngine.SetValue("_server", S);
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(S));
_scriptEngine.Execute("plugin.onEventAsync(_gameEvent, _server)").GetCompletionValue();
await _onProcessing.WaitAsync();
_scriptEngine.SetValue("_gameEvent", gameEvent);
_scriptEngine.SetValue("_server", server);
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(server));
_scriptEngine.Evaluate("plugin.onEventAsync(_gameEvent, _server)");
}
catch (JavaScriptException ex)
{
using (LogContext.PushProperty("Server", S.ToString()))
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} with event type {eventType} {@locationInfo}",
nameof(OnEventAsync), _fileName, E.Type, ex.Location);
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} with event type {EventType} {@LocationInfo}",
nameof(OnEventAsync), Path.GetFileName(_fileName), gameEvent.Type, ex.Location);
}
throw new PluginException($"An error occured while executing action for script plugin");
throw new PluginException("An error occured while executing action for script plugin");
}
catch (Exception e)
catch (Exception ex)
{
using (LogContext.PushProperty("Server", S.ToString()))
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(e,
"Encountered unexpected error while running {methodName} for script plugin {plugin} with event type {eventType}",
nameof(OnEventAsync), _fileName, E.Type);
_logger.LogError(ex,
"Encountered error while running {MethodName} for script plugin {Plugin} with event type {EventType}",
nameof(OnEventAsync), _fileName, gameEvent.Type);
}
throw new PluginException($"An error occured while executing action for script plugin");
throw new PluginException("An error occured while executing action for script plugin");
}
finally
@ -269,23 +272,68 @@ namespace IW4MAdmin.Application.Misc
public Task OnLoadAsync(IManager manager)
{
_logger.LogDebug("OnLoad executing for {name}", Name);
_scriptEngine.SetValue("_manager", manager);
return Task.FromResult(_scriptEngine.Execute("plugin.onLoadAsync(_manager)").GetCompletionValue());
}
public Task OnTickAsync(Server S)
{
_scriptEngine.SetValue("_server", S);
return Task.FromResult(_scriptEngine.Execute("plugin.onTickAsync(_server)").GetCompletionValue());
}
public async Task OnUnloadAsync()
{
if (successfullyLoaded)
try
{
await Task.FromResult(_scriptEngine.Execute("plugin.onUnloadAsync()").GetCompletionValue());
_logger.LogDebug("OnLoad executing for {Name}", Name);
_scriptEngine.SetValue("_manager", manager);
_scriptEngine.SetValue("_timerHelper", _timerHelper);
_scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
return Task.CompletedTask;
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
nameof(OnLoadAsync), Path.GetFileName(_fileName), ex.Location);
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
nameof(OnLoadAsync), Path.GetFileName(_fileName));
throw new PluginException("An error occured while executing action for script plugin");
}
}
public async Task OnTickAsync(Server server)
{
_scriptEngine.SetValue("_server", server);
await Task.FromResult(_scriptEngine.Evaluate("plugin.onTickAsync(_server)"));
}
public Task OnUnloadAsync()
{
if (!_successfullyLoaded)
{
return Task.CompletedTask;
}
try
{
_scriptEngine.Evaluate("plugin.onUnloadAsync()");
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo}",
nameof(OnUnloadAsync), Path.GetFileName(_fileName), ex.Location);
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
nameof(OnUnloadAsync), Path.GetFileName(_fileName));
throw new PluginException("An error occured while executing action for script plugin");
}
return Task.CompletedTask;
}
/// <summary>
@ -294,9 +342,9 @@ namespace IW4MAdmin.Application.Misc
/// <param name="commands">commands value from jint parser</param>
/// <param name="scriptCommandFactory">factory to create the command from</param>
/// <returns></returns>
public IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory)
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory)
{
List<IManagerCommand> commandList = new List<IManagerCommand>();
var commandList = new List<IManagerCommand>();
// go through each defined command
foreach (var command in commands.AsArray())
@ -306,9 +354,10 @@ namespace IW4MAdmin.Application.Misc
string alias = dynamicCommand.alias;
string description = dynamicCommand.description;
string permission = dynamicCommand.permission;
bool targetRequired = false;
List<Server.Game> supportedGames = null;
var targetRequired = false;
List<(string, bool)> args = new List<(string, bool)>();
var args = new List<(string, bool)>();
dynamic arguments = null;
try
@ -339,26 +388,85 @@ namespace IW4MAdmin.Application.Misc
}
}
void execute(GameEvent e)
try
{
_scriptEngine.SetValue("_event", e);
var jsEventObject = _scriptEngine.GetValue("_event");
foreach (var game in dynamicCommand.supportedGames)
{
supportedGames ??= new List<Server.Game>();
supportedGames.Add(Enum.Parse(typeof(Server.Game), game.ToString()));
}
}
catch (RuntimeBinderException)
{
// supported games is optional
}
async Task Execute(GameEvent gameEvent)
{
try
{
dynamicCommand.execute.Target.Invoke(jsEventObject);
await _onProcessing.WaitAsync();
_scriptEngine.SetValue("_event", gameEvent);
var jsEventObject = _scriptEngine.Evaluate("_event");
dynamicCommand.execute.Target.Invoke(_scriptEngine, jsEventObject);
}
catch (JavaScriptException ex)
{
throw new PluginException($"An error occured while executing action for script plugin: {ex.Error} (Line: {ex.Location.Start.Line}, Character: {ex.Location.Start.Column})") { PluginFile = _fileName };
using (LogContext.PushProperty("Server", gameEvent.Owner?.ToString()))
{
_logger.LogError(ex, "Could not execute command action for {Filename} {@Location}",
Path.GetFileName(_fileName), ex.Location);
}
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", gameEvent.Owner?.ToString()))
{
_logger.LogError(ex,
"Could not execute command action for script plugin {FileName}",
Path.GetFileName(_fileName));
}
throw new PluginException("An error occured while executing action for script plugin");
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, targetRequired, args, execute));
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission,
targetRequired, args, Execute, supportedGames?.ToArray()));
}
return commandList;
}
}
public class PermissionLevelToStringConverter : IObjectConverter
{
public bool TryConvert(Engine engine, object value, out JsValue result)
{
if (value is Data.Models.Client.EFClient.Permission)
{
result = value.ToString();
return true;
}
result = JsValue.Null;
return false;
}
}
}

View File

@ -0,0 +1,95 @@
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 ScriptPluginConfiguration _config;
private readonly string _pluginName;
private readonly Engine _scriptEngine;
public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine)
{
_handler = new BaseConfigurationHandler<ScriptPluginConfiguration>("ScriptPluginSettings");
_pluginName = pluginName;
_scriptEngine = scriptEngine;
}
public async Task InitializeAsync()
{
await _handler.BuildAsync();
_config = _handler.Configuration() ??
(ScriptPluginConfiguration) new ScriptPluginConfiguration().Generate();
}
private static int? AsInteger(double d)
{
return int.TryParse(d.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : (int?) null;
}
public async Task SetValue(string key, object value)
{
var castValue = value;
if (value is double d)
{
castValue = AsInteger(d) ?? value;
}
if (value is object[] array && array.All(item => item is double d && AsInteger(d) != null))
{
castValue = array.Select(item => AsInteger((double)item)).ToArray();
}
if (!_config.ContainsKey(_pluginName))
{
_config.Add(_pluginName, new Dictionary<string, object>());
}
var plugin = _config[_pluginName];
if (plugin.ContainsKey(key))
{
plugin[key] = castValue;
}
else
{
plugin.Add(key, castValue);
}
_handler.Set(_config);
await _handler.Save();
}
public JsValue GetValue(string key)
{
if (!_config.ContainsKey(_pluginName))
{
return JsValue.Undefined;
}
if (!_config[_pluginName].ContainsKey(key))
{
return JsValue.Undefined;
}
var item = _config[_pluginName][key];
if (item is JArray array)
{
item = array.ToObject<List<dynamic>>();
}
return JsValue.FromObject(_scriptEngine, item);
}
}
}

View File

@ -0,0 +1,157 @@
using System;
using System.Threading;
using Jint.Native;
using Jint.Runtime;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc;
public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{
private Timer _timer;
private Action _actions;
private Delegate _jsAction;
private string _actionName;
private const int DefaultDelay = 0;
private const int DefaultInterval = 1000;
private readonly ILogger _logger;
private readonly ManualResetEventSlim _onRunningTick = new();
private SemaphoreSlim _onDependentAction;
public ScriptPluginTimerHelper(ILogger<ScriptPluginTimerHelper> logger)
{
_logger = logger;
}
~ScriptPluginTimerHelper()
{
if (_timer != null)
{
Stop();
}
_onRunningTick.Dispose();
}
public void Start(int delay, int interval)
{
if (_actions is null)
{
throw new InvalidOperationException("Timer action must be defined before starting");
}
if (delay < 0)
{
throw new ArgumentException("Timer delay must be >= 0");
}
if (interval < 20)
{
throw new ArgumentException("Timer interval must be at least 20ms");
}
Stop();
_logger.LogDebug("Starting script timer...");
_onRunningTick.Set();
_timer ??= new Timer(callback => _actions(), null, delay, interval);
IsRunning = true;
}
public void Start(int interval)
{
Start(DefaultDelay, interval);
}
public void Start()
{
Start(DefaultDelay, DefaultInterval);
}
public void Stop()
{
if (_timer == null)
{
return;
}
_logger.LogDebug("Stopping script timer...");
_timer.Change(Timeout.Infinite, Timeout.Infinite);
_timer.Dispose();
_timer = null;
IsRunning = false;
}
public void OnTick(Delegate action, string actionName)
{
if (string.IsNullOrEmpty(actionName))
{
throw new ArgumentException("actionName must be provided", nameof(actionName));
}
if (action is null)
{
throw new ArgumentException("action must be provided", nameof(action));
}
_logger.LogDebug("Adding new action with name {ActionName}", actionName);
_jsAction = action;
_actionName = actionName;
_actions = OnTick;
}
private void ReleaseThreads()
{
_onRunningTick.Set();
if (_onDependentAction?.CurrentCount != 0)
{
return;
}
_onDependentAction?.Release(1);
}
private void OnTick()
{
try
{
if (!_onRunningTick.IsSet)
{
_logger.LogWarning("Previous {OnTick} is still running, so we are skipping this one",
nameof(OnTick));
return;
}
_onRunningTick.Reset();
// the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously
_onDependentAction?.WaitAsync().Wait();
_jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined });
ReleaseThreads();
}
catch (Exception ex) when (ex.InnerException is JavaScriptException jsex)
{
_logger.LogError(jsex,
"Could not execute timer tick for script action {ActionName} [@{LocationInfo}]", _actionName,
jsex.Location);
ReleaseThreads();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not execute timer tick for script action {ActionName}", _actionName);
_onRunningTick.Set();
ReleaseThreads();
}
}
public void SetDependency(SemaphoreSlim dependentSemaphore)
{
_onDependentAction = dependentSemaphore;
}
public bool IsRunning { get; private set; }
}

View File

@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Data.Models.Client;
using Data.Models.Client.Stats.Reference;
using Data.Models.Server;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc
{
/// <inheritdoc/>
public class ServerDataCollector : IServerDataCollector
{
private readonly ILogger _logger;
private readonly IManager _manager;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ApplicationConfiguration _appConfig;
private readonly IEventPublisher _eventPublisher;
private bool _inProgress;
private TimeSpan _period;
public ServerDataCollector(ILogger<ServerDataCollector> logger, ApplicationConfiguration appConfig,
IManager manager, IDatabaseContextFactory contextFactory, IEventPublisher eventPublisher)
{
_logger = logger;
_appConfig = appConfig;
_manager = manager;
_contextFactory = contextFactory;
_eventPublisher = eventPublisher;
_eventPublisher.OnClientConnect += SaveConnectionInfo;
_eventPublisher.OnClientDisconnect += SaveConnectionInfo;
}
~ServerDataCollector()
{
_eventPublisher.OnClientConnect -= SaveConnectionInfo;
_eventPublisher.OnClientDisconnect -= SaveConnectionInfo;
}
public async Task BeginCollectionAsync(TimeSpan? period = null, CancellationToken cancellationToken = default)
{
if (_inProgress)
{
throw new InvalidOperationException($"{nameof(ServerDataCollector)} is already collecting data");
}
_logger.LogDebug("Initializing data collection with {Name}", nameof(ServerDataCollector));
_inProgress = true;
_period = period ?? (Utilities.IsDevelopment
? TimeSpan.FromMinutes(1)
: _appConfig.ServerDataCollectionInterval);
while (!cancellationToken.IsCancellationRequested)
{
try
{
await Task.Delay(_period, cancellationToken);
_logger.LogDebug("{Name} is collecting server data", nameof(ServerDataCollector));
var data = await BuildCollectionData(cancellationToken);
await SaveData(data, cancellationToken);
}
catch (TaskCanceledException)
{
_logger.LogInformation("Shutdown requested for {Name}", nameof(ServerDataCollector));
return;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error encountered collecting server data for {Name}",
nameof(ServerDataCollector));
}
}
}
private async Task<IEnumerable<EFServerSnapshot>> BuildCollectionData(CancellationToken token)
{
var data = await Task.WhenAll(_manager.GetServers()
.Select(async server => new EFServerSnapshot
{
CapturedAt = DateTime.UtcNow,
PeriodBlock = (int) (DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch).TotalMinutes,
ServerId = await server.GetIdForServer(),
MapId = await GetOrCreateMap(server.CurrentMap.Name, (Reference.Game) server.GameName, token),
ClientCount = server.ClientNum
}));
return data;
}
private async Task<int> GetOrCreateMap(string mapName, Reference.Game game, CancellationToken token)
{
await using var context = _contextFactory.CreateContext();
var existingMap =
await context.Maps.FirstOrDefaultAsync(map => map.Name == mapName && map.Game == game, token);
if (existingMap != null)
{
return existingMap.MapId;
}
var newMap = new EFMap
{
Name = mapName,
Game = game
};
context.Maps.Add(newMap);
await context.SaveChangesAsync(token);
return newMap.MapId;
}
private async Task SaveData(IEnumerable<EFServerSnapshot> snapshots, CancellationToken token)
{
await using var context = _contextFactory.CreateContext();
context.ServerSnapshots.AddRange(snapshots);
await context.SaveChangesAsync(token);
}
private void SaveConnectionInfo(object sender, GameEvent gameEvent)
{
using var context = _contextFactory.CreateContext(enableTracking: false);
context.ConnectionHistory.Add(new EFClientConnectionHistory
{
ClientId = gameEvent.Origin.ClientId,
ServerId = gameEvent.Owner.GetIdForServer().Result,
ConnectionType = gameEvent.Type == GameEvent.EventType.Connect
? Reference.ConnectionType.Connect
: Reference.ConnectionType.Disconnect
});
context.SaveChanges();
}
}
}

View File

@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Server;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.Misc
{
/// <inheritdoc/>
public class ServerDataViewer : IServerDataViewer
{
private readonly ILogger _logger;
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache,
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache)
{
_logger = logger;
_snapshotCache = snapshotCache;
_serverStatsCache = serverStatsCache;
_clientHistoryCache = clientHistoryCache;
}
public async Task<(int?, DateTime?)>
MaxConcurrentClientsAsync(long? serverId = null, TimeSpan? overPeriod = null,
CancellationToken token = default)
{
_snapshotCache.SetCacheItem(async (snapshots, cancellationToken) =>
{
var oldestEntry = overPeriod.HasValue
? DateTime.UtcNow - overPeriod.Value
: DateTime.UtcNow.AddDays(-1);
int? maxClients;
DateTime? maxClientsTime;
if (serverId != null)
{
var clients = await snapshots.Where(snapshot => snapshot.ServerId == serverId)
.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.OrderByDescending(snapshot => snapshot.ClientCount)
.Select(snapshot => new
{
snapshot.ClientCount,
snapshot.CapturedAt
})
.FirstOrDefaultAsync(cancellationToken);
maxClients = clients?.ClientCount;
maxClientsTime = clients?.CapturedAt;
}
else
{
var clients = await snapshots.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.GroupBy(snapshot => snapshot.PeriodBlock)
.Select(grp => new
{
ClientCount = grp.Sum(snapshot => (int?) snapshot.ClientCount),
Time = grp.Max(snapshot => (DateTime?) snapshot.CapturedAt)
})
.OrderByDescending(snapshot => snapshot.ClientCount)
.FirstOrDefaultAsync(cancellationToken);
maxClients = clients?.ClientCount;
maxClientsTime = clients?.Time;
}
_logger.LogDebug("Max concurrent clients since {Start} is {Clients}", oldestEntry, maxClients);
return (maxClients, maxClientsTime);
}, nameof(MaxConcurrentClientsAsync), _cacheTimeSpan, true);
try
{
return await _snapshotCache.GetCacheItem(nameof(MaxConcurrentClientsAsync), token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(MaxConcurrentClientsAsync));
return (null, null);
}
}
public async Task<(int, int)> ClientCountsAsync(TimeSpan? overPeriod = null, CancellationToken token = default)
{
_serverStatsCache.SetCacheItem(async (set, cancellationToken) =>
{
var count = await set.CountAsync(cancellationToken);
var startOfPeriod =
DateTime.UtcNow.AddHours(-overPeriod?.TotalHours ?? -24);
var recentCount = await set.CountAsync(client => client.LastConnection >= startOfPeriod,
cancellationToken);
return (count, recentCount);
}, nameof(_serverStatsCache), _cacheTimeSpan, true);
try
{
return await _serverStatsCache.GetCacheItem(nameof(_serverStatsCache), token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(ClientCountsAsync));
return (0, 0);
}
}
public async Task<IEnumerable<ClientHistoryInfo>> ClientHistoryAsync(TimeSpan? overPeriod = null, CancellationToken token = default)
{
_clientHistoryCache.SetCacheItem(async (set, cancellationToken) =>
{
var oldestEntry = overPeriod.HasValue
? DateTime.UtcNow - overPeriod.Value
: DateTime.UtcNow.AddHours(-12);
var history = await set.Where(snapshot => snapshot.CapturedAt >= oldestEntry)
.Select(snapshot =>
new
{
snapshot.ServerId,
snapshot.CapturedAt,
snapshot.ClientCount
})
.OrderBy(snapshot => snapshot.CapturedAt)
.ToListAsync(cancellationToken);
return history.GroupBy(snapshot => snapshot.ServerId).Select(byServer => new ClientHistoryInfo
{
ServerId = byServer.Key,
ClientCounts = byServer.Select(snapshot => new ClientCountSnapshot()
{Time = snapshot.CapturedAt, ClientCount = snapshot.ClientCount}).ToList()
}).ToList();
}, nameof(_clientHistoryCache), TimeSpan.MaxValue);
try
{
return await _clientHistoryCache.GetCacheItem(nameof(_clientHistoryCache), token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(ClientHistoryAsync));
return Enumerable.Empty<ClientHistoryInfo>();
}
}
}
}

View File

@ -7,26 +7,26 @@ using System.Text;
namespace IW4MAdmin.Application.Misc
{
class TokenAuthentication : ITokenAuthentication
internal class TokenAuthentication : ITokenAuthentication
{
private readonly ConcurrentDictionary<long, TokenState> _tokens;
private readonly RNGCryptoServiceProvider _random;
private readonly static TimeSpan _timeoutPeriod = new TimeSpan(0, 0, 120);
private const short TOKEN_LENGTH = 4;
private readonly RandomNumberGenerator _random;
private static readonly TimeSpan TimeoutPeriod = new TimeSpan(0, 0, 120);
private const short TokenLength = 4;
public TokenAuthentication()
{
_tokens = new ConcurrentDictionary<long, TokenState>();
_random = new RNGCryptoServiceProvider();
_random = RandomNumberGenerator.Create();
}
public bool AuthorizeToken(long networkId, string token)
{
bool authorizeSuccessful = _tokens.ContainsKey(networkId) && _tokens[networkId].Token == token;
var authorizeSuccessful = _tokens.ContainsKey(networkId) && _tokens[networkId].Token == token;
if (authorizeSuccessful)
{
_tokens.TryRemove(networkId, out TokenState _);
_tokens.TryRemove(networkId, out _);
}
return authorizeSuccessful;
@ -34,15 +34,15 @@ namespace IW4MAdmin.Application.Misc
public TokenState GenerateNextToken(long networkId)
{
TokenState state = null;
TokenState state;
if (_tokens.ContainsKey(networkId))
{
state = _tokens[networkId];
if ((DateTime.Now - state.RequestTime) > _timeoutPeriod)
if ((DateTime.Now - state.RequestTime) > TimeoutPeriod)
{
_tokens.TryRemove(networkId, out TokenState _);
_tokens.TryRemove(networkId, out _);
}
else
@ -51,11 +51,11 @@ namespace IW4MAdmin.Application.Misc
}
}
state = new TokenState()
state = new TokenState
{
NetworkId = networkId,
Token = _generateToken(),
TokenDuration = _timeoutPeriod
TokenDuration = TimeoutPeriod
};
_tokens.TryAdd(networkId, state);
@ -63,31 +63,31 @@ namespace IW4MAdmin.Application.Misc
// perform some housekeeping so we don't have built up tokens if they're not ever used
foreach (var (key, value) in _tokens)
{
if ((DateTime.Now - value.RequestTime) > _timeoutPeriod)
if ((DateTime.Now - value.RequestTime) > TimeoutPeriod)
{
_tokens.TryRemove(key, out TokenState _);
_tokens.TryRemove(key, out _);
}
}
return state;
}
public string _generateToken()
private string _generateToken()
{
bool validCharacter(char c)
bool ValidCharacter(char c)
{
// this ensure that the characters are 0-9, A-Z, a-z
return (c > 47 && c < 58) || (c > 64 && c < 91) || (c > 96 && c < 123);
}
StringBuilder token = new StringBuilder();
var token = new StringBuilder();
while (token.Length < TOKEN_LENGTH)
var charSet = new byte[1];
while (token.Length < TokenLength)
{
byte[] charSet = new byte[1];
_random.GetBytes(charSet);
if (validCharacter((char)charSet[0]))
if (ValidCharacter((char)charSet[0]))
{
token.Append((char)charSet[0]);
}

View File

@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.RconParsers
namespace IW4MAdmin.Application.RConParsers
{
public class BaseRConParser : IRConParser
{
@ -56,6 +56,7 @@ namespace IW4MAdmin.Application.RconParsers
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4);
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.GametypeStatus.Pattern = "";
@ -73,11 +74,13 @@ namespace IW4MAdmin.Application.RconParsers
public Game GameName { get; set; } = Game.COD;
public bool CanGenerateLogPath { get; set; } = true;
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)
{
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)
@ -128,7 +131,7 @@ namespace IW4MAdmin.Application.RconParsers
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)),
DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)),
LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)),
@ -136,53 +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);
_logger.LogDebug("Status Response {response}", string.Join(Environment.NewLine, response));
return (ClientsFromStatus(response), MapFromStatus(response), GameTypeFromStatus(response));
return new StatusResponse
{
Clients = ClientsFromStatus(response).ToArray(),
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus.Pattern),
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus.Pattern),
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)
{
var regex = Regex.Match(line, Configuration.MapStatus.Pattern);
var regex = Regex.Match(line, groupPattern);
if (regex.Success)
{
map = regex.Groups[Configuration.MapStatus.GroupMapping[ParserRegex.GroupType.RConStatusMap]].ToString();
value = regex.Groups[Configuration.MapStatus.GroupMapping[groupType]].ToString();
}
}
return map;
}
private string GameTypeFromStatus(string[] response)
{
if (string.IsNullOrWhiteSpace(Configuration.GametypeStatus.Pattern))
if (value == null)
{
return null;
return default;
}
string gametype = null;
foreach (var line in response)
if (typeof(T) == typeof(int?))
{
var regex = Regex.Match(line, Configuration.GametypeStatus.Pattern);
if (regex.Success)
{
gametype = regex.Groups[Configuration.GametypeStatus.GroupMapping[ParserRegex.GroupType.RConStatusGametype]].ToString();
}
return (T)Convert.ChangeType(int.Parse(value), Nullable.GetUnderlyingType(typeof(T)));
}
return gametype;
return (T)Convert.ChangeType(value, typeof(T));
}
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue)
{
string dvarString = (dvarValue is string str)
? $"{dvarName} \"{str}\""
: $"{dvarName} {dvarValue.ToString()}";
: $"{dvarName} {dvarValue}";
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0;
}
@ -212,10 +217,15 @@ namespace IW4MAdmin.Application.RconParsers
continue;
}
int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]);
int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]);
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
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3)
@ -224,7 +234,7 @@ namespace IW4MAdmin.Application.RconParsers
}
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();
@ -258,6 +268,17 @@ namespace IW4MAdmin.Application.RconParsers
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);
}
}

View File

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

View File

@ -3,8 +3,9 @@ using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
using System.Collections.Generic;
using System.Globalization;
using SharedLibraryCore.Formatting;
namespace IW4MAdmin.Application.RconParsers
namespace IW4MAdmin.Application.RConParsers
{
/// <summary>
/// generic implementation of the IRConParserConfiguration
@ -16,6 +17,8 @@ namespace IW4MAdmin.Application.RconParsers
public ParserRegex Status { get; set; }
public ParserRegex MapStatus { get; set; }
public ParserRegex GametypeStatus { get; set; }
public ParserRegex HostnameStatus { get; set; }
public ParserRegex MaxPlayersStatus { get; set; }
public ParserRegex Dvar { get; set; }
public ParserRegex StatusHeader { get; set; }
public string ServerNotRunningResponse { get; set; }
@ -26,6 +29,25 @@ namespace IW4MAdmin.Application.RconParsers
public int NoticeMaximumLines { get; set; } = 8;
public int NoticeMaxCharactersPerLine { get; set; } = 50;
public string NoticeLineSeparator { get; set; } = Environment.NewLine;
public int? DefaultRConPort { get; set; }
public string DefaultInstallationDirectoryHint { get; set; }
public short FloodProtectInterval { get; set; } = 750;
public ColorCodeMapping ColorCodeMapping { get; set; } = new ColorCodeMapping
{
// this is the default mapping (IW4), but can be overridden as needed in the parsers
{ColorCodes.Black.ToString(), "^0"},
{ColorCodes.Red.ToString(), "^1"},
{ColorCodes.Green.ToString(), "^2"},
{ColorCodes.Yellow.ToString(), "^3"},
{ColorCodes.Blue.ToString(), "^4"},
{ColorCodes.Cyan.ToString(), "^5"},
{ColorCodes.Pink.ToString(), "^6"},
{ColorCodes.White.ToString(), "^7"},
{ColorCodes.Map.ToString(), "^8"},
{ColorCodes.Grey.ToString(), "^9"},
{ColorCodes.Wildcard.ToString(), ":^"},
};
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
{
@ -34,6 +56,8 @@ namespace IW4MAdmin.Application.RconParsers
GametypeStatus = parserRegexFactory.CreateParserRegex();
Dvar = 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

@ -0,0 +1,10 @@
using System;
namespace Data.Abstractions
{
public class IAuditFields
{
DateTime CreatedDateTime { get; set; }
DateTime? UpdatedDateTime { get; set; }
}
}

View File

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

View File

@ -23,7 +23,7 @@ namespace Data.Context
{
var link = new EFAliasLink();
context.Clients.Add(new EFClient()
context.Clients.Add(new EFClient
{
Active = false,
Connections = 0,
@ -33,7 +33,7 @@ namespace Data.Context
Masked = true,
NetworkId = 0,
AliasLink = link,
CurrentAlias = new EFAlias()
CurrentAlias = new EFAlias
{
Link = link,
Active = true,
@ -46,4 +46,4 @@ namespace Data.Context
}
}
}
}
}

View File

@ -7,6 +7,7 @@ using Data.Models;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Client.Stats.Reference;
using Data.Models.Misc;
using Data.Models.Server;
namespace Data.Context
@ -35,6 +36,14 @@ namespace Data.Context
public DbSet<EFWeapon> Weapons { get; set; }
public DbSet<EFWeaponAttachment> WeaponAttachments { get; set; }
public DbSet<EFMap> Maps { get; set; }
#endregion
#region MISC
public DbSet<EFInboxMessage> InboxMessages { get; set; }
public DbSet<EFServerSnapshot> ServerSnapshots { get;set; }
public DbSet<EFClientConnectionHistory> ConnectionHistory { get; set; }
#endregion
@ -109,7 +118,7 @@ namespace Data.Context
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();
ent.HasIndex(_alias => new {_alias.Name, _alias.IPAddress});
});
modelBuilder.Entity<EFMeta>(ent =>
@ -121,15 +130,19 @@ namespace Data.Context
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity<EFClientConnectionHistory>(ent => ent.HasIndex(history => history.CreatedDateTime));
// 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");
modelBuilder.Entity<EFServerSnapshot>().ToTable(nameof(EFServerSnapshot));
modelBuilder.Entity<EFClientConnectionHistory>().ToTable(nameof(EFClientConnectionHistory));
Models.Configuration.StatsModelConfiguration.Configure(modelBuilder);
base.OnModelCreating(modelBuilder);
}
}
}
}

View File

@ -1,82 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</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.2.0</PackageVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Migrations\MySql\20210210221342_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\MySql\20210210221342_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210224014503_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Postgresql\20210224014503_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210224030227_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Postgresql\20210224030227_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210224031245_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Postgresql\20210224031245_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210227041237_AddPerformancePercentileToClientStats.cs" />
<Compile Remove="Migrations\Postgresql\20210227041237_AddPerformancePercentileToClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210227161333_AddPerformancePercentileToClientStats.cs" />
<Compile Remove="Migrations\Postgresql\20210227161333_AddPerformancePercentileToClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210307163752_AddRankingHistory.cs" />
<Compile Remove="Migrations\Postgresql\20210307163752_AddRankingHistory.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210209205243_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210209205243_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210209205948_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210209205948_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210209211745_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210209211745_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210209212725_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210209212725_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210210020314_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210210020314_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210210140835_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210210140835_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210210154738_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210210154738_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210210163803_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210210163803_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210210193852_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210210193852_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210211033835_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210211033835_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210219013429_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210219013429_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210220171950_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210220171950_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210223163022_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210223163022_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210226215929_AddPerformancePercentileToClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210226215929_AddPerformancePercentileToClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210227160800_AddPerformancePercentileToClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210227160800_AddPerformancePercentileToClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210305033616_AddRankingHistory.cs" />
<Compile Remove="Migrations\Sqlite\20210305033616_AddRankingHistory.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210305033846_AddRankingHistory.cs" />
<Compile Remove="Migrations\Sqlite\20210305033846_AddRankingHistory.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210306223712_AddRankingHistory.cs" />
<Compile Remove="Migrations\Sqlite\20210306223712_AddRankingHistory.Designer.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.10">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
<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" />
<PackageReference Include="Npgsql" Version="6.0.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
</ItemGroup>
</Project>

View File

@ -1,54 +1,84 @@
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Timer = System.Timers.Timer;
namespace Data.Helpers
{
public class DataValueCache<T, V> : IDataValueCache<T, V> where T : class
public class DataValueCache<TEntityType, TReturnType> : IDataValueCache<TEntityType, TReturnType>
where TEntityType : 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
private readonly ConcurrentDictionary<string, CacheState<TReturnType>> _cacheStates =
new ConcurrentDictionary<string, CacheState<TReturnType>>();
private bool _autoRefresh;
private const int DefaultExpireMinutes = 15;
private Timer _timer;
private class CacheState<TCacheType>
{
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 Func<DbSet<TEntityType>, CancellationToken, Task<TCacheType>> Getter { get; set; }
public TCacheType Value { get; set; }
public bool IsSet { get; set; }
public bool IsExpired => ExpirationTime != TimeSpan.MaxValue &&
(DateTime.Now - LastRetrieval.Add(ExpirationTime)).TotalSeconds > 0;
}
public DataValueCache(ILogger<DataValueCache<T, V>> logger, IDatabaseContextFactory contextFactory)
public DataValueCache(ILogger<DataValueCache<TEntityType, TReturnType>> logger,
IDatabaseContextFactory contextFactory)
{
_logger = logger;
_contextFactory = contextFactory;
}
public void SetCacheItem(Func<DbSet<T>, Task<V>> getter, string key, TimeSpan? expirationTime = null)
~DataValueCache()
{
_timer?.Stop();
_timer?.Dispose();
}
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
TimeSpan? expirationTime = null, bool autoRefresh = false)
{
if (_cacheStates.ContainsKey(key))
{
_logger.LogDebug("Cache key {key} is already added", key);
_logger.LogDebug("Cache key {Key} is already added", key);
return;
}
var state = new CacheState()
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_cacheStates.Add(key, state);
_autoRefresh = autoRefresh;
_cacheStates.TryAdd(key, state);
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
}
public async Task<V> GetCacheItem(string keyName)
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default)
{
if (!_cacheStates.ContainsKey(keyName))
{
@ -57,27 +87,31 @@ namespace Data.Helpers
var state = _cacheStates[keyName];
if (state.IsExpired)
// when auto refresh is off we want to check the expiration and value
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
if ((state.IsExpired || !state.IsSet) && !_autoRefresh || _autoRefresh && !state.IsSet)
{
await RunCacheUpdate(state);
await RunCacheUpdate(state, cancellationToken);
}
return state.Value;
}
private async Task RunCacheUpdate(CacheState state)
private async Task RunCacheUpdate(CacheState<TReturnType> state, CancellationToken token)
{
try
{
_logger.LogDebug("Running update for {ClassName} {@State}", GetType().Name, state);
await using var context = _contextFactory.CreateContext(false);
var set = context.Set<T>();
var value = await state.Getter(set);
var set = context.Set<TEntityType>();
var value = await state.Getter(set, token);
state.Value = value;
state.IsSet = true;
state.LastRetrieval = DateTime.Now;
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not get cached value for {key}", state.Key);
_logger.LogError(ex, "Could not get cached value for {Key}", state.Key);
}
}
}

View File

@ -102,7 +102,7 @@ namespace Data.Helpers
{
try
{
await using var context = _contextFactory.CreateContext();
await using var context = _contextFactory.CreateContext(false);
_cachedItems = await context.Set<T>().ToDictionaryAsync(item => item.Id);
}
catch (Exception ex)
@ -111,4 +111,4 @@ namespace Data.Helpers
}
}
}
}
}

View File

@ -24,10 +24,10 @@ namespace Data.MigrationContext
{
if (MigrationExtensions.IsMigration)
{
optionsBuilder.UseMySql("Server=127.0.0.1;Database=IW4MAdmin_Migration;Uid=root;Pwd=password;")
.EnableDetailedErrors(true)
.EnableSensitiveDataLogging(true);
optionsBuilder.UseMySql(ServerVersion.AutoDetect("Server=127.0.0.1;Database=IW4MAdmin_Migration;Uid=root;Pwd=password;"))
.EnableDetailedErrors()
.EnableSensitiveDataLogging();
}
}
}
}
}

View File

@ -25,10 +25,10 @@ namespace Data.MigrationContext
{
optionsBuilder.UseNpgsql(
"Host=127.0.0.1;Database=IW4MAdmin_Migration;Username=postgres;Password=password;",
options => options.SetPostgresVersion(new Version("9.4")))
options => options.SetPostgresVersion(new Version("12.9")))
.EnableDetailedErrors(true)
.EnableSensitiveDataLogging(true);
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.MySql
{
public partial class AddWeaponReferenceToEFClientKill : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "WeaponReference",
table: "EFClientKills",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "WeaponReference",
table: "EFClientKills");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.MySql
{
public partial class AddWeaponReferenceAndServerIdToEFACSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "ServerId",
table: "EFACSnapshot",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "WeaponReference",
table: "EFACSnapshot",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_EFACSnapshot_ServerId",
table: "EFACSnapshot",
column: "ServerId");
migrationBuilder.AddForeignKey(
name: "FK_EFACSnapshot_EFServers_ServerId",
table: "EFACSnapshot",
column: "ServerId",
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Restrict);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_EFACSnapshot_EFServers_ServerId",
table: "EFACSnapshot");
migrationBuilder.DropIndex(
name: "IX_EFACSnapshot_ServerId",
table: "EFACSnapshot");
migrationBuilder.DropColumn(
name: "ServerId",
table: "EFACSnapshot");
migrationBuilder.DropColumn(
name: "WeaponReference",
table: "EFACSnapshot");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.MySql
{
public partial class AddHitLocationReferenceToEFACSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "HitLocationReference",
table: "EFACSnapshot",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HitLocationReference",
table: "EFACSnapshot");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.MySql
{
public partial class AddEFInboxMessage : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "InboxMessages",
columns: table => new
{
InboxMessageId = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
CreatedDateTime = table.Column<DateTime>(nullable: false),
UpdatedDateTime = table.Column<DateTime>(nullable: true),
SourceClientId = table.Column<int>(nullable: false),
DestinationClientId = table.Column<int>(nullable: false),
ServerId = table.Column<long>(nullable: true),
Message = table.Column<string>(nullable: true),
IsDelivered = table.Column<bool>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_InboxMessages", x => x.InboxMessageId);
table.ForeignKey(
name: "FK_InboxMessages_EFClients_DestinationClientId",
column: x => x.DestinationClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_InboxMessages_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_InboxMessages_EFClients_SourceClientId",
column: x => x.SourceClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_InboxMessages_DestinationClientId",
table: "InboxMessages",
column: "DestinationClientId");
migrationBuilder.CreateIndex(
name: "IX_InboxMessages_ServerId",
table: "InboxMessages",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_InboxMessages_SourceClientId",
table: "InboxMessages",
column: "SourceClientId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "InboxMessages");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.MySql
{
public partial class AddEFServerSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EFServerSnapshot",
columns: table => new
{
ServerSnapshotId = table.Column<long>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Active = table.Column<bool>(nullable: false),
CapturedAt = table.Column<DateTime>(nullable: false),
PeriodBlock = table.Column<int>(nullable: false),
ServerId = table.Column<long>(nullable: false),
MapId = table.Column<int>(nullable: false),
ClientCount = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFServerSnapshot", x => x.ServerSnapshotId);
table.ForeignKey(
name: "FK_EFServerSnapshot_EFMaps_MapId",
column: x => x.MapId,
principalTable: "EFMaps",
principalColumn: "MapId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFServerSnapshot_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_MapId",
table: "EFServerSnapshot",
column: "MapId");
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_ServerId",
table: "EFServerSnapshot",
column: "ServerId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EFServerSnapshot");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.MySql
{
public partial class AddEFClientConnectionHistory : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EFClientConnectionHistory",
columns: table => new
{
ClientConnectionId = table.Column<long>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
CreatedDateTime = table.Column<DateTime>(nullable: false),
UpdatedDateTime = table.Column<DateTime>(nullable: true),
ClientId = table.Column<int>(nullable: false),
ServerId = table.Column<long>(nullable: false),
ConnectionType = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFClientConnectionHistory", x => x.ClientConnectionId);
table.ForeignKey(
name: "FK_EFClientConnectionHistory_EFClients_ClientId",
column: x => x.ClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFClientConnectionHistory_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_EFClientConnectionHistory_ClientId",
table: "EFClientConnectionHistory",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_EFClientConnectionHistory_CreatedDateTime",
table: "EFClientConnectionHistory",
column: "CreatedDateTime");
migrationBuilder.CreateIndex(
name: "IX_EFClientConnectionHistory_ServerId",
table: "EFClientConnectionHistory",
column: "ServerId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EFClientConnectionHistory");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.MySql
{
public partial class RemoveUniqueAliasIndexConstraint : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFAlias_Name_IPAddress",
table: "EFAlias");
migrationBuilder.CreateIndex(
name: "IX_EFAlias_Name_IPAddress",
table: "EFAlias",
columns: new[] { "Name", "IPAddress" });
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFAlias_Name_IPAddress",
table: "EFAlias");
migrationBuilder.CreateIndex(
name: "IX_EFAlias_Name_IPAddress",
table: "EFAlias",
columns: new[] { "Name", "IPAddress" },
unique: true);
}
}
}

View File

@ -95,6 +95,38 @@ namespace Data.Migrations.MySql
b.ToTable("EFClients");
});
modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b =>
{
b.Property<long>("ClientConnectionId")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<int>("ClientId")
.HasColumnType("int");
b.Property<int>("ConnectionType")
.HasColumnType("int");
b.Property<DateTime>("CreatedDateTime")
.HasColumnType("datetime(6)");
b.Property<long>("ServerId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedDateTime")
.HasColumnType("datetime(6)");
b.HasKey("ClientConnectionId");
b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("ServerId");
b.ToTable("EFClientConnectionHistory");
});
modelBuilder.Entity("Data.Models.Client.EFClientKill", b =>
{
b.Property<long>("KillId")
@ -146,6 +178,9 @@ namespace Data.Migrations.MySql
b.Property<int>("Weapon")
.HasColumnType("int");
b.Property<string>("WeaponReference")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<DateTime>("When")
.HasColumnType("datetime(6)");
@ -237,6 +272,9 @@ namespace Data.Migrations.MySql
b.Property<int>("HitLocation")
.HasColumnType("int");
b.Property<string>("HitLocationReference")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<int>("HitOriginId")
.HasColumnType("int");
@ -255,6 +293,9 @@ namespace Data.Migrations.MySql
b.Property<double>("RecoilOffset")
.HasColumnType("double");
b.Property<long?>("ServerId")
.HasColumnType("bigint");
b.Property<double>("SessionAngleOffset")
.HasColumnType("double");
@ -279,6 +320,9 @@ namespace Data.Migrations.MySql
b.Property<int>("WeaponId")
.HasColumnType("int");
b.Property<string>("WeaponReference")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<DateTime>("When")
.HasColumnType("datetime(6)");
@ -294,6 +338,8 @@ namespace Data.Migrations.MySql
b.HasIndex("LastStrainAngleId");
b.HasIndex("ServerId");
b.ToTable("EFACSnapshot");
});
@ -770,8 +816,7 @@ namespace Data.Migrations.MySql
b.HasIndex("SearchableName");
b.HasIndex("Name", "IPAddress")
.IsUnique();
b.HasIndex("Name", "IPAddress");
b.ToTable("EFAlias");
});
@ -921,6 +966,44 @@ namespace Data.Migrations.MySql
b.ToTable("EFPenalties");
});
modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b =>
{
b.Property<int>("InboxMessageId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedDateTime")
.HasColumnType("datetime(6)");
b.Property<int>("DestinationClientId")
.HasColumnType("int");
b.Property<bool>("IsDelivered")
.HasColumnType("tinyint(1)");
b.Property<string>("Message")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<long?>("ServerId")
.HasColumnType("bigint");
b.Property<int>("SourceClientId")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedDateTime")
.HasColumnType("datetime(6)");
b.HasKey("InboxMessageId");
b.HasIndex("DestinationClientId");
b.HasIndex("ServerId");
b.HasIndex("SourceClientId");
b.ToTable("InboxMessages");
});
modelBuilder.Entity("Data.Models.Server.EFServer", b =>
{
b.Property<long>("ServerId")
@ -949,6 +1032,39 @@ namespace Data.Migrations.MySql
b.ToTable("EFServers");
});
modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b =>
{
b.Property<long>("ServerSnapshotId")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<bool>("Active")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("CapturedAt")
.HasColumnType("datetime(6)");
b.Property<int>("ClientCount")
.HasColumnType("int");
b.Property<int>("MapId")
.HasColumnType("int");
b.Property<int>("PeriodBlock")
.HasColumnType("int");
b.Property<long>("ServerId")
.HasColumnType("bigint");
b.HasKey("ServerSnapshotId");
b.HasIndex("MapId");
b.HasIndex("ServerId");
b.ToTable("EFServerSnapshot");
});
modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b =>
{
b.Property<int>("StatisticId")
@ -1024,6 +1140,21 @@ namespace Data.Migrations.MySql
.IsRequired();
});
modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Client.EFClientKill", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Attacker")
@ -1103,6 +1234,10 @@ namespace Data.Migrations.MySql
.HasForeignKey("LastStrainAngleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
});
modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b =>
@ -1264,6 +1399,40 @@ namespace Data.Migrations.MySql
.IsRequired();
});
modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b =>
{
b.HasOne("Data.Models.Client.EFClient", "DestinationClient")
.WithMany()
.HasForeignKey("DestinationClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
b.HasOne("Data.Models.Client.EFClient", "SourceClient")
.WithMany()
.HasForeignKey("SourceClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b =>
{
b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map")
.WithMany()
.HasForeignKey("MapId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b =>
{
b.HasOne("Data.Models.Server.EFServer", "Server")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Postgresql
{
public partial class AddWeaponReferenceToEFClientKill : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "WeaponReference",
table: "EFClientKills",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "WeaponReference",
table: "EFClientKills");
}
}
}

View File

@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Postgresql
{
public partial class AddWeaponReferenceAndServerIdToEFACSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "ServerId",
table: "EFACSnapshot",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "WeaponReference",
table: "EFACSnapshot",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_EFACSnapshot_ServerId",
table: "EFACSnapshot",
column: "ServerId");
migrationBuilder.AddForeignKey(
name: "FK_EFACSnapshot_EFServers_ServerId",
table: "EFACSnapshot",
column: "ServerId",
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Restrict);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_EFACSnapshot_EFServers_ServerId",
table: "EFACSnapshot");
migrationBuilder.DropIndex(
name: "IX_EFACSnapshot_ServerId",
table: "EFACSnapshot");
migrationBuilder.DropColumn(
name: "ServerId",
table: "EFACSnapshot");
migrationBuilder.DropColumn(
name: "WeaponReference",
table: "EFACSnapshot");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Postgresql
{
public partial class AddHitLocationReferenceToEFACSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "HitLocationReference",
table: "EFACSnapshot",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HitLocationReference",
table: "EFACSnapshot");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Data.Migrations.Postgresql
{
public partial class AddEFInboxMessage : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "InboxMessages",
columns: table => new
{
InboxMessageId = table.Column<int>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn),
CreatedDateTime = table.Column<DateTime>(nullable: false),
UpdatedDateTime = table.Column<DateTime>(nullable: true),
SourceClientId = table.Column<int>(nullable: false),
DestinationClientId = table.Column<int>(nullable: false),
ServerId = table.Column<long>(nullable: true),
Message = table.Column<string>(nullable: true),
IsDelivered = table.Column<bool>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_InboxMessages", x => x.InboxMessageId);
table.ForeignKey(
name: "FK_InboxMessages_EFClients_DestinationClientId",
column: x => x.DestinationClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_InboxMessages_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_InboxMessages_EFClients_SourceClientId",
column: x => x.SourceClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_InboxMessages_DestinationClientId",
table: "InboxMessages",
column: "DestinationClientId");
migrationBuilder.CreateIndex(
name: "IX_InboxMessages_ServerId",
table: "InboxMessages",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_InboxMessages_SourceClientId",
table: "InboxMessages",
column: "SourceClientId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "InboxMessages");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Data.Migrations.Postgresql
{
public partial class AddEFServerSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EFServerSnapshot",
columns: table => new
{
ServerSnapshotId = table.Column<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn),
Active = table.Column<bool>(nullable: false),
CapturedAt = table.Column<DateTime>(nullable: false),
PeriodBlock = table.Column<int>(nullable: false),
ServerId = table.Column<long>(nullable: false),
MapId = table.Column<int>(nullable: false),
ClientCount = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFServerSnapshot", x => x.ServerSnapshotId);
table.ForeignKey(
name: "FK_EFServerSnapshot_EFMaps_MapId",
column: x => x.MapId,
principalTable: "EFMaps",
principalColumn: "MapId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFServerSnapshot_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_MapId",
table: "EFServerSnapshot",
column: "MapId");
migrationBuilder.CreateIndex(
name: "IX_EFServerSnapshot_ServerId",
table: "EFServerSnapshot",
column: "ServerId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EFServerSnapshot");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Data.Migrations.Postgresql
{
public partial class AddEFClientConnectionHistory : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EFClientConnectionHistory",
columns: table => new
{
ClientConnectionId = table.Column<long>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn),
CreatedDateTime = table.Column<DateTime>(nullable: false),
UpdatedDateTime = table.Column<DateTime>(nullable: true),
ClientId = table.Column<int>(nullable: false),
ServerId = table.Column<long>(nullable: false),
ConnectionType = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFClientConnectionHistory", x => x.ClientConnectionId);
table.ForeignKey(
name: "FK_EFClientConnectionHistory_EFClients_ClientId",
column: x => x.ClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFClientConnectionHistory_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_EFClientConnectionHistory_ClientId",
table: "EFClientConnectionHistory",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_EFClientConnectionHistory_CreatedDateTime",
table: "EFClientConnectionHistory",
column: "CreatedDateTime");
migrationBuilder.CreateIndex(
name: "IX_EFClientConnectionHistory_ServerId",
table: "EFClientConnectionHistory",
column: "ServerId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EFClientConnectionHistory");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Postgresql
{
public partial class RemoveUniqueAliasIndexConstraint : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFAlias_Name_IPAddress",
table: "EFAlias");
migrationBuilder.CreateIndex(
name: "IX_EFAlias_Name_IPAddress",
table: "EFAlias",
columns: new[] { "Name", "IPAddress" });
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFAlias_Name_IPAddress",
table: "EFAlias");
migrationBuilder.CreateIndex(
name: "IX_EFAlias_Name_IPAddress",
table: "EFAlias",
columns: new[] { "Name", "IPAddress" },
unique: true);
}
}
}

View File

@ -99,6 +99,39 @@ namespace Data.Migrations.Postgresql
b.ToTable("EFClients");
});
modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b =>
{
b.Property<long>("ClientConnectionId")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn);
b.Property<int>("ClientId")
.HasColumnType("integer");
b.Property<int>("ConnectionType")
.HasColumnType("integer");
b.Property<DateTime>("CreatedDateTime")
.HasColumnType("timestamp without time zone");
b.Property<long>("ServerId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedDateTime")
.HasColumnType("timestamp without time zone");
b.HasKey("ClientConnectionId");
b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("ServerId");
b.ToTable("EFClientConnectionHistory");
});
modelBuilder.Entity("Data.Models.Client.EFClientKill", b =>
{
b.Property<long>("KillId")
@ -151,6 +184,9 @@ namespace Data.Migrations.Postgresql
b.Property<int>("Weapon")
.HasColumnType("integer");
b.Property<string>("WeaponReference")
.HasColumnType("text");
b.Property<DateTime>("When")
.HasColumnType("timestamp without time zone");
@ -244,6 +280,9 @@ namespace Data.Migrations.Postgresql
b.Property<int>("HitLocation")
.HasColumnType("integer");
b.Property<string>("HitLocationReference")
.HasColumnType("text");
b.Property<int>("HitOriginId")
.HasColumnType("integer");
@ -262,6 +301,9 @@ namespace Data.Migrations.Postgresql
b.Property<double>("RecoilOffset")
.HasColumnType("double precision");
b.Property<long?>("ServerId")
.HasColumnType("bigint");
b.Property<double>("SessionAngleOffset")
.HasColumnType("double precision");
@ -286,6 +328,9 @@ namespace Data.Migrations.Postgresql
b.Property<int>("WeaponId")
.HasColumnType("integer");
b.Property<string>("WeaponReference")
.HasColumnType("text");
b.Property<DateTime>("When")
.HasColumnType("timestamp without time zone");
@ -301,6 +346,8 @@ namespace Data.Migrations.Postgresql
b.HasIndex("LastStrainAngleId");
b.HasIndex("ServerId");
b.ToTable("EFACSnapshot");
});
@ -789,8 +836,7 @@ namespace Data.Migrations.Postgresql
b.HasIndex("SearchableName");
b.HasIndex("Name", "IPAddress")
.IsUnique();
b.HasIndex("Name", "IPAddress");
b.ToTable("EFAlias");
});
@ -944,6 +990,45 @@ namespace Data.Migrations.Postgresql
b.ToTable("EFPenalties");
});
modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b =>
{
b.Property<int>("InboxMessageId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn);
b.Property<DateTime>("CreatedDateTime")
.HasColumnType("timestamp without time zone");
b.Property<int>("DestinationClientId")
.HasColumnType("integer");
b.Property<bool>("IsDelivered")
.HasColumnType("boolean");
b.Property<string>("Message")
.HasColumnType("text");
b.Property<long?>("ServerId")
.HasColumnType("bigint");
b.Property<int>("SourceClientId")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedDateTime")
.HasColumnType("timestamp without time zone");
b.HasKey("InboxMessageId");
b.HasIndex("DestinationClientId");
b.HasIndex("ServerId");
b.HasIndex("SourceClientId");
b.ToTable("InboxMessages");
});
modelBuilder.Entity("Data.Models.Server.EFServer", b =>
{
b.Property<long>("ServerId")
@ -972,6 +1057,40 @@ namespace Data.Migrations.Postgresql
b.ToTable("EFServers");
});
modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b =>
{
b.Property<long>("ServerSnapshotId")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn);
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CapturedAt")
.HasColumnType("timestamp without time zone");
b.Property<int>("ClientCount")
.HasColumnType("integer");
b.Property<int>("MapId")
.HasColumnType("integer");
b.Property<int>("PeriodBlock")
.HasColumnType("integer");
b.Property<long>("ServerId")
.HasColumnType("bigint");
b.HasKey("ServerSnapshotId");
b.HasIndex("MapId");
b.HasIndex("ServerId");
b.ToTable("EFServerSnapshot");
});
modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b =>
{
b.Property<int>("StatisticId")
@ -1049,6 +1168,21 @@ namespace Data.Migrations.Postgresql
.IsRequired();
});
modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Client.EFClientKill", b =>
{
b.HasOne("Data.Models.Client.EFClient", "Attacker")
@ -1128,6 +1262,10 @@ namespace Data.Migrations.Postgresql
.HasForeignKey("LastStrainAngleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
});
modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b =>
@ -1289,6 +1427,40 @@ namespace Data.Migrations.Postgresql
.IsRequired();
});
modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b =>
{
b.HasOne("Data.Models.Client.EFClient", "DestinationClient")
.WithMany()
.HasForeignKey("DestinationClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
b.HasOne("Data.Models.Client.EFClient", "SourceClient")
.WithMany()
.HasForeignKey("SourceClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b =>
{
b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map")
.WithMany()
.HasForeignKey("MapId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b =>
{
b.HasOne("Data.Models.Server.EFServer", "Server")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Sqlite
{
public partial class AddWeaponReferenceToEFClientKill : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "WeaponReference",
table: "EFClientKills",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "WeaponReference",
table: "EFClientKills");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,162 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Sqlite
{
public partial class AddWeaponReferenceAndServerIdToEFACSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"PRAGMA foreign_keys = 0;
CREATE TABLE sqlitestudio_temp_table AS SELECT *
FROM EFACSnapshot;
DROP TABLE EFACSnapshot;
CREATE TABLE EFACSnapshot (
Active INTEGER NOT NULL,
TimeSinceLastEvent INTEGER NOT NULL,
SnapshotId INTEGER NOT NULL
CONSTRAINT PK_EFACSnapshot PRIMARY KEY AUTOINCREMENT,
ClientId INTEGER NOT NULL,
ServerId INTEGER CONSTRAINT FK_EFACSnapshot_EFServers_ServerId REFERENCES EFServers (ServerId) ON DELETE RESTRICT,
[When] TEXT NOT NULL,
CurrentSessionLength INTEGER NOT NULL,
EloRating REAL NOT NULL,
SessionScore INTEGER NOT NULL,
SessionSPM REAL NOT NULL,
Hits INTEGER NOT NULL,
Kills INTEGER NOT NULL,
Deaths INTEGER NOT NULL,
CurrentStrain REAL NOT NULL,
StrainAngleBetween REAL NOT NULL,
SessionAngleOffset REAL NOT NULL,
LastStrainAngleId INTEGER NOT NULL,
HitOriginId INTEGER NOT NULL,
HitDestinationId INTEGER NOT NULL,
Distance REAL NOT NULL,
CurrentViewAngleId INTEGER,
WeaponId INTEGER NOT NULL,
WeaponReference TEXT,
HitLocation INTEGER NOT NULL,
HitType INTEGER NOT NULL,
RecoilOffset REAL NOT NULL
DEFAULT 0.0,
SessionAverageSnapValue REAL NOT NULL
DEFAULT 0.0,
SessionSnapHits INTEGER NOT NULL
DEFAULT 0,
CONSTRAINT FK_EFACSnapshot_EFClients_ClientId FOREIGN KEY (
ClientId
)
REFERENCES EFClients (ClientId) ON DELETE CASCADE,
CONSTRAINT FK_EFACSnapshot_Vector3_CurrentViewAngleId FOREIGN KEY (
CurrentViewAngleId
)
REFERENCES Vector3 (Vector3Id) ON DELETE RESTRICT,
CONSTRAINT FK_EFACSnapshot_Vector3_HitDestinationId FOREIGN KEY (
HitDestinationId
)
REFERENCES Vector3 (Vector3Id) ON DELETE CASCADE,
CONSTRAINT FK_EFACSnapshot_Vector3_HitOriginId FOREIGN KEY (
HitOriginId
)
REFERENCES Vector3 (Vector3Id) ON DELETE CASCADE,
CONSTRAINT FK_EFACSnapshot_Vector3_LastStrainAngleId FOREIGN KEY (
LastStrainAngleId
)
REFERENCES Vector3 (Vector3Id) ON DELETE CASCADE
);
INSERT INTO EFACSnapshot (
Active,
TimeSinceLastEvent,
SnapshotId,
ClientId,
[When],
CurrentSessionLength,
EloRating,
SessionScore,
SessionSPM,
Hits,
Kills,
Deaths,
CurrentStrain,
StrainAngleBetween,
SessionAngleOffset,
LastStrainAngleId,
HitOriginId,
HitDestinationId,
Distance,
CurrentViewAngleId,
WeaponId,
HitLocation,
HitType,
RecoilOffset,
SessionAverageSnapValue,
SessionSnapHits
)
SELECT Active,
TimeSinceLastEvent,
SnapshotId,
ClientId,
""When"",
CurrentSessionLength,
EloRating,
SessionScore,
SessionSPM,
Hits,
Kills,
Deaths,
CurrentStrain,
StrainAngleBetween,
SessionAngleOffset,
LastStrainAngleId,
HitOriginId,
HitDestinationId,
Distance,
CurrentViewAngleId,
WeaponId,
HitLocation,
HitType,
RecoilOffset,
SessionAverageSnapValue,
SessionSnapHits
FROM sqlitestudio_temp_table;
DROP TABLE sqlitestudio_temp_table;
CREATE INDEX IX_EFACSnapshot_ClientId ON EFACSnapshot (
""ClientId""
);
CREATE INDEX IX_EFACSnapshot_CurrentViewAngleId ON EFACSnapshot (
""CurrentViewAngleId""
);
CREATE INDEX IX_EFACSnapshot_HitDestinationId ON EFACSnapshot (
""HitDestinationId""
);
CREATE INDEX IX_EFACSnapshot_HitOriginId ON EFACSnapshot (
""HitOriginId""
);
CREATE INDEX IX_EFACSnapshot_LastStrainAngleId ON EFACSnapshot (
""LastStrainAngleId""
);
CREATE INDEX IX_EFACSnapshot_ServerId ON EFACSnapshot (
""_ServerId""
);
PRAGMA foreign_keys = 1;
");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

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