Compare commits

...

218 Commits

Author SHA1 Message Date
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
e2ed57f674 prevent autoflag from running player has been manually unflagged 2021-03-23 10:34:44 -05:00
824b1c0990 prevent loading of privileged clients page for issue #188 2021-03-23 10:28:17 -05:00
a8b331a5e5 prevent missing config from causing stats error
small advanced stats fixes
2021-03-23 10:16:27 -05:00
802ec8cea5 Added iw6 aliases (#184) 2021-03-23 08:14:07 -05:00
2313c4357b add removal of obsolete plugins 2021-03-22 11:46:32 -05:00
c5375b661b huge commit for advanced stats feature.
broke data out into its own library.
may be breaking changes with existing plugins
2021-03-22 11:09:25 -05:00
db2e1deb2f modify rule shortcut to just have 1 list 2021-02-27 09:40:25 -06:00
191a68e7dd revert unintended commit file 2021-01-24 13:30:22 -06:00
c4f19e94ef implement custom tag (descriptor) feature
allow override of level names through configuration
few small fixes/improvements
2021-01-24 11:47:19 -06:00
2512b9f251 Added iw6 aliases (#184) 2021-01-20 12:43:44 -06:00
c419d80b57 preemptive checks 2021-01-17 22:12:18 -06:00
23a33ba489 implement more robust command api and login
improve web console command response reliability and consistency
2021-01-17 21:58:18 -06:00
dd3ebf6b34 increase buffer size for rcon connection 2021-01-17 20:04:32 -06:00
28373b9325 implement admin "privacy" for issue #185 2021-01-09 12:37:20 -06:00
843c01061d update 'uptime' output
use translations for certain webfront page meta that was neglected
update plutonium parsers to not use new line in notices as it is not supported
2021-01-08 19:21:23 -06:00
5cb2d05f33 add preset rules, configurable time spans, and separate rule shortcut for issue #180 2020-12-31 18:48:58 -06:00
5a288dafc1 update shared library core version and plugins 2020-12-20 19:23:14 -06:00
4afc478076 fix issue with view stats and reset stats failing
fix issue with set level returning wrong error message if setting a client to the same level they're currently at
update CoD4x parser version
update nuget packages
2020-12-16 13:11:30 -06:00
928cbef845 resolve bot guid issue with T5
remove unneeded check for CNCT state
2020-12-14 21:10:50 -06:00
02b910234a add official T4/WaW support for issue #178
CoD4x parser tweak to parse full guid as decimal
2020-12-13 20:33:37 -06:00
f03626c3ae Another tweak for CoD4x rcon parsing. 2020-12-12 21:43:27 -06:00
6648b75255 update CoD4x parser
tweak handling segmented status response
actually support more than 18 clients LOL
2020-12-02 14:29:49 -06:00
bd3f0caf60 fix memory leak issue related to AddDbContext not working as expected 2020-11-29 16:01:52 -06:00
b2d282d412 include ; for timeout string 2020-11-27 22:08:13 -06:00
36a02b3d7b update for database provider specific migrations
fix issues with live radar
2020-11-27 21:52:52 -06:00
8ef2959f63 make notice line separator configurable for different parsers
(updated tekno's as it doesn't support \n)
2020-11-19 20:48:25 -06:00
d58b24b5b2 add shortcut for rules in penalty reasons for issue #159 2020-11-18 18:48:51 -06:00
09f37d7941 clean up some logic related to tracking stats on player join 2020-11-18 16:28:14 -06:00
103d2726c2 persist say command messages with webfront denotation to chat log
per issue #159
2020-11-18 09:08:24 -06:00
941d9cea73 more consistent/enhanced game penalty messages per issue #171 2020-11-17 18:24:54 -06:00
a574fb0d4b update index for ratings/prune old entries
small stat tweaks to add players on first kill/damage event
(instead of on connect which causes issues with slow writes)
2020-11-14 18:24:51 -06:00
664eb32587 fix small logging issue with loading plugins
add minigun turret to list of ignored ac weapons
2020-11-14 10:53:01 -06:00
799 changed files with 137505 additions and 8299 deletions

5
.gitignore vendored
View File

@ -244,4 +244,7 @@ launchSettings.json
/Tests/ApplicationTests/Files/GameEvents.json /Tests/ApplicationTests/Files/GameEvents.json
/Tests/ApplicationTests/Files/replay.json /Tests/ApplicationTests/Files/replay.json
/GameLogServer/game_log_server_env /GameLogServer/game_log_server_env
.idea/* .idea/*
*.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; } public int Uptime { get; set; }
/// <summary> /// <summary>
/// Specifices the version of the instance /// Specifies the version of the instance
/// </summary> /// </summary>
[JsonProperty("version")] [JsonProperty("version")]
[JsonConverter(typeof(BuildNumberJsonConverter))] [JsonConverter(typeof(BuildNumberJsonConverter))]
@ -33,5 +33,11 @@ namespace IW4MAdmin.Application.API.Master
/// </summary> /// </summary>
[JsonProperty("servers")] [JsonProperty("servers")]
public List<ApiServer> Servers { get; set; } 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> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish> <MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId> <PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2020.0.0.0</Version> <Version>2020.0.0.0</Version>
@ -25,13 +25,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-1632" /> <PackageReference Include="Jint" Version="3.0.0-beta-1632" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="RestEase" Version="1.5.0" /> <PackageReference Include="RestEase" Version="1.5.5" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@ -39,7 +39,6 @@
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection> <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation> <TieredCompilation>true</TieredCompilation>
<LangVersion>Latest</LangVersion> <LangVersion>Latest</LangVersion>
<StartupObject></StartupObject>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
@ -49,6 +48,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Integrations\Cod\Integrations.Cod.csproj" />
<ProjectReference Include="..\Integrations\Source\Integrations.Source.csproj" />
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj"> <ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>true</Private> <Private>true</Private>
</ProjectReference> </ProjectReference>
@ -60,7 +61,7 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="Configuration\LoggingConfiguration.json"> <None Update="Configuration\LoggingConfiguration.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>

View File

@ -1,12 +1,11 @@
using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RconParsers; using IW4MAdmin.Application.RConParsers;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Commands; using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Configuration.Validation; using SharedLibraryCore.Configuration.Validation;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
@ -16,14 +15,19 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Data.Context;
using IW4MAdmin.Application.Migration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Serilog.Context; using Serilog.Context;
using SharedLibraryCore.Formatting;
using static SharedLibraryCore.GameEvent; using static SharedLibraryCore.GameEvent;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger; using ObsoleteLogger = SharedLibraryCore.Interfaces.ILogger;
@ -52,7 +56,6 @@ namespace IW4MAdmin.Application
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly List<MessageToken> MessageTokens; private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc; private readonly ClientService ClientSvc;
readonly AliasService AliasSvc;
readonly PenaltyService PenaltySvc; readonly PenaltyService PenaltySvc;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList; readonly IPageList PageList;
@ -72,6 +75,7 @@ namespace IW4MAdmin.Application
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly ChangeHistoryService _changeHistoryService; private readonly ChangeHistoryService _changeHistoryService;
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
public ConcurrentDictionary<long, GameEvent> ProcessingEvents { get; } = new ConcurrentDictionary<long, GameEvent>();
public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands, public ApplicationManager(ILogger<ApplicationManager> logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration, ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
@ -79,14 +83,13 @@ namespace IW4MAdmin.Application
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider, IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig) ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>(); MessageTokens = new List<MessageToken>();
ClientSvc = clientService; ClientSvc = clientService;
AliasSvc = new AliasService(); PenaltySvc = penaltyService;
PenaltySvc = new PenaltyService();
ConfigHandler = appConfigHandler; ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow; StartTime = DateTime.UtcNow;
PageList = new PageList(); PageList = new PageList();
@ -116,6 +119,8 @@ namespace IW4MAdmin.Application
public async Task ExecuteEvent(GameEvent newEvent) public async Task ExecuteEvent(GameEvent newEvent)
{ {
ProcessingEvents.TryAdd(newEvent.Id, newEvent);
// the event has failed already // the event has failed already
if (newEvent.Failed) if (newEvent.Failed)
{ {
@ -176,6 +181,29 @@ namespace IW4MAdmin.Application
} }
skip: skip:
if (newEvent.Type == EventType.Command && newEvent.ImpersonationOrigin == null)
{
var correlatedEvents =
ProcessingEvents.Values.Where(ev =>
ev.CorrelationId == newEvent.CorrelationId && ev.Id != newEvent.Id)
.ToList();
await Task.WhenAll(correlatedEvents.Select(ev =>
ev.WaitAsync(Utilities.DefaultCommandTimeout, CancellationToken)));
newEvent.Output.AddRange(correlatedEvents.SelectMany(ev => ev.Output));
foreach (var correlatedEvent in correlatedEvents)
{
ProcessingEvents.Remove(correlatedEvent.Id, out _);
}
}
// we don't want to remove events that are correlated to command
if (ProcessingEvents.Values.ToList()?.Count(gameEvent => gameEvent.CorrelationId == newEvent.CorrelationId) == 1)
{
ProcessingEvents.Remove(newEvent.Id, out _);
}
// tell anyone waiting for the output that we're done // tell anyone waiting for the output that we're done
newEvent.Complete(); newEvent.Complete();
OnGameEventExecuted?.Invoke(this, newEvent); OnGameEventExecuted?.Invoke(this, newEvent);
@ -191,18 +219,20 @@ namespace IW4MAdmin.Application
return _commands; return _commands;
} }
public IReadOnlyList<IManagerCommand> Commands => _commands.ToImmutableList();
public async Task UpdateServerStates() public async Task UpdateServerStates()
{ {
// store the server hash code and task for it // store the server hash code and task for it
var runningUpdateTasks = new Dictionary<long, Task>(); var runningUpdateTasks = new Dictionary<long, (Task task, CancellationTokenSource tokenSource, DateTime startTime)>();
while (!_tokenSource.IsCancellationRequested) while (!_tokenSource.IsCancellationRequested)
{ {
// select the server ids that have completed the update task // select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks var serverTasksToRemove = runningUpdateTasks
.Where(ut => ut.Value.Status == TaskStatus.RanToCompletion || .Where(ut => ut.Value.task.Status == TaskStatus.RanToCompletion ||
ut.Value.Status == TaskStatus.Canceled || ut.Value.task.Status == TaskStatus.Canceled || // we want to cancel if a task takes longer than 5 minutes
ut.Value.Status == TaskStatus.Faulted) ut.Value.task.Status == TaskStatus.Faulted || DateTime.Now - ut.Value.startTime > TimeSpan.FromMinutes(5))
.Select(ut => ut.Key) .Select(ut => ut.Key)
.ToList(); .ToList();
@ -213,9 +243,14 @@ namespace IW4MAdmin.Application
IsInitialized = true; IsInitialized = true;
} }
// remove the update tasks as they have completd // remove the update tasks as they have completed
foreach (long serverId in serverTasksToRemove) foreach (var serverId in serverTasksToRemove.Where(serverId => runningUpdateTasks.ContainsKey(serverId)))
{ {
if (!runningUpdateTasks[serverId].tokenSource.Token.IsCancellationRequested)
{
runningUpdateTasks[serverId].tokenSource.Cancel();
}
runningUpdateTasks.Remove(serverId); runningUpdateTasks.Remove(serverId);
} }
@ -223,11 +258,16 @@ namespace IW4MAdmin.Application
var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList(); var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint))) foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
{ {
runningUpdateTasks.Add(server.EndPoint, Task.Run(async () => var tokenSource = new CancellationTokenSource();
runningUpdateTasks.Add(server.EndPoint, (Task.Run(async () =>
{ {
try try
{ {
await server.ProcessUpdatesAsync(_tokenSource.Token); if (runningUpdateTasks.ContainsKey(server.EndPoint))
{
await server.ProcessUpdatesAsync(_tokenSource.Token)
.WithWaitCancellation(runningUpdateTasks[server.EndPoint].tokenSource.Token);
}
} }
catch (Exception e) catch (Exception e)
@ -242,7 +282,7 @@ namespace IW4MAdmin.Application
{ {
server.IsInitialized = true; server.IsInitialized = true;
} }
})); }, tokenSource.Token), tokenSource, DateTime.Now));
} }
try try
@ -266,6 +306,15 @@ namespace IW4MAdmin.Application
IsRunning = true; IsRunning = true;
ExternalIPAddress = await Utilities.GetExternalIP(); ExternalIPAddress = await Utilities.GetExternalIP();
#region DATABASE
_logger.LogInformation("Beginning database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_START"]);
await ContextSeed.Seed(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
await DatabaseHousekeeping.RemoveOldRatings(_serviceProvider.GetRequiredService<IDatabaseContextFactory>(), _tokenSource.Token);
_logger.LogInformation("Finished database migration sync");
Console.WriteLine(_translationLookup["MANAGER_MIGRATION_END"]);
#endregion
#region PLUGINS #region PLUGINS
foreach (var plugin in Plugins) foreach (var plugin in Plugins)
{ {
@ -306,15 +355,13 @@ namespace IW4MAdmin.Application
// copy over default config if it doesn't exist // copy over default config if it doesn't exist
if (!_appConfig.Servers?.Any() ?? true) if (!_appConfig.Servers?.Any() ?? true)
{ {
var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration(); var defaultHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
//ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate()); await defaultHandler.BuildAsync();
//var newConfig = ConfigHandler.Configuration(); var defaultConfig = defaultHandler.Configuration();
_appConfig.AutoMessages = defaultConfig.AutoMessages; _appConfig.AutoMessages = defaultConfig.AutoMessages;
_appConfig.GlobalRules = defaultConfig.GlobalRules; _appConfig.GlobalRules = defaultConfig.GlobalRules;
_appConfig.Maps = defaultConfig.Maps;
_appConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames; _appConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
_appConfig.QuickMessages = defaultConfig.QuickMessages;
//if (newConfig.Servers == null) //if (newConfig.Servers == null)
{ {
@ -355,6 +402,18 @@ namespace IW4MAdmin.Application
await ConfigHandler.Save(); 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 validator = new ApplicationConfigurationValidator();
var validationResult = validator.Validate(_appConfig); var validationResult = validator.Validate(_appConfig);
@ -369,7 +428,7 @@ namespace IW4MAdmin.Application
foreach (var serverConfig in _appConfig.Servers) foreach (var serverConfig in _appConfig.Servers)
{ {
Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig); ConfigurationMigration.ModifyLogPath020919(serverConfig);
if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null) if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null)
{ {
@ -397,18 +456,21 @@ namespace IW4MAdmin.Application
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(_appConfig.CustomParserEncoding) ? _appConfig.CustomParserEncoding : "windows-1252"); Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(_appConfig.CustomParserEncoding) ? _appConfig.CustomParserEncoding : "windows-1252");
#endregion foreach (var parser in AdditionalRConParsers)
#region DATABASE
using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString,
GetApplicationSettings().Configuration()?.DatabaseProvider))
{ {
await new ContextSeed(db).Seed(); 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 #endregion
#region COMMANDS #region COMMANDS
if (ClientSvc.GetOwners().Result.Count > 0) if (await ClientSvc.HasOwnerAsync(_tokenSource.Token))
{ {
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand)); _commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
} }
@ -430,13 +492,17 @@ namespace IW4MAdmin.Application
// this is because I want to store the command prefix in IW4MAdminSettings, but can't easily // this is because I want to store the command prefix in IW4MAdminSettings, but can't easily
// inject it to all the places that need it // inject it to all the places that need it
cmdConfig.CommandPrefix = _appConfig.CommandPrefix; cmdConfig.CommandPrefix = _appConfig?.CommandPrefix ?? "!";
cmdConfig.BroadcastCommandPrefix = _appConfig.BroadcastCommandPrefix; cmdConfig.BroadcastCommandPrefix = _appConfig?.BroadcastCommandPrefix ?? "@";
foreach (var cmd in commandsToAddToConfig) foreach (var cmd in commandsToAddToConfig)
{ {
if (cmdConfig.Commands.ContainsKey(cmd.CommandConfigNameForType()))
{
continue;
}
cmdConfig.Commands.Add(cmd.CommandConfigNameForType(), cmdConfig.Commands.Add(cmd.CommandConfigNameForType(),
new CommandProperties() new CommandProperties
{ {
Name = cmd.Name, Name = cmd.Name,
Alias = cmd.Alias, Alias = cmd.Alias,
@ -485,13 +551,13 @@ namespace IW4MAdmin.Application
_servers.Add(ServerInstance); _servers.Add(ServerInstance);
Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname.StripColors())); 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 // add the start event for this server
var e = new GameEvent() var e = new GameEvent()
{ {
Type = GameEvent.EventType.Start, Type = EventType.Start,
Data = $"{ServerInstance.GameName} started", Data = $"{ServerInstance.GameName} started",
Owner = ServerInstance Owner = ServerInstance
}; };
@ -558,16 +624,16 @@ namespace IW4MAdmin.Application
return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList(); 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() public ClientService GetClientService()
{ {
return ClientSvc; return ClientSvc;
} }
public AliasService GetAliasService()
{
return AliasSvc;
}
public PenaltyService GetPenaltyService() public PenaltyService GetPenaltyService()
{ {
return PenaltySvc; return PenaltySvc;

View File

@ -13,4 +13,5 @@ if not exist "%TargetDir%Plugins" (
md "%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" if not exist "%PublishDir%\Configuration" md "%PublishDir%\Configuration"
move "%PublishDir%\DefaultSettings.json" "%PublishDir%\Configuration\" move "%PublishDir%\DefaultSettings.json" "%PublishDir%\Configuration\"
if not exist "%PublishDir%\Lib\" md "%PublishDir%\Lib\" if not exist "%PublishDir%\Lib\" md "%PublishDir%\Lib\"
del "%PublishDir%\Microsoft.CodeAnalysis*.dll" /F /Q
move "%PublishDir%\*.dll" "%PublishDir%\Lib\" move "%PublishDir%\*.dll" "%PublishDir%\Lib\"
move "%PublishDir%\*.json" "%PublishDir%\Lib\" move "%PublishDir%\*.json" "%PublishDir%\Lib\"
move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes" move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes"
@ -30,16 +31,37 @@ move "%PublishDir%\ru" "%PublishDir%\Lib\ru"
move "%PublishDir%\de" "%PublishDir%\Lib\de" move "%PublishDir%\de" "%PublishDir%\Lib\de"
move "%PublishDir%\pt" "%PublishDir%\Lib\pt" move "%PublishDir%\pt" "%PublishDir%\Lib\pt"
move "%PublishDir%\es" "%PublishDir%\Lib\es" 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" if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs"
echo making start scripts 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 @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 #!/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 echo moving front-end library dependencies
if not exist "%PublishDir%\wwwroot\font" mkdir "%PublishDir%\wwwroot\font" if not exist "%PublishDir%\wwwroot\font" mkdir "%PublishDir%\wwwroot\font"
move "WebfrontCore\wwwroot\lib\open-iconic\font\fonts\*.*" "%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 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... 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": [ "Using": [
"Serilog.Sinks.File" "Serilog.Sinks.File"
], ],
"MinimumLevel": "Information", "MinimumLevel": {
"Default": "Information",
"Override": {
"System": "Warning",
"Microsoft": "Warning"
}
},
"WriteTo": [ "WriteTo": [
{ {
"Name": "File", "Name": "File",
@ -12,6 +18,13 @@
"rollingInterval": "Day", "rollingInterval": "Day",
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Server} {Level:u3}] {Message:lj}{NewLine}{Exception}" "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": [ "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

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

View File

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

View File

@ -1,6 +1,10 @@
using IW4MAdmin.Application.Misc; using System;
using System.Collections.Generic;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Linq; using System.Linq;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Extensions namespace IW4MAdmin.Application.Extensions
{ {
@ -13,9 +17,19 @@ namespace IW4MAdmin.Application.Extensions
/// <returns></returns> /// <returns></returns>
public static string CommandConfigNameForType(this IManagerCommand command) public static string CommandConfigNameForType(this IManagerCommand command)
{ {
return command.GetType() == typeof(ScriptCommand) ? return command.GetType() == typeof(ScriptCommand)
$"{char.ToUpper(command.Name[0])}{command.Name.Substring(1)}Command" : ? $"{char.ToUpper(command.Name[0])}{command.Name.Substring(1)}Command"
command.GetType().Name; : 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

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

View File

@ -1,4 +1,5 @@
using IW4MAdmin.Application.Misc; using System.Threading.Tasks;
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
@ -17,7 +18,17 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns> /// <returns></returns>
public IConfigurationHandler<T> GetConfigurationHandler<T>(string name) where T : IBaseConfiguration 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

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

View File

@ -1,4 +1,7 @@
using System; using System;
using Data.Abstractions;
using Data.Models.Server;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -19,7 +22,7 @@ namespace IW4MAdmin.Application.Factories
/// </summary> /// </summary>
/// <param name="translationLookup"></param> /// <param name="translationLookup"></param>
/// <param name="rconConnectionFactory"></param> /// <param name="rconConnectionFactory"></param>
public GameServerInstanceFactory(ITranslationLookup translationLookup, public GameServerInstanceFactory(ITranslationLookup translationLookup,
IMetaService metaService, IMetaService metaService,
IServiceProvider serviceProvider) IServiceProvider serviceProvider)
{ {
@ -36,7 +39,10 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns> /// <returns></returns>
public Server CreateServer(ServerConfiguration config, IManager manager) public Server CreateServer(ServerConfiguration config, IManager manager)
{ {
return new IW4MServer(config, _translationLookup, _metaService, _serviceProvider); 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 SharedLibraryCore.Interfaces;
using System.Text; using System.Text;
using Integrations.Cod;
using Integrations.Source;
using Integrations.Source.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore.Configuration;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -10,28 +16,31 @@ namespace IW4MAdmin.Application.Factories
/// </summary> /// </summary>
internal class RConConnectionFactory : IRConConnectionFactory internal class RConConnectionFactory : IRConConnectionFactory
{ {
private static readonly Encoding gameEncoding = Encoding.GetEncoding("windows-1252"); private static readonly Encoding GameEncoding = Encoding.GetEncoding("windows-1252");
private readonly ILogger<RConConnection> _logger; private readonly IServiceProvider _serviceProvider;
/// <summary> /// <summary>
/// Base constructor /// Base constructor
/// </summary> /// </summary>
/// <param name="logger"></param> /// <param name="logger"></param>
public RConConnectionFactory(ILogger<RConConnection> logger) public RConConnectionFactory(IServiceProvider serviceProvider)
{ {
_logger = logger; _serviceProvider = serviceProvider;
} }
/// <summary> /// <inheritdoc/>
/// creates a new rcon connection instance public IRConConnection CreateConnection(IPEndPoint ipEndpoint, string password, string rconEngine)
/// </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)
{ {
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,9 +6,9 @@ using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Data.Models.Client;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Database.Models.EFClient;
namespace IW4MAdmin.Application.Factories namespace IW4MAdmin.Application.Factories
{ {
@ -32,7 +32,7 @@ namespace IW4MAdmin.Application.Factories
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, 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, Action<GameEvent> executeAction)
{ {
var permissionEnum = Enum.Parse<Permission>(permission); var permissionEnum = Enum.Parse<EFClient.Permission>(permission);
var argsArray = args.Select(_arg => new CommandArgument var argsArray = args.Select(_arg => new CommandArgument
{ {
Name = _arg.Item1, Name = _arg.Item1,

View File

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

View File

@ -11,14 +11,22 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Serilog.Context; using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient; 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 namespace IW4MAdmin
{ {
@ -29,25 +37,36 @@ namespace IW4MAdmin
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IMetaService _metaService; private readonly IMetaService _metaService;
private const int REPORT_FLAG_COUNT = 4; private const int REPORT_FLAG_COUNT = 4;
private int lastGameTime = 0; private long lastGameTime = 0;
public int Id { get; private set; } public int Id { get; private set; }
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IClientNoticeMessageFormatter _messageFormatter;
private readonly ILookupCache<EFServer> _serverCache;
private readonly CommandConfiguration _commandConfiguration;
public IW4MServer( public IW4MServer(
ServerConfiguration serverConfiguration, ServerConfiguration serverConfiguration,
CommandConfiguration commandConfiguration,
ITranslationLookup lookup, ITranslationLookup lookup,
IMetaService metaService, IMetaService metaService,
IServiceProvider serviceProvider) : base(serviceProvider.GetRequiredService<ILogger<Server>>(), IServiceProvider serviceProvider,
IClientNoticeMessageFormatter messageFormatter,
ILookupCache<EFServer> serverCache) : base(serviceProvider.GetRequiredService<ILogger<Server>>(),
#pragma warning disable CS0612
serviceProvider.GetRequiredService<SharedLibraryCore.Interfaces.ILogger>(), serviceProvider.GetRequiredService<SharedLibraryCore.Interfaces.ILogger>(),
#pragma warning restore CS0612
serverConfiguration, serverConfiguration,
serviceProvider.GetRequiredService<IManager>(), serviceProvider.GetRequiredService<IManager>(),
serviceProvider.GetRequiredService<IRConConnectionFactory>(), serviceProvider.GetRequiredService<IRConConnectionFactory>(),
serviceProvider.GetRequiredService<IGameLogReaderFactory>()) serviceProvider.GetRequiredService<IGameLogReaderFactory>(), serviceProvider)
{ {
_translationLookup = lookup; _translationLookup = lookup;
_metaService = metaService; _metaService = metaService;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_messageFormatter = messageFormatter;
_serverCache = serverCache;
_commandConfiguration = commandConfiguration;
} }
public override async Task<EFClient> OnClientConnected(EFClient clientFromLog) public override async Task<EFClient> OnClientConnected(EFClient clientFromLog)
@ -64,6 +83,8 @@ namespace IW4MAdmin
client = await Manager.GetClientService().Create(clientFromLog); client = await Manager.GetClientService().Create(clientFromLog);
} }
client.CopyAdditionalProperties(clientFromLog);
// this is only a temporary version until the IPAddress is transmitted // this is only a temporary version until the IPAddress is transmitted
client.CurrentAlias = new EFAlias() client.CurrentAlias = new EFAlias()
{ {
@ -146,12 +167,13 @@ namespace IW4MAdmin
{ {
try 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) catch (CommandException e)
{ {
ServerLogger.LogWarning(e, "Error validating command from event {@event}", E); ServerLogger.LogWarning(e, "Error validating command from event {@event}",
new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId });
E.FailReason = GameEvent.EventFailReason.Invalid; E.FailReason = GameEvent.EventFailReason.Invalid;
} }
@ -194,6 +216,7 @@ namespace IW4MAdmin
catch (Exception e) catch (Exception e)
{ {
ServerLogger.LogError(e, "Unexpected exception occurred processing event");
if (E.Origin != null && E.Type == GameEvent.EventType.Command) if (E.Origin != null && E.Type == GameEvent.EventType.Command)
{ {
E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]); E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
@ -223,11 +246,11 @@ namespace IW4MAdmin
try try
{ {
await (plugin.OnEventAsync(gameEvent, this)).WithWaitCancellation(tokenSource.Token); await plugin.OnEventAsync(gameEvent, this).WithWaitCancellation(tokenSource.Token);
} }
catch (Exception ex) 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}", ServerLogger.LogError(ex, "Could not execute {methodName} for plugin {plugin}",
nameof(plugin.OnEventAsync), plugin.Name); nameof(plugin.OnEventAsync), plugin.Name);
} }
@ -245,6 +268,28 @@ namespace IW4MAdmin
{ {
ServerLogger.LogDebug("processing event of type {type}", E.Type); ServerLogger.LogDebug("processing event of type {type}", E.Type);
if (E.Type == GameEvent.EventType.Start)
{
var existingServer = (await _serverCache
.FirstAsync(server => server.Id == EndPoint));
var serverId = await GetIdForServer(E.Owner);
if (existingServer == null)
{
var server = new EFServer()
{
Port = Port,
EndPoint = ToString(),
ServerId = serverId,
GameName = (Reference.Game?)GameName,
HostName = Hostname
};
await _serverCache.AddAsync(server);
}
}
if (E.Type == GameEvent.EventType.ConnectionLost) if (E.Type == GameEvent.EventType.ConnectionLost)
{ {
var exception = E.Extra as Exception; var exception = E.Extra as Exception;
@ -269,6 +314,11 @@ namespace IW4MAdmin
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]")); Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
} }
if (!string.IsNullOrEmpty(CustomSayName))
{
await this.SetDvarAsync("sv_sayname", CustomSayName);
}
Throttled = false; Throttled = false;
} }
@ -295,7 +345,33 @@ namespace IW4MAdmin
Time = DateTime.UtcNow Time = DateTime.UtcNow
}); });
await E.Origin.OnJoin(E.Origin.IPAddress); var clientTag = await _metaService.GetPersistentMeta(EFMeta.ClientTag, E.Origin);
if (clientTag?.LinkedMeta != null)
{
E.Origin.Tag = clientTag.LinkedMeta.Value;
}
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);
} }
} }
@ -330,7 +406,7 @@ namespace IW4MAdmin
// possible a connect/reconnect game event before we get to process it here // possible a connect/reconnect game event before we get to process it here
// it appears that new games decide to switch client slots between maps (even if the clients aren't disconnecting) // it appears that new games decide to switch client slots between maps (even if the clients aren't disconnecting)
// bots can have duplicate names which causes conflicting GUIDs // bots can have duplicate names which causes conflicting GUIDs
else if (existingClient != null && existingClient.ClientNumber != E.Origin.ClientNumber && if (existingClient != null && existingClient.ClientNumber != E.Origin.ClientNumber &&
!E.Origin.IsBot) !E.Origin.IsBot)
{ {
ServerLogger.LogWarning( ServerLogger.LogWarning(
@ -361,7 +437,7 @@ namespace IW4MAdmin
if (E.Origin.Level > Permission.Moderator) 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));
} }
} }
@ -395,7 +471,7 @@ namespace IW4MAdmin
Link = E.Target.AliasLink Link = E.Target.AliasLink
}; };
var addedPenalty = await Manager.GetPenaltyService().Create(newPenalty); await Manager.GetPenaltyService().Create(newPenalty);
E.Target.SetLevel(Permission.Flagged, E.Origin); E.Target.SetLevel(Permission.Flagged, E.Origin);
} }
@ -440,10 +516,10 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().Create(newReport); await Manager.GetPenaltyService().Create(newReport);
int reportNum = await Manager.GetClientService().GetClientReportCount(E.Target.ClientId); var reportNum = await Manager.GetClientService().GetClientReportCount(E.Target.ClientId);
bool isAutoFlagged = await Manager.GetClientService().IsAutoFlagged(E.Target.ClientId); var canBeAutoFlagged = await Manager.GetClientService().CanBeAutoFlagged(E.Target.ClientId);
if (!E.Target.IsPrivileged() && reportNum >= REPORT_FLAG_COUNT && !isAutoFlagged) if (!E.Target.IsPrivileged() && reportNum >= REPORT_FLAG_COUNT && canBeAutoFlagged)
{ {
E.Target.Flag( E.Target.Flag(
Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"] Utilities.CurrentLocalization.LocalizationIndex["SERVER_AUTO_FLAG_REPORT"]
@ -454,7 +530,6 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.TempBan) else if (E.Type == GameEvent.EventType.TempBan)
{ {
await TempBan(E.Data, (TimeSpan) E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin); await TempBan(E.Data, (TimeSpan) E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin);
;
} }
else if (E.Type == GameEvent.EventType.Ban) else if (E.Type == GameEvent.EventType.Ban)
@ -470,7 +545,7 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.Kick) else if (E.Type == GameEvent.EventType.Kick)
{ {
await Kick(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin); await Kick(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin, E.Extra as EFPenalty);
} }
else if (E.Type == GameEvent.EventType.Warn) else if (E.Type == GameEvent.EventType.Warn)
@ -548,7 +623,7 @@ namespace IW4MAdmin
{ {
try try
{ {
message = Manager.GetApplicationSettings().Configuration() message = _serviceProvider.GetRequiredService<DefaultSettings>()
.QuickMessages .QuickMessages
.First(_qm => _qm.Game == GameName) .First(_qm => _qm.Game == GameName)
.Messages[E.Data.Substring(1)]; .Messages[E.Data.Substring(1)];
@ -654,11 +729,11 @@ namespace IW4MAdmin
private async Task OnClientUpdate(EFClient origin) 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) 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; return;
} }
@ -672,7 +747,7 @@ namespace IW4MAdmin
{ {
try try
{ {
await client.OnJoin(origin.IPAddress); await client.OnJoin(origin.IPAddress, Manager.GetApplicationSettings().Configuration().EnableImplicitAccountLinking);
} }
catch (Exception e) catch (Exception e)
@ -684,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) client.Level == Permission.Banned)
{ {
ServerLogger.LogWarning("{client} state is Unknown (probably kicked), but they are still connected. trying to kick again...", origin.ToString()); ServerLogger.LogWarning("{Client} state is Unknown (probably kicked), but they are still connected. trying to kick again...", origin.ToString());
await client.CanConnect(client.IPAddress); await client.CanConnect(client.IPAddress, Manager.GetApplicationSettings().Configuration().EnableImplicitAccountLinking);
} }
} }
@ -699,11 +774,11 @@ namespace IW4MAdmin
/// array index 2 = updated clients /// array index 2 = updated clients
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
async Task<IList<EFClient>[]> PollPlayersAsync() async Task<List<EFClient>[]> PollPlayersAsync()
{ {
var currentClients = GetClientsAsList(); var currentClients = GetClientsAsList();
var statusResponse = (await this.GetStatusAsync()); var statusResponse = (await this.GetStatusAsync());
var polledClients = statusResponse.Item1.AsEnumerable(); var polledClients = statusResponse.Clients.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots) if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
{ {
@ -713,16 +788,39 @@ namespace IW4MAdmin
var connectingClients = polledClients.Except(currentClients); var connectingClients = polledClients.Except(currentClients);
var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients); var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients);
UpdateMap(statusResponse.Item2); UpdateMap(statusResponse.Map);
UpdateGametype(statusResponse.Item3); UpdateGametype(statusResponse.GameType);
UpdateHostname(statusResponse.Hostname);
UpdateMaxPlayers(statusResponse.MaxClients);
return new List<EFClient>[] return new []
{ {
connectingClients.ToList(), connectingClients.ToList(),
disconnectingClients.ToList(), disconnectingClients.ToList(),
updatedClients.ToList() updatedClients.ToList()
}; };
} }
public override async Task<long> GetIdForServer(Server server = null)
{
server ??= this;
if ($"{server.IP}:{server.Port.ToString()}" == "66.150.121.184:28965")
{
return 886229536;
}
// todo: this is not stable and will need to be migrated again...
long id = HashCode.Combine(server.IP, server.Port);
id = id < 0 ? Math.Abs(id) : id;
var serverId = (await _serverCache
.FirstAsync(_server => _server.ServerId == server.EndPoint ||
_server.EndPoint == server.ToString() ||
_server.ServerId == id))?.ServerId;
return !serverId.HasValue ? id : serverId.Value;
}
private void UpdateMap(string mapname) private void UpdateMap(string mapname)
{ {
@ -744,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() private async Task ShutdownInternal()
{ {
foreach (var client in GetClientsAsList()) foreach (var client in GetClientsAsList())
@ -883,10 +1011,13 @@ namespace IW4MAdmin
LastMessage = DateTime.Now - start; LastMessage = DateTime.Now - start;
lastCount = DateTime.Now; lastCount = DateTime.Now;
var appConfig = _serviceProvider.GetService<ApplicationConfiguration>();
// update the player history // 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(); ClientHistory.Dequeue();
} }
@ -943,27 +1074,45 @@ namespace IW4MAdmin
public async Task Initialize() 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 RconParser = Manager.AdditionalRConParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.RConParserVersion); .FirstOrDefault(parser =>
parser.Version == ServerConfig.RConParserVersion ||
parser.Name == ServerConfig.RConParserVersion);
EventParser = Manager.AdditionalEventParsers EventParser = Manager.AdditionalEventParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion); .FirstOrDefault(parser =>
parser.Version == ServerConfig.EventParserVersion ||
parser.Name == ServerConfig.RConParserVersion);
RconParser = RconParser ?? Manager.AdditionalRConParsers[0]; RconParser ??= Manager.AdditionalRConParsers[0];
EventParser = EventParser ?? Manager.AdditionalEventParsers[0]; EventParser ??= Manager.AdditionalEventParsers[0];
RemoteConnection = RConConnectionFactory.CreateConnection(ResolvedIpEndPoint, Password, RconParser.RConEngine);
RemoteConnection.SetConfiguration(RconParser); RemoteConnection.SetConfiguration(RconParser);
var version = await this.GetMappedDvarValueOrDefaultAsync<string>("version"); var version = await this.GetMappedDvarValueOrDefaultAsync<string>("version");
Version = version.Value; Version = version.Value;
GameName = Utilities.GetGame(version?.Value ?? RconParser.Version); GameName = Utilities.GetGame(version.Value ?? RconParser.Version);
if (GameName == Game.UKN) if (GameName == Game.UKN)
{ {
GameName = RconParser.GameName; GameName = RconParser.GameName;
} }
if (version?.Value?.Length != 0) if (version.Value?.Length != 0)
{ {
var matchedRconParser = Manager.AdditionalRConParsers.FirstOrDefault(_parser => _parser.Version == version.Value); var matchedRconParser = Manager.AdditionalRConParsers.FirstOrDefault(_parser => _parser.Version == version.Value);
RconParser.Configuration = matchedRconParser != null ? matchedRconParser.Configuration : RconParser.Configuration; RconParser.Configuration = matchedRconParser != null ? matchedRconParser.Configuration : RconParser.Configuration;
@ -984,8 +1133,9 @@ namespace IW4MAdmin
string mapname = (await this.GetMappedDvarValueOrDefaultAsync<string>("mapname", infoResponse: infoResponse)).Value; string mapname = (await this.GetMappedDvarValueOrDefaultAsync<string>("mapname", infoResponse: infoResponse)).Value;
int maxplayers = (await this.GetMappedDvarValueOrDefaultAsync<int>("sv_maxclients", 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; string gametype = (await this.GetMappedDvarValueOrDefaultAsync<string>("g_gametype", "gametype", infoResponse)).Value;
var basepath = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basepath")); var basepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basepath");
var basegame = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basegame")); 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 game = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_game", infoResponse: infoResponse));
var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log"); var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log");
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync"); var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync");
@ -1015,8 +1165,20 @@ namespace IW4MAdmin
{ {
Website = loc["SERVER_WEBSITE_GENERIC"]; Website = loc["SERVER_WEBSITE_GENERIC"];
} }
// todo: remove this once _website is weaned off
if (string.IsNullOrEmpty(Manager.GetApplicationSettings().Configuration().ContactUri))
{
Manager.GetApplicationSettings().Configuration().ContactUri = Website;
}
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; WorkingDirectory = basepath.Value;
this.Hostname = hostname; this.Hostname = hostname;
@ -1074,15 +1236,16 @@ namespace IW4MAdmin
{ {
BaseGameDirectory = basegame.Value, BaseGameDirectory = basegame.Value,
BasePathDirectory = basepath.Value, BasePathDirectory = basepath.Value,
HomePathDirectory = homepath.Value,
GameDirectory = EventParser.Configuration.GameDirectory ?? "", GameDirectory = EventParser.Configuration.GameDirectory ?? "",
ModDirectory = game.Value ?? "", ModDirectory = game.Value ?? "",
LogFile = logfile.Value, LogFile = logfile.Value,
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
IsOneLog = RconParser.IsOneLog
}; };
LogPath = GenerateLogPath(logInfo); LogPath = GenerateLogPath(logInfo);
ServerLogger.LogInformation("Game log information {@logInfo}", logInfo); ServerLogger.LogInformation("Game log information {@logInfo}", logInfo);
if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null) if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null)
{ {
Console.WriteLine(loc["SERVER_ERROR_DNE"].FormatExt(LogPath)); Console.WriteLine(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
@ -1096,6 +1259,7 @@ namespace IW4MAdmin
this, this,
GenerateUriForLog(LogPath, ServerConfig.GameLogServerUrl?.AbsoluteUri), gameLogReaderFactory); GenerateUriForLog(LogPath, ServerConfig.GameLogServerUrl?.AbsoluteUri), gameLogReaderFactory);
await _serverCache.InitializeAsync();
_ = Task.Run(() => LogEvent.PollForChanges()); _ = Task.Run(() => LogEvent.PollForChanges());
if (!Utilities.IsDevelopment) if (!Utilities.IsDevelopment)
@ -1106,7 +1270,7 @@ namespace IW4MAdmin
public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl) public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl)
{ {
var logUri = new Uri(logPath); var logUri = new Uri(logPath, UriKind.Absolute);
if (string.IsNullOrEmpty(gameLogServerUrl)) if (string.IsNullOrEmpty(gameLogServerUrl))
{ {
@ -1122,21 +1286,31 @@ namespace IW4MAdmin
public static string GenerateLogPath(LogPathGeneratorInfo logInfo) public static string GenerateLogPath(LogPathGeneratorInfo logInfo)
{ {
string logPath; 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) && var baseGameIsRelative = path.FixDirectoryCharacters()
logInfo.BaseGameDirectory.IndexOfAny(Utilities.DirectorySeparatorChars) != -1; .Equals(logInfo.GameDirectory.FixDirectoryCharacters(), StringComparison.InvariantCultureIgnoreCase);
bool baseGameIsRelative = logInfo.BaseGameDirectory.FixDirectoryCharacters() return baseGameIsDirectory && !baseGameIsRelative;
.Equals(logInfo.GameDirectory.FixDirectoryCharacters(), StringComparison.InvariantCultureIgnoreCase); }
// we want to see if base game is provided and it 'looks' like a directory // 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; workingDirectory = logInfo.BaseGameDirectory;
} }
if (string.IsNullOrWhiteSpace(logInfo.ModDirectory)) if (string.IsNullOrWhiteSpace(logInfo.ModDirectory) || logInfo.IsOneLog)
{ {
logPath = Path.Combine(workingDirectory, logInfo.GameDirectory, logInfo.LogFile); logPath = Path.Combine(workingDirectory, logInfo.GameDirectory, logInfo.LogFile);
} }
@ -1158,12 +1332,9 @@ namespace IW4MAdmin
public override async Task Warn(string reason, EFClient targetClient, EFClient targetOrigin) public override async Task Warn(string reason, EFClient targetClient, EFClient targetOrigin)
{ {
// ensure player gets warned if command not performed on them in game // ensure player gets warned if command not performed on them in game
targetClient = targetClient.ClientNumber < 0 ? var activeClient = Manager.FindActiveClient(targetClient);
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var newPenalty = new EFPenalty() var newPenalty = new EFPenalty
{ {
Type = EFPenalty.PenaltyType.Warning, Type = EFPenalty.PenaltyType.Warning,
Expires = DateTime.UtcNow, Expires = DateTime.UtcNow,
@ -1173,99 +1344,95 @@ namespace IW4MAdmin
Link = targetClient.AliasLink 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); 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; return;
} }
// todo: move to translation sheet var message = loc["COMMANDS_WARNING_FORMAT_V2"]
string message = $"^1{loc["SERVER_WARNING"]} ^7[^3{targetClient.Warnings}^7]: ^3{targetClient.Name}^7, {reason}"; .FormatExt(activeClient.Warnings, activeClient.Name, reason);
targetClient.CurrentServer.Broadcast(message); activeClient.CurrentServer.Broadcast(message);
} }
} }
public override async Task Kick(string Reason, EFClient targetClient, EFClient originClient) public override async Task Kick(string reason, EFClient targetClient, EFClient originClient, EFPenalty previousPenalty)
{ {
targetClient = targetClient.ClientNumber < 0 ? var activeClient = Manager.FindActiveClient(targetClient);
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var newPenalty = new EFPenalty() var newPenalty = new EFPenalty
{ {
Type = EFPenalty.PenaltyType.Kick, Type = EFPenalty.PenaltyType.Kick,
Expires = DateTime.UtcNow, Expires = DateTime.UtcNow,
Offender = targetClient, Offender = targetClient,
Offense = Reason, Offense = reason,
Punisher = originClient, Punisher = originClient,
Link = targetClient.AliasLink Link = targetClient.AliasLink
}; };
ServerLogger.LogDebug("Creating kick penalty for {targetClient}", targetClient.ToString()); ServerLogger.LogDebug("Creating kick penalty for {TargetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger); await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame) if (activeClient.IsIngame)
{ {
var e = new GameEvent() var gameEvent = new GameEvent
{ {
Type = GameEvent.EventType.PreDisconnect, Type = GameEvent.EventType.PreDisconnect,
Origin = targetClient, Origin = activeClient,
Owner = this Owner = this
}; };
Manager.AddEvent(e); Manager.AddEvent(gameEvent);
// todo: move to translation sheet var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7"); activeClient.TemporalClientNumber,
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick); _messageFormatter.BuildFormattedMessage(RconParser.Configuration,
newPenalty,
previousPenalty));
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 // ensure player gets kicked if command not performed on them in the same server
targetClient = targetClient.ClientNumber < 0 ? var activeClient = Manager.FindActiveClient(targetClient);
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var newPenalty = new EFPenalty() var newPenalty = new EFPenalty
{ {
Type = EFPenalty.PenaltyType.TempBan, Type = EFPenalty.PenaltyType.TempBan,
Expires = DateTime.UtcNow + length, Expires = DateTime.UtcNow + length,
Offender = targetClient, Offender = targetClient,
Offense = Reason, Offense = reason,
Punisher = originClient, Punisher = originClient,
Link = targetClient.AliasLink 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); await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
if (targetClient.IsIngame) if (activeClient.IsIngame)
{ {
// todo: move to translation sheet var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}"); activeClient.TemporalClientNumber,
ServerLogger.LogDebug("Executing tempban kick command for {targetClient}", targetClient.ToString()); _messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick); ServerLogger.LogDebug("Executing tempban kick command for {ActiveClient}", activeClient.ToString());
await activeClient.CurrentServer.ExecuteCommandAsync(formattedKick);
} }
} }
override public async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false) public override async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
{ {
// ensure player gets kicked if command not performed on them in the same server // ensure player gets kicked if command not performed on them in the same server
targetClient = targetClient.ClientNumber < 0 ? var activeClient = Manager.FindActiveClient(targetClient);
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
EFPenalty newPenalty = new EFPenalty() var newPenalty = new EFPenalty
{ {
Type = EFPenalty.PenaltyType.Ban, Type = EFPenalty.PenaltyType.Ban,
Expires = null, Expires = null,
@ -1276,45 +1443,47 @@ namespace IW4MAdmin
IsEvadedOffense = isEvade IsEvadedOffense = isEvade
}; };
ServerLogger.LogDebug("Creating ban penalty for {targetClient}", targetClient.ToString()); ServerLogger.LogDebug("Creating ban penalty for {TargetClient}", targetClient.ToString());
targetClient.SetLevel(Permission.Banned, originClient); activeClient.SetLevel(Permission.Banned, originClient);
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger); 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());
// todo: move to translation sheet
string formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7"); var formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString); activeClient.TemporalClientNumber,
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
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, Type = EFPenalty.PenaltyType.Unban,
Expires = DateTime.Now, Expires = DateTime.Now,
Offender = Target, Offender = targetClient,
Offense = reason, Offense = reason,
Punisher = Origin, Punisher = originClient,
When = DateTime.UtcNow, When = DateTime.UtcNow,
Active = true, Active = true,
Link = Target.AliasLink Link = targetClient.AliasLink
}; };
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", Target.ToString()); ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString());
Target.SetLevel(Permission.User, Origin); targetClient.SetLevel(Permission.User, originClient);
await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId); await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId);
await Manager.GetPenaltyService().Create(unbanPenalty); 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("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("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("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

@ -63,6 +63,10 @@ namespace IW4MAdmin.Application.Localization
{ {
var localizationContents = File.ReadAllText(filePath, Encoding.UTF8); var localizationContents = File.ReadAllText(filePath, Encoding.UTF8);
var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents); var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents);
if (eachLocalizationFile == null)
{
continue;
}
foreach (var item in eachLocalizationFile.LocalizationIndex.Set) foreach (var item in eachLocalizationFile.LocalizationIndex.Set)
{ {

View File

@ -17,25 +17,34 @@ using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Repositories; using SharedLibraryCore.Repositories;
using SharedLibraryCore.Services; using SharedLibraryCore.Services;
using Stats.Dtos; using Stats.Dtos;
using StatsWeb;
using System; using System;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Data.Helpers;
using Integrations.Source.Extensions;
using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Localization; using IW4MAdmin.Application.Localization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
using IW4MAdmin.Plugins.Stats.Client;
using Stats.Client.Abstractions;
using Stats.Client;
using Stats.Config;
using Stats.Helpers;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
public class Program public class Program
{ {
public static BuildNumber Version { get; private set; } = BuildNumber.Parse(Utilities.GetVersionAsString()); public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString());
public static ApplicationManager ServerManager; private static ApplicationManager _serverManager;
private static Task ApplicationTask; private static Task _applicationTask;
private static ServiceProvider serviceProvider; private static ServiceProvider _serviceProvider;
/// <summary> /// <summary>
/// entrypoint of the application /// entrypoint of the application
@ -48,7 +57,7 @@ namespace IW4MAdmin.Application
Console.OutputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray; Console.ForegroundColor = ConsoleColor.Gray;
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey); Console.CancelKeyPress += OnCancelKey;
Console.WriteLine("====================================================="); Console.WriteLine("=====================================================");
Console.WriteLine(" IW4MAdmin"); Console.WriteLine(" IW4MAdmin");
@ -67,8 +76,11 @@ namespace IW4MAdmin.Application
/// <param name="e"></param> /// <param name="e"></param>
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e) private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
{ {
ServerManager?.Stop(); _serverManager?.Stop();
await ApplicationTask; if (_applicationTask != null)
{
await _applicationTask;
}
} }
/// <summary> /// <summary>
@ -77,32 +89,44 @@ namespace IW4MAdmin.Application
/// <returns></returns> /// <returns></returns>
private static async Task LaunchAsync(string[] args) private static async Task LaunchAsync(string[] args)
{ {
restart: restart:
ITranslationLookup translationLookup = null; ITranslationLookup translationLookup = null;
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration()); var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
Utilities.DefaultLogger = logger; Utilities.DefaultLogger = logger;
logger.LogInformation("Begin IW4MAdmin startup. Version is {version} {@args}", Version, args); logger.LogInformation("Begin IW4MAdmin startup. Version is {Version} {@Args}", Version, args);
try try
{ {
// do any needed housekeeping file/folder migrations // do any needed housekeeping file/folder migrations
ConfigurationMigration.MoveConfigFolder10518(null); ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories(); ConfigurationMigration.CheckDirectories();
ConfigurationMigration.RemoveObsoletePlugins20210322();
logger.LogDebug("Configuring services..."); logger.LogDebug("Configuring services...");
var services = ConfigureServices(args); var services = await ConfigureServices(args);
serviceProvider = services.BuildServiceProvider(); _serviceProvider = services.BuildServiceProvider();
var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>(); var versionChecker = _serviceProvider.GetRequiredService<IMasterCommunication>();
ServerManager = (ApplicationManager)serviceProvider.GetRequiredService<IManager>(); _serverManager = (ApplicationManager) _serviceProvider.GetRequiredService<IManager>();
translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>(); translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
await versionChecker.CheckVersion(); _applicationTask = RunApplicationTasksAsync(logger, services);
await ServerManager.Init(); var tasks = new[]
{
versionChecker.CheckVersion(),
_serverManager.Init(),
_applicationTask
};
await Task.WhenAll(tasks);
} }
catch (Exception e) catch (Exception e)
{ {
string failMessage = translationLookup == null ? "Failed to initialize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"]; var failMessage = translationLookup == null
string exitMessage = translationLookup == null ? "Press enter to exit..." : translationLookup["MANAGER_EXIT"]; ? "Failed to initialize IW4MAdmin"
: translationLookup["MANAGER_INIT_FAIL"];
var exitMessage = translationLookup == null
? "Press enter to exit..."
: translationLookup["MANAGER_EXIT"];
logger.LogCritical(e, "Failed to initialize IW4MAdmin"); logger.LogCritical(e, "Failed to initialize IW4MAdmin");
Console.WriteLine(failMessage); Console.WriteLine(failMessage);
@ -116,10 +140,11 @@ namespace IW4MAdmin.Application
{ {
if (translationLookup != null) if (translationLookup != null)
{ {
Console.WriteLine(translationLookup[configException.Message].FormatExt(configException.ConfigurationFileName)); Console.WriteLine(translationLookup[configException.Message]
.FormatExt(configException.ConfigurationFileName));
} }
foreach (string error in configException.Errors) foreach (var error in configException.Errors)
{ {
Console.WriteLine(error); Console.WriteLine(error);
} }
@ -135,57 +160,49 @@ namespace IW4MAdmin.Application
return; return;
} }
try if (_serverManager.IsRestartRequested)
{
ApplicationTask = RunApplicationTasksAsync(logger);
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)
{ {
goto restart; goto restart;
} }
serviceProvider.Dispose(); await _serviceProvider.DisposeAsync();
} }
/// <summary> /// <summary>
/// runs the core application tasks /// runs the core application tasks
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private static async Task RunApplicationTasksAsync(ILogger logger) private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
{ {
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ? var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront
WebfrontCore.Program.Init(ServerManager, serviceProvider, ServerManager.CancellationToken) : ? WebfrontCore.Program.Init(_serverManager, _serviceProvider, services, _serverManager.CancellationToken)
Task.CompletedTask; : Task.CompletedTask;
var collectionService = _serviceProvider.GetRequiredService<IServerDataCollector>();
// we want to run this one on a manual thread instead of letting the thread pool handle it, // we want to run this one on a manual thread instead of letting the thread pool handle it,
// because we can't exit early from waiting on console input, and it prevents us from restarting // because we can't exit early from waiting on console input, and it prevents us from restarting
var inputThread = new Thread(async () => await ReadConsoleInput(logger)); async void ReadInput() => await ReadConsoleInput(logger);
var inputThread = new Thread(ReadInput);
inputThread.Start(); inputThread.Start();
var tasks = new[] var tasks = new[]
{ {
ServerManager.Start(),
webfrontTask, 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"); logger.LogDebug("Starting webfront and input tasks");
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
logger.LogInformation("Shutdown completed successfully"); logger.LogInformation("Shutdown completed successfully");
Console.Write(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]); Console.WriteLine(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
} }
/// <summary> /// <summary>
/// reads input from the console and executes entered commands on the default server /// reads input from the console and executes entered commands on the default server
/// </summary> /// </summary>
@ -198,40 +215,50 @@ namespace IW4MAdmin.Application
return; return;
} }
string lastCommand; EFClient origin = null;
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
try try
{ {
while (!ServerManager.CancellationToken.IsCancellationRequested) while (!_serverManager.CancellationToken.IsCancellationRequested)
{ {
lastCommand = await Console.In.ReadLineAsync(); if (!_serverManager.IsInitialized)
if (lastCommand?.Length > 0)
{ {
if (lastCommand?.Length > 0) await Task.Delay(1000);
{ continue;
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('>');
}
} }
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) catch (OperationCanceledException)
{ } {
}
} }
private static IServiceCollection HandlePluginRegistration(ApplicationConfiguration appConfig, private static IServiceCollection HandlePluginRegistration(ApplicationConfiguration appConfig,
IServiceCollection serviceCollection, IServiceCollection serviceCollection,
IMasterApi masterApi) IMasterApi masterApi)
{ {
var defaultLogger = BuildDefaultLogger<Program>(appConfig); var defaultLogger = BuildDefaultLogger<Program>(appConfig);
@ -244,33 +271,45 @@ namespace IW4MAdmin.Application
.BuildServiceProvider(); .BuildServiceProvider();
var pluginImporter = pluginServiceProvider.GetRequiredService<IPluginImporter>(); var pluginImporter = pluginServiceProvider.GetRequiredService<IPluginImporter>();
// we need to register the rest client with regular collection // we need to register the rest client with regular collection
serviceCollection.AddSingleton(masterApi); serviceCollection.AddSingleton(masterApi);
// register the native commands // register the native commands
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes() foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
.Where(_command => _command.BaseType == typeof(Command))) .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); serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
} }
// register the plugin implementations // register the plugin implementations
var pluginImplementations = pluginImporter.DiscoverAssemblyPluginImplementations(); var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in pluginImplementations.Item1) 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); serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
} }
// register the plugin commands // register the plugin commands
foreach (var commandType in pluginImplementations.Item2) 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); serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
} }
foreach (var configurationType in configurations)
{
defaultLogger.LogDebug("Registered plugin config type {Name}", configurationType.Name);
var configInstance = (IBaseConfiguration) Activator.CreateInstance(configurationType);
var handlerType = typeof(BaseConfigurationHandler<>).MakeGenericType(configurationType);
var handlerInstance = Activator.CreateInstance(handlerType, configInstance.Name());
var genericInterfaceType = typeof(IConfigurationHandler<>).MakeGenericType(configurationType);
serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
}
// register any script plugins // register any script plugins
foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins()) foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins())
{ {
@ -279,11 +318,10 @@ namespace IW4MAdmin.Application
// register any eventable types // register any eventable types
foreach (var assemblyType in typeof(Program).Assembly.GetTypes() foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType)) .Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType))
.Union(pluginImplementations .Union(plugins.SelectMany(asm => asm.Assembly.GetTypes())
.Item1.SelectMany(_asm => _asm.Assembly.GetTypes()) .Distinct()
.Distinct() .Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType))))
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
{ {
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent; var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
serviceCollection.AddSingleton(instance); serviceCollection.AddSingleton(instance);
@ -291,40 +329,70 @@ namespace IW4MAdmin.Application
return serviceCollection; return serviceCollection;
} }
/// <summary> /// <summary>
/// Configures the dependency injection services /// Configures the dependency injection services
/// </summary> /// </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) // setup the static resources (config/master api/translations)
var serviceCollection = new ServiceCollection(); var serviceCollection = new ServiceCollection();
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings"); 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 appConfig = appConfigHandler.Configuration();
var masterUri = Utilities.IsDevelopment var masterUri = Utilities.IsDevelopment
? new Uri("http://127.0.0.1:8080") ? new Uri("http://127.0.0.1:8080")
: appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl; : appConfig?.MasterUrl ?? new ApplicationConfiguration().MasterUrl;
var masterRestClient = RestClient.For<IMasterApi>(masterUri); var httpClient = new HttpClient
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig); {
BaseAddress = masterUri,
Timeout = TimeSpan.FromSeconds(15)
};
var masterRestClient = RestClient.For<IMasterApi>(httpClient);
var translationLookup = Configure.Initialize(Utilities.DefaultLogger, masterRestClient, appConfig);
if (appConfig == null) if (appConfig == null)
{ {
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate(); appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
appConfigHandler.Set(appConfig); appConfigHandler.Set(appConfig);
appConfigHandler.Save(); await appConfigHandler.Save();
} }
// register override level names
foreach (var (key, value) in appConfig.OverridePermissionLevelNames)
{
if (!Utilities.PermissionLevelOverrides.ContainsKey(key))
{
Utilities.PermissionLevelOverrides.Add(key, value);
}
}
// build the dependency list // build the dependency list
HandlePluginRegistration(appConfig, serviceCollection, masterRestClient); HandlePluginRegistration(appConfig, serviceCollection, masterRestClient);
serviceCollection serviceCollection
.AddBaseLogger(appConfig) .AddBaseLogger(appConfig)
.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection) .AddSingleton(defaultConfig)
.AddSingleton<IServiceCollection>(serviceCollection)
.AddSingleton<IConfigurationHandler<DefaultSettings>, BaseConfigurationHandler<DefaultSettings>>()
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler) .AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
.AddSingleton(new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as IConfigurationHandler<CommandConfiguration>) .AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler)
.AddSingleton(appConfig) .AddSingleton(appConfig)
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>().Configuration() ?? new CommandConfiguration()) .AddSingleton(statsCommandHandler.Configuration() ?? new StatsConfiguration())
.AddSingleton(serviceProvider =>
serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
.Configuration() ?? new CommandConfiguration())
.AddSingleton<IPluginImporter, PluginImporter>() .AddSingleton<IPluginImporter, PluginImporter>()
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>() .AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
.AddSingleton<IRConConnectionFactory, RConConnectionFactory>() .AddSingleton<IRConConnectionFactory, RConConnectionFactory>()
@ -338,19 +406,37 @@ namespace IW4MAdmin.Application
.AddSingleton<IEntityService<EFClient>, ClientService>() .AddSingleton<IEntityService<EFClient>, ClientService>()
.AddSingleton<IMetaService, MetaService>() .AddSingleton<IMetaService, MetaService>()
.AddSingleton<ClientService>() .AddSingleton<ClientService>()
.AddSingleton<PenaltyService>()
.AddSingleton<ChangeHistoryService>() .AddSingleton<ChangeHistoryService>()
.AddSingleton<IMetaRegistration, MetaRegistration>() .AddSingleton<IMetaRegistration, MetaRegistration>()
.AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>() .AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>, ReceivedPenaltyResourceQueryHelper>() .AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>,
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>, AdministeredPenaltyResourceQueryHelper>() ReceivedPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>, UpdatedAliasResourceQueryHelper>() .AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>,
AdministeredPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>,
UpdatedAliasResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>() .AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>, ConnectionsResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>() .AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>() .AddSingleton<IRemoteAssemblyHandler, RemoteAssemblyHandler>()
.AddSingleton<IMasterCommunication, MasterCommunication>() .AddSingleton<IMasterCommunication, MasterCommunication>()
.AddSingleton<IManager, ApplicationManager>() .AddSingleton<IManager, ApplicationManager>()
#pragma warning disable CS0612
.AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>() .AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>()
.AddSingleton(translationLookup); #pragma warning restore CS0612
.AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>()
.AddSingleton<IClientStatisticCalculator, HitCalculator>()
.AddSingleton<IServerDistributionCalculator, ServerDistributionCalculator>()
.AddSingleton<IWeaponNameParser, WeaponNameParser>()
.AddSingleton<IHitInfoBuilder, HitInfoBuilder>()
.AddSingleton(typeof(ILookupCache<>), typeof(LookupCache<>))
.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>))
.AddSingleton<IServerDataViewer, ServerDataViewer>()
.AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IEventPublisher, EventPublisher>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);
if (args.Contains("serialevents")) if (args.Contains("serialevents"))
{ {
@ -361,9 +447,11 @@ namespace IW4MAdmin.Application
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>(); serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
} }
serviceCollection.AddSource();
return serviceCollection; return serviceCollection;
} }
private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig) private static ILogger BuildDefaultLogger<T>(ApplicationConfiguration appConfig)
{ {
var collection = new ServiceCollection() var collection = new ServiceCollection()

View File

@ -1,8 +1,9 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses; using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -28,7 +29,7 @@ namespace IW4MAdmin.Application.Meta
public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query) public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{ {
using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var iqPenalties = ctx.Penalties.AsNoTracking() var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => query.ClientId == _penalty.PunisherId) .Where(_penalty => query.ClientId == _penalty.PunisherId)

View File

@ -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, ReceivedPenaltyResponse> _receivedPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> _administeredPenaltyHelper; private readonly IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> _administeredPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> _updatedAliasHelper; private readonly IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> _updatedAliasHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse>
_connectionHistoryHelper;
public MetaRegistration(ILogger<MetaRegistration> logger, IMetaService metaService, ITranslationLookup transLookup, IEntityService<EFClient> clientEntityService, public MetaRegistration(ILogger<MetaRegistration> logger, IMetaService metaService, ITranslationLookup transLookup, IEntityService<EFClient> clientEntityService,
IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> receivedPenaltyHelper, IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> receivedPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> administeredPenaltyHelper, IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> administeredPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper) IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper,
IResourceQueryHelper<ClientPaginationRequest, ConnectionHistoryResponse> connectionHistoryHelper)
{ {
_logger = logger; _logger = logger;
_transLookup = transLookup; _transLookup = transLookup;
@ -33,6 +36,7 @@ namespace IW4MAdmin.Application.Meta
_receivedPenaltyHelper = receivedPenaltyHelper; _receivedPenaltyHelper = receivedPenaltyHelper;
_administeredPenaltyHelper = administeredPenaltyHelper; _administeredPenaltyHelper = administeredPenaltyHelper;
_updatedAliasHelper = updatedAliasHelper; _updatedAliasHelper = updatedAliasHelper;
_connectionHistoryHelper = connectionHistoryHelper;
} }
public void Register() public void Register()
@ -41,6 +45,7 @@ namespace IW4MAdmin.Application.Meta
_metaService.AddRuntimeMeta<ClientPaginationRequest, ReceivedPenaltyResponse>(MetaType.ReceivedPenalty, GetReceivedPenaltiesMeta); _metaService.AddRuntimeMeta<ClientPaginationRequest, ReceivedPenaltyResponse>(MetaType.ReceivedPenalty, GetReceivedPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, AdministeredPenaltyResponse>(MetaType.Penalized, GetAdministeredPenaltiesMeta); _metaService.AddRuntimeMeta<ClientPaginationRequest, AdministeredPenaltyResponse>(MetaType.Penalized, GetAdministeredPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, UpdatedAliasResponse>(MetaType.AliasUpdate, GetUpdatedAliasMeta); _metaService.AddRuntimeMeta<ClientPaginationRequest, UpdatedAliasResponse>(MetaType.AliasUpdate, GetUpdatedAliasMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, ConnectionHistoryResponse>(MetaType.ConnectionHistory, GetConnectionHistoryMeta);
} }
private async Task<IEnumerable<InformationResponse>> GetProfileMeta(ClientPaginationRequest request) private async Task<IEnumerable<InformationResponse>> GetProfileMeta(ClientPaginationRequest request)
@ -163,5 +168,11 @@ namespace IW4MAdmin.Application.Meta
var aliases = await _updatedAliasHelper.QueryResource(request); var aliases = await _updatedAliasHelper.QueryResource(request);
return aliases.Results; 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; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos.Meta.Responses; using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -21,29 +23,57 @@ namespace IW4MAdmin.Application.Meta
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; 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; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
_appConfig = appConfig;
} }
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query) public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{ {
var linkedPenaltyType = Utilities.LinkedPenaltyTypes(); var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
var linkId = await ctx.Clients.AsNoTracking() var linkId = await ctx.Clients.AsNoTracking()
.Where(_client => _client.ClientId == query.ClientId) .Where(_client => _client.ClientId == query.ClientId)
.Select(_client => _client.AliasLinkId) .Select(_client => new {_client.AliasLinkId, _client.CurrentAliasId })
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
var iqPenalties = ctx.Penalties.AsNoTracking() var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => _penalty.OffenderId == query.ClientId || (linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId)) .Where(_penalty => _penalty.OffenderId == query.ClientId ||
.Where(_penalty => _penalty.When < query.Before) linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId.AliasLinkId);
.OrderByDescending(_penalty => _penalty.When);
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) .Take(query.Count)
.Select(_penalty => new ReceivedPenaltyResponse() .Select(_penalty => new ReceivedPenaltyResponse()
{ {

View File

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

View File

@ -85,5 +85,20 @@ namespace IW4MAdmin.Application.Migration
config.ManualLogPath = null; config.ManualLogPath = null;
} }
} }
public static void RemoveObsoletePlugins20210322()
{
var files = new[] {"StatsWeb.dll", "StatsWeb.Views.dll"};
foreach (var file in files)
{
var path = Path.Join(Utilities.OperatingDirectory, "Plugins", file);
if (File.Exists(path))
{
File.Delete(path);
}
}
}
} }
} }

View File

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

View File

@ -1,10 +1,13 @@
using Newtonsoft.Json; using SharedLibraryCore;
using SharedLibraryCore;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.IO; using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -14,22 +17,39 @@ namespace IW4MAdmin.Application.Misc
/// <typeparam name="T">base configuration type</typeparam> /// <typeparam name="T">base configuration type</typeparam>
public class BaseConfigurationHandler<T> : IConfigurationHandler<T> where T : IBaseConfiguration 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"); _serializerOptions = new JsonSerializerOptions
Build(); {
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 string FileName { get; }
public void Build() public async Task BuildAsync()
{ {
try try
{ {
var configContent = File.ReadAllText(FileName); await using var fileStream = File.OpenRead(FileName);
_configuration = JsonConvert.DeserializeObject<T>(configContent); _configuration = await JsonSerializer.DeserializeAsync<T>(fileStream, _serializerOptions);
} }
catch (FileNotFoundException) catch (FileNotFoundException)
@ -47,16 +67,23 @@ namespace IW4MAdmin.Application.Misc
} }
} }
public Task Save() public async Task Save()
{ {
var settings = new JsonSerializerSettings() try
{ {
Formatting = Formatting.Indented await _onSaving.WaitAsync();
};
settings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
var appConfigJSON = JsonConvert.SerializeObject(_configuration, settings); await using var fileStream = File.Create(FileName);
return File.WriteAllTextAsync(FileName, appConfigJSON); await JsonSerializer.SerializeAsync(fileStream, _configuration, _serializerOptions);
}
finally
{
if (_onSaving.CurrentCount == 0)
{
_onSaving.Release(1);
}
}
} }
public T Configuration() public T Configuration()

View File

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

View File

@ -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> /// </summary>
public string BasePathDirectory { get; set; } = ""; public string BasePathDirectory { get; set; } = "";
/// <summary>
/// directory for local storage
/// <remarks>fs_homepath</remarks>
/// </summary>
public string HomePathDirectory { get; set; } = "";
/// <summary> /// <summary>
/// overide game directory /// overide game directory
/// <remarks>plugin driven</remarks> /// <remarks>plugin driven</remarks>
@ -41,5 +47,11 @@ namespace IW4MAdmin.Application.Misc
/// indicates if running on windows /// indicates if running on windows
/// </summary> /// </summary>
public bool IsWindows { get; set; } = true; public bool IsWindows { get; set; } = true;
/// <summary>
/// indicates that the game does not log to the mods folder (when mod is loaded),
/// but rather always to the fs_basegame directory
/// </summary>
public bool IsOneLog { get; set; }
} }
} }

View File

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

View File

@ -7,8 +7,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
using Data.Models;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
@ -29,7 +31,7 @@ namespace IW4MAdmin.Application.Misc
_contextFactory = contextFactory; _contextFactory = contextFactory;
} }
public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client) public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client, EFMeta linkedMeta = null)
{ {
// this seems to happen if the client disconnects before they've had time to authenticate and be added // this seems to happen if the client disconnects before they've had time to authenticate and be added
if (client.ClientId < 1) if (client.ClientId < 1)
@ -37,7 +39,7 @@ namespace IW4MAdmin.Application.Misc
return; return;
} }
using var ctx = _contextFactory.CreateContext(); await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta var existingMeta = await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey) .Where(_meta => _meta.Key == metaKey)
@ -48,6 +50,7 @@ namespace IW4MAdmin.Application.Misc
{ {
existingMeta.Value = metaValue; existingMeta.Value = metaValue;
existingMeta.Updated = DateTime.UtcNow; existingMeta.Updated = DateTime.UtcNow;
existingMeta.LinkedMetaId = linkedMeta?.MetaId;
} }
else else
@ -57,16 +60,101 @@ namespace IW4MAdmin.Application.Misc
ClientId = client.ClientId, ClientId = client.ClientId,
Created = DateTime.UtcNow, Created = DateTime.UtcNow,
Key = metaKey, Key = metaKey,
Value = metaValue Value = metaValue,
LinkedMetaId = linkedMeta?.MetaId
}); });
} }
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
public async Task AddPersistentMeta(string metaKey, string metaValue)
{
await using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.ToListAsync();
var matchValues = existingMeta
.Where(meta => meta.Value == metaValue)
.ToArray();
if (matchValues.Any())
{
foreach (var meta in matchValues)
{
_logger.LogDebug("Updating existing meta with key {key} and id {id}", meta.Key, meta.MetaId);
meta.Value = metaValue;
meta.Updated = DateTime.UtcNow;
}
await ctx.SaveChangesAsync();
}
else
{
_logger.LogDebug("Adding new meta with key {key}", metaKey);
ctx.EFMeta.Add(new EFMeta()
{
Created = DateTime.UtcNow,
Key = metaKey,
Value = metaValue
});
await ctx.SaveChangesAsync();
}
}
public async Task RemovePersistentMeta(string metaKey, EFClient client)
{
await using var context = _contextFactory.CreateContext();
var existingMeta = await context.EFMeta
.FirstOrDefaultAsync(meta => meta.Key == metaKey && meta.ClientId == client.ClientId);
if (existingMeta == null)
{
_logger.LogDebug("No meta with key {key} found for client id {id}", metaKey, client.ClientId);
return;
}
_logger.LogDebug("Removing meta for key {key} with id {id}", metaKey, existingMeta.MetaId);
context.EFMeta.Remove(existingMeta);
await context.SaveChangesAsync();
}
public async Task RemovePersistentMeta(string metaKey, string metaValue = null)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
var existingMeta = await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.ToListAsync();
if (metaValue == null)
{
_logger.LogDebug("Removing all meta for key {key} with ids [{ids}] ", metaKey, string.Join(", ", existingMeta.Select(meta => meta.MetaId)));
existingMeta.ForEach(meta => context.Remove(existingMeta));
await context.SaveChangesAsync();
return;
}
var foundMeta = existingMeta.FirstOrDefault(meta => meta.Value == metaValue);
if (foundMeta != null)
{
_logger.LogDebug("Removing meta for key {key} with id {id}", metaKey, foundMeta.MetaId);
context.Remove(foundMeta);
await context.SaveChangesAsync();
}
}
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client) public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
{ {
using var ctx = _contextFactory.CreateContext(enableTracking: false); await using var ctx = _contextFactory.CreateContext(enableTracking: false);
return await ctx.EFMeta return await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey) .Where(_meta => _meta.Key == metaKey)
@ -76,11 +164,34 @@ namespace IW4MAdmin.Application.Misc
MetaId = _meta.MetaId, MetaId = _meta.MetaId,
Key = _meta.Key, Key = _meta.Key,
ClientId = _meta.ClientId, ClientId = _meta.ClientId,
Value = _meta.Value Value = _meta.Value,
LinkedMetaId = _meta.LinkedMetaId,
LinkedMeta = _meta.LinkedMetaId != null ? new EFMeta()
{
MetaId = _meta.LinkedMeta.MetaId,
Key = _meta.LinkedMeta.Key,
Value = _meta.LinkedMeta.Value
} : null
}) })
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }
public async Task<IEnumerable<EFMeta>> GetPersistentMeta(string metaKey)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
return await context.EFMeta
.Where(meta => meta.Key == metaKey)
.Where(meta => meta.ClientId == null)
.Select(meta => new EFMeta
{
MetaId = meta.MetaId,
Key = meta.Key,
ClientId = meta.ClientId,
Value = meta.Value,
})
.ToListAsync();
}
public void AddRuntimeMeta<T, V>(MetaType metaKey, Func<T, Task<IEnumerable<V>>> metaAction) where T : PaginationRequest where V : IClientMeta public void AddRuntimeMeta<T, V>(MetaType metaKey, Func<T, Task<IEnumerable<V>>> metaAction) where T : PaginationRequest where V : IClientMeta
{ {
if (!_metaActions.ContainsKey(metaKey)) if (!_metaActions.ContainsKey(metaKey))
@ -96,42 +207,30 @@ namespace IW4MAdmin.Application.Misc
public async Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request) 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) return metas.SelectMany(m => (IEnumerable<IClientMeta>)m)
{ .OrderByDescending(m => m.When)
// information is not listed chronologically
if (type != MetaType.Information)
{
var metaItems = await actions[0](request);
meta.AddRange(metaItems);
}
}
return meta.OrderByDescending(_meta => _meta.When)
.Take(request.Count) .Take(request.Count)
.ToList(); .ToList();
} }
public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType) where T : IClientMeta public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType) where T : IClientMeta
{ {
IEnumerable<T> meta;
if (metaType == MetaType.Information) if (metaType == MetaType.Information)
{ {
var allMeta = new List<T>(); var allMeta = new List<T>();
foreach (var individualMetaRegistration in _metaActions[metaType]) var completedMeta = await Task.WhenAll(_metaActions[metaType].Select(async individualMetaRegistration =>
{ (IEnumerable<T>)await individualMetaRegistration(request)));
allMeta.AddRange(await individualMetaRegistration(request));
} allMeta.AddRange(completedMeta.SelectMany(meta => meta));
return ProcessInformationMeta(allMeta); return ProcessInformationMeta(allMeta);
} }
else var meta = await _metaActions[metaType][0](request) as IEnumerable<T>;
{
meta = await _metaActions[metaType][0](request) as IEnumerable<T>;
}
return meta; return meta;
} }

View File

@ -63,11 +63,12 @@ namespace IW4MAdmin.Application.Misc
/// discovers all the C# assembly plugins and commands /// discovers all the C# assembly plugins and commands
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public (IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations() public (IEnumerable<Type>, IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
{ {
string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}"; var pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
var pluginTypes = Enumerable.Empty<Type>(); var pluginTypes = Enumerable.Empty<Type>();
var commandTypes = Enumerable.Empty<Type>(); var commandTypes = Enumerable.Empty<Type>();
var configurationTypes = Enumerable.Empty<Type>();
if (Directory.Exists(pluginDir)) if (Directory.Exists(pluginDir))
{ {
@ -82,20 +83,55 @@ namespace IW4MAdmin.Application.Misc
.GroupBy(_assembly => _assembly.FullName).Select(_assembly => _assembly.OrderByDescending(_assembly => _assembly.GetName().Version).First()); .GroupBy(_assembly => _assembly.FullName).Select(_assembly => _assembly.OrderByDescending(_assembly => _assembly.GetName().Version).First());
pluginTypes = assemblies 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); .Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null);
_logger.LogDebug("Discovered {count} plugin implementations", pluginTypes.Count()); _logger.LogDebug("Discovered {count} plugin implementations", pluginTypes.Count());
commandTypes = assemblies 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)); .Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command));
_logger.LogDebug("Discovered {count} plugin commands", commandTypes.Count()); _logger.LogDebug("Discovered {count} plugin commands", commandTypes.Count());
configurationTypes = assemblies
.SelectMany(asm => {
try
{
return asm.GetTypes();
}
catch
{
return Enumerable.Empty<Type>();
}
})
.Where(asmType =>
asmType.IsClass && asmType.GetInterface(nameof(IBaseConfiguration), false) != null);
_logger.LogDebug("Discovered {count} configuration implementations", configurationTypes.Count());
} }
} }
return (pluginTypes, commandTypes); return (pluginTypes, commandTypes, configurationTypes);
} }
private IEnumerable<Assembly> GetRemoteAssemblies() private IEnumerable<Assembly> GetRemoteAssemblies()

View File

@ -4,6 +4,7 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models.Client;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -18,7 +19,7 @@ namespace IW4MAdmin.Application.Misc
private readonly Action<GameEvent> _executeAction; private readonly Action<GameEvent> _executeAction;
private readonly ILogger _logger; private readonly ILogger _logger;
public ScriptCommand(string name, string alias, string description, bool isTargetRequired, Permission permission, public ScriptCommand(string name, string alias, string description, bool isTargetRequired, EFClient.Permission permission,
CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout, ILogger<ScriptCommand> logger) CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout, ILogger<ScriptCommand> logger)
: base(config, layout) : base(config, layout)
{ {

View File

@ -118,7 +118,28 @@ namespace IW4MAdmin.Application.Misc
}) })
.CatchClrExceptions()); .CatchClrExceptions());
_scriptEngine.Execute(script); try
{
_scriptEngine.Execute(script);
}
catch (JavaScriptException ex)
{
_logger.LogError(ex,
"Encountered JavaScript runtime error while executing {methodName} for script plugin {plugin} at {@locationInfo}",
nameof(Initialize), _fileName, ex.Location);
throw new PluginException($"A JavaScript parsing error occured while initializing script plugin");
}
catch (Exception e)
{
_logger.LogError(e,
"Encountered unexpected error while running {methodName} for script plugin {plugin}",
nameof(Initialize), _fileName);
throw new PluginException($"An unexpected error occured while initialization script plugin");
}
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization); _scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver); _scriptEngine.SetValue("_serviceResolver", serviceResolver);
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject(); dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
@ -147,21 +168,26 @@ namespace IW4MAdmin.Application.Misc
} }
} }
await OnLoadAsync(manager);
try try
{ {
if (pluginObject.isParser) if (pluginObject.isParser)
{ {
await OnLoadAsync(manager);
IsParser = true; IsParser = true;
IEventParser eventParser = (IEventParser)_scriptEngine.GetValue("eventParser").ToObject(); var eventParser = (IEventParser)_scriptEngine.GetValue("eventParser").ToObject();
IRConParser rconParser = (IRConParser)_scriptEngine.GetValue("rconParser").ToObject(); var rconParser = (IRConParser)_scriptEngine.GetValue("rconParser").ToObject();
manager.AdditionalEventParsers.Add(eventParser); manager.AdditionalEventParsers.Add(eventParser);
manager.AdditionalRConParsers.Add(rconParser); 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) if (!firstRun)
{ {
@ -183,7 +209,7 @@ namespace IW4MAdmin.Application.Misc
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, _logger.LogError(ex,
"Encountered unexpected error while running {methodName} for script plugin {plugin} with event type {eventType}", "Encountered unexpected error while running {methodName} for script plugin {plugin}",
nameof(OnLoadAsync), _fileName); nameof(OnLoadAsync), _fileName);
throw new PluginException("An unexpected error occured while initializing script plugin"); throw new PluginException("An unexpected error occured while initializing script plugin");
@ -246,17 +272,17 @@ namespace IW4MAdmin.Application.Misc
} }
} }
public Task OnLoadAsync(IManager manager) public async Task OnLoadAsync(IManager manager)
{ {
_logger.LogDebug("OnLoad executing for {name}", Name); _logger.LogDebug("OnLoad executing for {name}", Name);
_scriptEngine.SetValue("_manager", manager); _scriptEngine.SetValue("_manager", manager);
return Task.FromResult(_scriptEngine.Execute("plugin.onLoadAsync(_manager)").GetCompletionValue()); await Task.FromResult(_scriptEngine.Execute("plugin.onLoadAsync(_manager)").GetCompletionValue());
} }
public Task OnTickAsync(Server S) public async Task OnTickAsync(Server S)
{ {
_scriptEngine.SetValue("_server", S); _scriptEngine.SetValue("_server", S);
return Task.FromResult(_scriptEngine.Execute("plugin.onTickAsync(_server)").GetCompletionValue()); await Task.FromResult(_scriptEngine.Execute("plugin.onTickAsync(_server)").GetCompletionValue());
} }
public async Task OnUnloadAsync() public async Task OnUnloadAsync()

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

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

View File

@ -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 namespace IW4MAdmin.Application.Misc
{ {
class TokenAuthentication : ITokenAuthentication internal class TokenAuthentication : ITokenAuthentication
{ {
private readonly ConcurrentDictionary<long, TokenState> _tokens; private readonly ConcurrentDictionary<long, TokenState> _tokens;
private readonly RNGCryptoServiceProvider _random; private readonly RandomNumberGenerator _random;
private readonly static TimeSpan _timeoutPeriod = new TimeSpan(0, 0, 120); private static readonly TimeSpan TimeoutPeriod = new TimeSpan(0, 0, 120);
private const short TOKEN_LENGTH = 4; private const short TokenLength = 4;
public TokenAuthentication() public TokenAuthentication()
{ {
_tokens = new ConcurrentDictionary<long, TokenState>(); _tokens = new ConcurrentDictionary<long, TokenState>();
_random = new RNGCryptoServiceProvider(); _random = RandomNumberGenerator.Create();
} }
public bool AuthorizeToken(long networkId, string token) 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) if (authorizeSuccessful)
{ {
_tokens.TryRemove(networkId, out TokenState _); _tokens.TryRemove(networkId, out _);
} }
return authorizeSuccessful; return authorizeSuccessful;
@ -34,15 +34,15 @@ namespace IW4MAdmin.Application.Misc
public TokenState GenerateNextToken(long networkId) public TokenState GenerateNextToken(long networkId)
{ {
TokenState state = null; TokenState state;
if (_tokens.ContainsKey(networkId)) if (_tokens.ContainsKey(networkId))
{ {
state = _tokens[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 else
@ -51,11 +51,11 @@ namespace IW4MAdmin.Application.Misc
} }
} }
state = new TokenState() state = new TokenState
{ {
NetworkId = networkId, NetworkId = networkId,
Token = _generateToken(), Token = _generateToken(),
TokenDuration = _timeoutPeriod TokenDuration = TimeoutPeriod
}; };
_tokens.TryAdd(networkId, state); _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 // perform some housekeeping so we don't have built up tokens if they're not ever used
foreach (var (key, value) in _tokens) 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; 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 // 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); 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); _random.GetBytes(charSet);
if (validCharacter((char)charSet[0])) if (ValidCharacter((char)charSet[0]))
{ {
token.Append((char)charSet[0]); token.Append((char)charSet[0]);
} }

View File

@ -8,11 +8,12 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace IW4MAdmin.Application.RconParsers namespace IW4MAdmin.Application.RConParsers
{ {
public class BaseRConParser : IRConParser public class BaseRConParser : IRConParser
{ {
@ -55,6 +56,7 @@ namespace IW4MAdmin.Application.RconParsers
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.AdditionalGroup, int.MaxValue);
Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *"; Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *";
Configuration.GametypeStatus.Pattern = ""; Configuration.GametypeStatus.Pattern = "";
@ -72,11 +74,13 @@ namespace IW4MAdmin.Application.RconParsers
public Game GameName { get; set; } = Game.COD; public Game GameName { get; set; } = Game.COD;
public bool CanGenerateLogPath { get; set; } = true; public bool CanGenerateLogPath { get; set; } = true;
public string Name { get; set; } = "Call of Duty"; public string Name { get; set; } = "Call of Duty";
public string RConEngine { get; set; } = "COD";
public bool IsOneLog { get; set; }
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command) public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command)
{ {
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command); var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
return response.Skip(1).ToArray(); return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
} }
public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default) public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default)
@ -127,7 +131,7 @@ namespace IW4MAdmin.Application.RconParsers
return new Dvar<T>() return new Dvar<T>()
{ {
Name = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarName]].Value, Name = dvarName,
Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)), Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)),
DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)), DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)),
LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)), LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)),
@ -135,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}", (object)response); _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) foreach (var line in response)
{ {
var regex = Regex.Match(line, Configuration.MapStatus.Pattern); var regex = Regex.Match(line, groupPattern);
if (regex.Success) if (regex.Success)
{ {
map = regex.Groups[Configuration.MapStatus.GroupMapping[ParserRegex.GroupType.RConStatusMap]].ToString(); value = regex.Groups[Configuration.MapStatus.GroupMapping[groupType]].ToString();
} }
} }
return map; if (value == null)
}
private string GameTypeFromStatus(string[] response)
{
if (string.IsNullOrWhiteSpace(Configuration.GametypeStatus.Pattern))
{ {
return null; return default;
} }
string gametype = null; if (typeof(T) == typeof(int?))
foreach (var line in response)
{ {
var regex = Regex.Match(line, Configuration.GametypeStatus.Pattern); return (T)Convert.ChangeType(int.Parse(value), Nullable.GetUnderlyingType(typeof(T)));
if (regex.Success)
{
gametype = regex.Groups[Configuration.GametypeStatus.GroupMapping[ParserRegex.GroupType.RConStatusGametype]].ToString();
}
} }
return gametype; return (T)Convert.ChangeType(value, typeof(T));
} }
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue) public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue)
{ {
string dvarString = (dvarValue is string str) string dvarString = (dvarValue is string str)
? $"{dvarName} \"{str}\"" ? $"{dvarName} \"{str}\""
: $"{dvarName} {dvarValue.ToString()}"; : $"{dvarName} {dvarValue}";
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0; return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0;
} }
@ -205,10 +211,21 @@ namespace IW4MAdmin.Application.RconParsers
if (match.Success) if (match.Success)
{ {
int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]); if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]] == "ZMBI")
int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]); {
_logger.LogDebug("Ignoring detected client {client} because they are zombie state", string.Join(",", match.Values));
continue;
}
var clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]);
var score = 0;
if (Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore] > 0)
{
score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]);
}
int ping = 999; var ping = 999;
// their state can be CNCT, ZMBI etc // their state can be CNCT, ZMBI etc
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3) if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3)
@ -217,14 +234,15 @@ namespace IW4MAdmin.Application.RconParsers
} }
long networkId; long networkId;
string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString; string networkIdString;
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
try try
{ {
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]]; networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkId = networkIdString.IsBotGuid() ? networkId = networkIdString.IsBotGuid() || (ip == null && ping == 999) ?
name.GenerateGuidFromString() : name.GenerateGuidFromString() :
networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle); networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
} }
@ -234,8 +252,6 @@ namespace IW4MAdmin.Application.RconParsers
continue; continue;
} }
int? ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
var client = new EFClient() var client = new EFClient()
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias()
@ -252,6 +268,17 @@ namespace IW4MAdmin.Application.RconParsers
client.SetAdditionalProperty("BotGuid", networkIdString); client.SetAdditionalProperty("BotGuid", networkIdString);
if (Configuration.Status.GroupMapping.ContainsKey(ParserRegex.GroupType.AdditionalGroup))
{
var additionalGroupIndex =
Configuration.Status.GroupMapping[ParserRegex.GroupType.AdditionalGroup];
if (match.Values.Length > additionalGroupIndex)
{
client.SetAdditionalProperty("ConnectionClientId", match.Values[additionalGroupIndex]);
}
}
StatusPlayers.Add(client); StatusPlayers.Add(client);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace Data.Abstractions
{
public interface IDataValueCache<TEntityType, TReturnType> where TEntityType : class
{
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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,49 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Data.Models.Client;
using Microsoft.EntityFrameworkCore;
namespace Data.Context
{
public static class ContextSeed
{
public static async Task Seed(IDatabaseContextFactory contextFactory, CancellationToken token)
{
await using var context = contextFactory.CreateContext();
var strategy = context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await context.Database.MigrateAsync(token);
});
if (!await context.AliasLinks.AnyAsync(token))
{
var link = new EFAliasLink();
context.Clients.Add(new EFClient
{
Active = false,
Connections = 0,
FirstConnection = DateTime.UtcNow,
LastConnection = DateTime.UtcNow,
Level = EFClient.Permission.Console,
Masked = true,
NetworkId = 0,
AliasLink = link,
CurrentAlias = new EFAlias
{
Link = link,
Active = true,
DateAdded = DateTime.UtcNow,
Name = "IW4MAdmin",
},
});
await context.SaveChangesAsync(token);
}
}
}
}

View File

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

27
Data/Data.csproj Normal file
View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<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>
<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="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

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

View File

@ -0,0 +1,118 @@
using System;
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<TEntityType, TReturnType> : IDataValueCache<TEntityType, TReturnType>
where TEntityType : class
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
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<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<TEntityType, TReturnType>> logger,
IDatabaseContextFactory contextFactory)
{
_logger = logger;
_contextFactory = contextFactory;
}
~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);
return;
}
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_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<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default)
{
if (!_cacheStates.ContainsKey(keyName))
{
throw new ArgumentException("No cache found for key {key}", keyName);
}
var state = _cacheStates[keyName];
// when auto refresh is off we want to check the expiration and value
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
if ((state.IsExpired || !state.IsSet) && !_autoRefresh || _autoRefresh && !state.IsSet)
{
await RunCacheUpdate(state, cancellationToken);
}
return state.Value;
}
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<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);
}
}
}
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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