Compare commits

..

287 Commits

Author SHA1 Message Date
b8d5495055 include client name in stats info result 2020-05-30 14:14:42 -05:00
fa79f4af73 fix issue with registering multiple script commands in command configuration 2020-05-30 14:06:04 -05:00
cad2952c46 [issue #140]
fix bug with friendly fire being disabled with custom callbacks on IW4x
2020-05-30 13:39:09 -05:00
43ac1218cc fix shared library linking issue 2020-05-25 14:09:41 -05:00
aef1ac6aae Merge pull request #141 from RaidMax/feature/issue-139-stats-api
[issue #139] client lookup and stats api
2020-05-25 13:06:44 -05:00
30f2f7bf09 [issue #139] client lookup and stats api 2020-05-25 13:04:44 -05:00
4457ee5461 Merge pull request #138 from RaidMax/feature/issue-137-custom-hostname
[issue 137] custom display hostnames for webfront
2020-05-23 13:26:07 -05:00
e91c60a753 [issue 137] custom display hostnames for webfront 2020-05-23 13:25:09 -05:00
1241ac459e re-enable claims permission add/remove 2020-05-22 21:38:38 -05:00
4afd1f3cdc Merge pull request #136 from RaidMax/feature/issue-135-enhanced-search
[issue 135] enhanced search
2020-05-22 20:35:42 -05:00
5042ea6c91 [issue 135] enhanced search
implement enhanced search for chat messages
2020-05-22 20:29:41 -05:00
bef5ffbd35 update IW5 parser 2020-05-19 11:01:08 -05:00
19f5f557bd update readme / upgrade game log server packages to work with latest python release 2020-05-18 21:03:40 -05:00
6aa6af526a fix issue with counting plugin tasks causing them to be executed. why ms? 2020-05-17 17:01:13 -05:00
0cabf6f8a3 only fix double forward slash characters (instead of single) when sending messages
retry kicks on banned players if they're banned from webfront, but don't actually get kicked because the game doesn't process the command (looking at you T6)
allow capturing chat messages for names spoofed to an empty string
make sure mostkills uses days not month for cutoff
2020-05-16 20:55:18 -05:00
d3d1f31ee0 bugfixes/enhancements
prevent users from trying to set the console's level to owner
fix issue with setting multiple owners
update/improve unit tests
2020-05-16 11:54:01 -05:00
420e0d5ab5 Merge pull request #133 from RaidMax/feature/issue-132-script-command-registration
implement script plugin command registration - issue #132
2020-05-11 16:21:33 -05:00
2bd895e99d implement script plugin command registration - issue #132 2020-05-11 16:20:25 -05:00
44cacc1741 Merge pull request #130 from RaidMax/feature/issue-126-most-kills-command
[issue #129]
2020-05-05 18:50:35 -05:00
aff19b9577 [issue #129]
Add most kills command/macro
sneaky fix for tekno parser
2020-05-05 18:49:30 -05:00
267e0b8cbe [tweaks and fixes]
reenable tekno support
address vagrant thread issue
refactor game log reader creation to follow better practices
fix bot issues/address how guids are generated for bots/none provided
2020-05-04 16:50:02 -05:00
b49592d666 fix latent issue with password login due to not retreiving password/salt
set semaphore count properly for event execution throttling
2020-04-29 17:05:36 -05:00
c82139b88c small tweak to hopefully prevent too many events executing simultaneously 2020-04-29 16:27:24 -05:00
33712f3d7d update shared library nuget version 2020-04-28 18:19:46 -05:00
9dfdf5a82b remove debug output for ef 2020-04-28 17:54:06 -05:00
f5b0167f81 Merge pull request #128 from RaidMax/feature/issue-126-create-run-as-command
Feature/issue 126 create run as command
2020-04-28 16:49:40 -05:00
7715113b56 implement audit log view in webfront 2020-04-28 16:48:06 -05:00
58bfd189d0 [issue #126]
implement basic run-as functionality
2020-04-26 21:12:49 -05:00
3645cf53ff update default profanity filters to have something a little more usable 2020-04-26 15:57:51 -05:00
8a98ed7c50 small tweak for preconnect events 2020-04-26 12:32:41 -05:00
5529858edd [misc bug fixes]
properly hide broadcast failure messages if ignore connection lost is turned on
fix concurent issue for update stats history that happened with new event processing
make get/set additional property thread safe
add ellipse to truncated chat messages on home
2020-04-25 19:01:26 -05:00
ff011be8a6 unmeme the build 2020-04-22 21:08:25 -05:00
b41c4c6245 include some of the changes meant for previous build 2020-04-22 20:51:04 -05:00
92a26600af actually fix the session score concurrency issue
fix rare bug with shared guid kicker plugin
allow hiding of the connection lost notification
2020-04-22 18:46:41 -05:00
9e74dac5ed fix stat issue with concurrent threads
fix potential lost penalty if server does not response to kick request
make sure that broadcast only shows one custom say name
add unit tests
2020-04-21 17:34:00 -05:00
3ae2e42718 properly implement sv_sayName for custom say name
prevent trying to register live radar page for every server (oops)
optimize event processing to prevent slow plugins from affecting command processing
enable database connection resilency
trim extra characters from T7 reassembled response
2020-04-20 10:45:58 -05:00
0b643b2099 unmeme a dvar check 2020-04-18 17:48:49 -05:00
ee087f1c85 fix T7 extra null bytes in status response
fix regression bug with info response on T6
2020-04-18 10:46:55 -05:00
8c29027b3f partial T7 (BO3) support. includes rcon communication improvements and a small fix for displaying live radar tab 2020-04-17 15:05:16 -05:00
5bc1ad5926 fix regression issue with wine drive name mangling 2020-04-14 15:46:14 -05:00
c376266090 Merge pull request #123 from RaidMax/feature/issue-77-allow-server-type-categorization
add server categorization feature (issue #77)
2020-04-13 20:27:14 -05:00
8539223a15 add server categorization feature (issue #77) 2020-04-13 20:26:13 -05:00
b188e36786 update for new pluto iw5 rcon response 2020-04-13 19:43:24 -05:00
fca47cbce0 fix regression issue with log paths oops 2020-04-13 18:15:46 -05:00
be8041b868 refactor and test log path generation to support pluto IW5 better 2020-04-13 16:16:31 -05:00
b63d2995ed allow auto log filepath generation for pluto iw5 2020-04-12 20:48:03 -05:00
8fb2394130 support for Plutonium IW5 and only show live radar tab if monitoring at least one IW4 serves 2020-04-11 18:05:18 -05:00
36af673fc7 add ability to register custom event generators for event parsers / truncate long client names fix 2020-04-04 12:40:23 -05:00
9fdf4bad9c fix for runaway regular expression on linux
explicitly set string dvars in quotes to allow setting empty dvars
allow piping in input from command line (#114)
update the distribution for top stats elo
prevent game log file rotation from stopping event parsing
2020-04-01 14:11:56 -05:00
02a784ad09 allow prompt string to have an empty/default value
upgrade some project dependencies
don't try to run events on parsers
update top players rank distribution
2020-02-17 10:05:31 -06:00
2e5ffe91fc fix a small bug with new line truncation missing 2020-02-12 15:11:43 -06:00
68490bde57 Merge pull request #113 from RaidMax/enhancement/issue-112-toggle-automated-penalties-webfront
allow toggle of automated penalties display on the webfront
2020-02-12 13:16:11 -06:00
f430dab3a7 allow toggle of automated penalties display on the webfront
issue #112
fix small issue with script plugin loading
2020-02-12 13:13:59 -06:00
c3c21a7749 refactor a good bit of stuff for better dependency injection
fix regular expression for T6 log parsing
2020-02-11 16:44:06 -06:00
ec053eb854 Merge pull request #111 from RaidMax/enhancement/issue-108-address-t6-specific-behaviors
re-kick working as expected now
2020-02-07 11:16:27 -06:00
33494197e3 re-kick working as expected now 2020-02-07 11:15:21 -06:00
239ca30fd1 Merge pull request #110 from RaidMax/enhancement/issue-108-address-t6-specific-behaviors
fix disconnect event being cancelled
2020-02-06 21:06:38 -06:00
1dd88cdacb fix disconnect event being cancelled 2020-02-06 21:05:50 -06:00
f0f9a6beda Merge pull request #109 from RaidMax/enhancement/issue-108-address-t6-specific-behaviors
Use game time from log to ignore potential false disconnect lines - F…
2020-02-06 18:36:16 -06:00
fe380ca331 Use game time from log to ignore potential false disconnect lines - Fix for latent linking issues with multiple ips - Anticheat fix for T6 - retry kick on update if they're not allowed to connect 2020-02-06 18:35:30 -06:00
15e2170100 just a small fix that I forgot to include in the last build. 2020-02-03 08:21:42 -06:00
2872d02c37 fix plugin error spam with multi-servers 2020-02-02 16:21:34 -06:00
60ff33834e make sure we have an empty command config during initial startup, oops. 2020-02-01 13:28:05 -06:00
06cdaef8a4 allow Kekno to run with sv_running not returning anything :upside_down:
make sure script plugins output correct errors instead of being swallowed
prevent webfront error when webfront tab is left open on a server no longer being modified
2020-02-01 12:27:14 -06:00
c6d6bebeab Merge pull request #107 from RaidMax/feature/issue-104-allow-per-command-permission-config
Feature/issue 104 allow per command permission config
2020-01-31 20:23:37 -06:00
31c259f966 merge dev changes 2020-01-31 20:22:59 -06:00
318a23ae5b Finish implementation of configuable command permissions 2020-01-31 20:15:07 -06:00
11ae91281f start work to allow customizing command properties via configuration 2020-01-26 18:06:50 -06:00
1fd31beb05 fix another meme 2020-01-26 15:40:00 -06:00
116c909c2d Merge pull request #106 from RaidMax/bugfix/issue-0-misc-small-fixes
fix nuget package version for scriptcommands
2020-01-26 14:09:40 -06:00
451072276d fix nuget package version for scriptcommands
fix only one server being added during setup
2020-01-26 14:08:53 -06:00
e6bdcc9012 fixed server parser setup bug I was retarded about 2020-01-24 08:57:20 -06:00
9e345752f2 update parser selection menu text during setup
update IW4 script commands gsc and plugin to give base example
fix issue with new account alias linking (I think)
2020-01-21 18:08:18 -06:00
23f4e14244 Merge pull request #102 from RaidMax/bugfix/issue-0-misc-small-fixes
small change to detemine valid anticheat log lines
2020-01-20 11:57:57 -06:00
a53c2f5c44 small change to detemine valid anticheat log lines
fix new configuration generation bug as result of last pr
2020-01-20 11:49:56 -06:00
ad64540bb6 Merge pull request #101 from RaidMax/bugfix/issue-0-misc-small-fixes
web configuration changes for issue #100
2020-01-20 10:25:25 -06:00
697a752be0 make the version name match the actual name for FTP deployment
fix rare issue with summing session scores
copy font to expected wwwroot dir in debug mode so we get pretty icons when developing
upgrade some packages

pretty much reworked the entire server web config to support better validation and stuff.. not really a small fix

finish web configuration changes (I think)

finish up configuration changes and update shared library nuget
2020-01-20 10:23:23 -06:00
3a1cfba251 Merge pull request #98 from RaidMax/bugfix/issue-0-misc-small-fixes
fix issue with PT6 guid parsing in log file
2020-01-15 18:45:12 -06:00
7e3f632399 fix issue with PT6 guid parsing in log file 2020-01-15 18:43:52 -06:00
fa8dbe7988 finish version file upload (I think) 2020-01-15 15:41:09 -06:00
bdeb5b2408 let's actually look in the right folder for the version file 2020-01-15 15:25:34 -06:00
191bde9d1c test updating website version file from ftp upload 2020-01-15 15:06:05 -06:00
8afdb6df6f remove detailed version from csproj to hopefully force compile time set 2020-01-15 14:32:57 -06:00
a078da2715 round 2 of version injection 2020-01-15 14:06:29 -06:00
1c287ee354 testing new way to read in version hopefully injects now 2020-01-15 14:00:18 -06:00
2ae8fd6e5b test injecting build number 2020-01-15 13:32:01 -06:00
68deaec081 maybe let's not trigger a build now? 🤔 2020-01-15 10:21:10 -06:00
e737d990e9 update pipeline to only build on merge into release branches or (now previous standard 2.4-pr) 2020-01-15 10:18:02 -06:00
01198b66ea Merge pull request #97 from RaidMax/bugfix/issue-95-fix-restart-command
Bugfix/issue 95 fix restart command
2020-01-14 18:59:22 -06:00
943808562f fix error code page for things over than 404s
allow request token when not logged in
2020-01-14 18:56:23 -06:00
ec994d51be fix restart command (thanks .net upgrade)
reworking a little bit of stuff to allow depedency injection to start creeping in... it's coming
2020-01-13 20:06:57 -06:00
9be7bafc53 Merge pull request #96 from RaidMax/enhancement/issue-91-check-sv-running
implement feedback from issue #91 when sv_running is off
2020-01-13 16:54:38 -06:00
cd387ca08b implement feedback from issue #91 when sv_running is off
clean up a trying to abort thread which doesn't actually work with .net core
don't log event tasks cancelled as error, because it's not
2020-01-13 16:51:16 -06:00
639db5d7eb test powershell master version update
don't upgrade for this build
2020-01-12 16:46:39 -06:00
e64e02342e test master version update
don't bother updating for this build
2020-01-12 16:30:34 -06:00
51f91ede2c testing version push to master
no reason to update for this build.
2020-01-12 16:21:31 -06:00
009da92285 Merge pull request #94 from RaidMax/enhancement/update-client-master-versioning-and-include-userraw
make sure we force copy as directory
2020-01-11 21:03:45 -06:00
780b3459af make sure we force copy as directory 2020-01-11 21:01:51 -06:00
6acf1c67c1 Merge pull request #93 from RaidMax/enhancement/update-client-master-versioning-and-include-userraw
lets see if the game scripts copy now
2020-01-11 20:51:50 -06:00
27ad4fca43 lets see if the game scripts copy now 2020-01-11 20:50:46 -06:00
dd0d7192eb Merge pull request #92 from RaidMax/enhancement/update-client-master-versioning-and-include-userraw
updates to support new master versioning
2020-01-11 20:33:52 -06:00
8a42239f36 updates to support new master versioning
make sure game files are copied correctly in build output
2020-01-11 20:32:27 -06:00
23c78997ac Fix anticheat issue with needing index casting. IW you seem a little sloppy there... 2020-01-06 18:43:00 -06:00
7cdfe618a2 Add missing active columns with migration 2020-01-06 11:04:36 -06:00
7bfadca84d fix issue accessing the wrong logger 2019-12-29 17:07:00 -06:00
a7620ffd50 Merge pull request #90 from RaidMax/feature/webfront-help-index
Feature/webfront help index
2019-12-29 11:36:36 -06:00
902cd9953e finish help layout and show only on permission level 2019-12-29 11:32:36 -06:00
344c3613b8 add help page to layout 2019-12-28 20:44:39 -06:00
32e1af0ffb fix live radar directory and a few minimap names 2019-12-28 17:53:41 -06:00
1e1a03c9d8 fix retarded method that wasn't returning when it should have 2019-12-28 15:40:55 -06:00
4d3f7da48e re-enable API events 2019-12-28 10:07:37 -06:00
2b26b9a707 fix javascript libraries not being loaded because the stupid CDN change. MICROSOFT, IF YOU'RE LISTENING, FIX YOUR INTEGRATION WITH CDNJS THANK YOU
force demo record and increase max demos saved for IW4x
fix issue with disconnect on a not fully connected client
2019-12-27 20:37:50 -06:00
82381457df fix duplicate aliases from color codes (AB#5) 2019-12-27 14:42:17 -06:00
d4b5120953 Update azure-pipelines.yml for Azure Pipelines 2019-12-27 14:01:00 -06:00
82152755c9 add github release to pipeline 2019-12-27 13:45:53 -06:00
b251ef00c4 potential fix for a invalid operation exception on client update
change client library cdn provider as cdnjs seems broken at the moment
2019-12-27 12:10:20 -06:00
042fde971e (potentially) fixed object disposed issue with semaphore
fix random issue where we were trying to reset a session for a player that has not fully connected
2019-12-26 18:17:49 -06:00
c9e6ce0bca fix authorize issue on penalty info after upgrading .NET Core runtime targets 2019-12-25 21:05:57 -06:00
c4df53c195 fix issue with script plugins not reloading (AB#2)
fix issues with collation on MySQL (AB#1)
2019-12-25 14:32:57 -06:00
3a06b3862d Update projects to .NET Core 3.0
Increase max sv_hostname length on master
2019-12-24 15:23:43 -06:00
9fa5de1418 woo it works, now we have a reasonable output filename 2019-12-09 14:17:12 -06:00
a9e7d2d314 Maybe this will work better 2019-12-09 14:08:52 -06:00
b3f4712dd2 update the json output encoding 2019-12-09 14:01:02 -06:00
e5d009d87d merge from 2.3 2019-12-09 13:30:50 -06:00
70ca202889 Test json generation of version info 2019-12-09 13:28:20 -06:00
b8aa8a0b4d reeee 2019-12-07 20:42:30 -06:00
1f755f535c I'm retarded 2019-12-07 20:28:50 -06:00
f37e954e2f third time's the charm? 2019-12-07 20:20:44 -06:00
31bcd52c79 lets try again 2019-12-07 20:11:22 -06:00
72f3a51657 run version grab after publish 2019-12-07 20:00:03 -06:00
3b4af20810 grab version information from file to setup output zip name 2019-12-07 19:52:03 -06:00
890c419133 Fix color code tag helper not being loaded 2019-12-07 10:49:40 -06:00
f9680971af Update to build solution instead of individual projects 2019-12-05 17:09:35 -06:00
5df9332d4c Update azure-pipelines.yml for Azure Pipelines
Make open-iconic directory if not exists (for real)
2019-12-05 16:32:23 -06:00
4379d04b00 Update azure-pipelines.yml for Azure Pipelines
Make open-iconic directory if not exists
2019-12-05 16:31:40 -06:00
9d639097d3 Merge pull request #86 from RaidMax/dependabot/pip/Master/werkzeug-0.15.3
Bump werkzeug from 0.14.1 to 0.15.3 in /Master
2019-12-05 16:20:28 -06:00
cade2242bf Merge pull request #87 from xerxes-at/2.3
Fixed the PT6 parser
2019-12-05 16:20:10 -06:00
1d7377f975 Fixed the PT6 parser
Reworked most of the regex.
Fixed the mapping.
This hopefully fixes all issues with it.
2019-12-04 20:08:01 +01:00
e7395f02ce Bump werkzeug from 0.14.1 to 0.15.3 in /Master
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 0.14.1 to 0.15.3.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/0.14.1...0.15.3)

Signed-off-by: dependabot[bot] <support@github.com>
2019-12-04 02:00:44 +00:00
c9e2a11745 Update azure-pipelines.yml for Azure Pipelines 2019-12-03 16:28:51 -06:00
9db38a130c fix stat controller build plugins in correct mode 2019-12-03 16:27:26 -06:00
25a69a2018 don't use temporary table on mysql migration as it breaks 2019-12-03 15:56:00 -06:00
98c4a700a2 merge 2019-12-02 15:56:30 -06:00
3defd3f486 move all the deployment setup into 2.4 pr (#85)
* don't run build commands in release

* fix test file

* Set up CI with Azure Pipelines

[skip ci]

* Include fonts and fix automessage hidden command

* more project changes

* migration from bower to libman

* more lib man changes

* project update for sneaky commands

* add missing canvas.js dep
update projects not to have stupid extra dlls

include in previous

* update pipeline file

* update post publish script and pipeline definition

* fix broken yaml

* move encoding conversion to seperate script

* remove extra uneeded rank icons
remove garbage language files being created
remove frontend lib when done

* fix publish script path

* grab localizations through powershell

* fix broken batch 🤷

* actually fixed

* only include runtime compilation in debug mode for webfront

* don't deploy un minified css
use full jquery version

* add step to download the scss for open iconic
change the font path

* update mkdir for iconic path

* don't include old iconic css

* correct font path for real now

* copy script plugins

* lots of changes for deployment

* build the projects

* use projectdir instead of solution dir

* nerf script commands plugin
fix live radar left over command

* actually kill script command post build

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* fix the font file copy (I think)

* maybe fix delete folder issue

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines

* Update azure-pipelines.yml for Azure Pipelines
2019-12-02 15:52:36 -06:00
b086190ab0 renable weapon name in anticheat snapshot list
update migrations for unique index
fix missing total connection time
include total connection time in get client query
2019-11-25 12:05:12 -06:00
56008e80c7 update mapname from status query 2019-11-18 14:02:35 -06:00
0ac1a4f861 Merge branch '2.3' into 2.4-pr 2019-11-18 13:07:24 -06:00
fb6d20e214 fix regex pattern for PT6 2019-11-18 12:29:37 -06:00
a9f6106c6e fix silly mistake with trying to assign something to an object that could be null 2019-11-18 09:25:39 -06:00
d1886fdd20 Fix small issue with query optimization missing a FK set
Fix accidentally rename of controller method
2019-11-18 08:08:09 -06:00
161b27e2f2 fix alias command sending message to origin instead of target
(hopefully) fix an issue with banned players causing exception if they create events before they are kicked out
fix issues with sometimes wrong error message for timeout
show most recent IP address at top of alias list
optimization to some sql queries
2019-11-15 14:50:20 -06:00
cb9119ac58 add more informative 404 errors 2019-10-23 13:35:20 -05:00
f31ce6b001 allow enabling of only specific detection types
allow override of anticheat for tmw3
fix invalid cast if E.Extra is not a command
add a delay after map rotation before getting the the server info. (hopefully prevents increased lost connection notification frequency)
2019-10-23 10:40:24 -05:00
96e434213f refactor some event handling
add concept of blocking events
2019-10-18 13:39:21 -05:00
b992f4d910 add unlink command
fix parsing names with colors codes enabled
2019-10-11 15:26:13 -05:00
6b27beb355 update mysql provider to pre release so it works with .net core 3.0 2019-10-10 13:15:31 -05:00
4872e2e8c4 fix issue with top stats query and client evaluation 2019-10-09 18:51:50 -05:00
a37524c726 fix small exit exceptions
fix the live radar tab switching for .net core 3.0
change events to use "sequential" but still parallel
update the publish scripts
2019-10-09 15:51:02 -05:00
6cd3879bac Merge 2019-10-08 17:02:22 -05:00
c630f65317 update the project files even though the culprit was actually the publish file 2019-10-08 16:47:36 -05:00
76cfe30c0f update version number 2019-10-07 17:39:17 -05:00
a7872aaffd ensure that demoted clients are logged out from the webfront 2019-10-07 17:35:37 -05:00
4635d85ff8 forgot an else in a migration 2019-10-07 10:43:44 -05:00
068e943fd3 update values for snap and offset
fix some issues from .NET Core 3.0 upgrade
2019-10-07 10:26:07 -05:00
c4e0b0272c update packages 2019-10-02 18:58:23 -05:00
ede5c9de51 update recently clients to show last 24 hours
fix color codes on profile meta data
2019-10-02 17:59:10 -05:00
524589717b Update to .NET Core 3.0 2019-09-30 18:35:36 -05:00
88af032736 Update shared GUIDs 2019-09-30 13:00:44 -05:00
260a8800a4 prevent raw html when color codes are enabled 2019-09-28 19:13:30 -05:00
37261c9a54 update some anticheat code 2019-09-27 15:53:52 -05:00
5073ec39bf Merge branch '2.3' into 2.4-pr 2019-09-27 15:51:57 -05:00
3af9f55bf1 Fix ordering of admins by level, then name 2019-09-27 15:49:03 -05:00
f7cbf73c44 Merge branch '2.3' into 2.4-pr 2019-09-26 16:11:58 -05:00
1e9a87d6fa prevent penalties from being lost in edge case alias linkage
small optimization with tasks
2019-09-26 16:08:49 -05:00
fe6fe39800 don't group admin list by alias id 2019-09-14 17:22:47 -05:00
082776aca5 prevent "laggy" angles from being tracked 2019-09-10 17:50:23 -05:00
483b7917ac Merge branch '2.3' into 2.4-pr 2019-09-10 17:26:48 -05:00
7f7353c505 only count hits for valid recoil detection 2019-09-10 17:24:32 -05:00
adc73eb7ff merge from 2.3 2019-09-09 17:41:58 -05:00
c18be20899 add snap metric to anticheat
update various small code bits
2019-09-09 17:40:04 -05:00
198f596ab3 small updates to stat handling
various little tweaks
2019-09-09 17:37:57 -05:00
148d28eaca Merge branch '2.3' into 2.4-pr 2019-08-30 17:52:35 -05:00
58a73e581f fix rare issues when converting encodings
add readme for AC
2019-08-30 17:24:44 -05:00
2d9b6b8394 prevent privileged client from being flagged when reported
fix issue with enum parsing on finding client
2019-08-30 13:31:23 -05:00
aa9dd7ac6d Merge branch '2.3' into 2.4-pr 2019-08-30 11:50:48 -05:00
47d5df1aa1 bump application version 2019-08-30 09:29:19 -05:00
d644387091 Strip out color codes and spaces when checking for min length 2019-08-30 09:26:16 -05:00
db3a20c60b merge from 2.3 2019-08-28 13:47:38 -05:00
27a05ce6db update api controller to support actually filtering events by server
fix up stats manager async semaphore wait
add new shared guids
fix regex parsing with empty name
2019-08-28 13:45:53 -05:00
7c0e37cc8e Merge branch '2.3' into 2.4-pr 2019-08-24 20:16:35 -05:00
11d2df1fe8 small stat changes 2019-08-24 20:15:50 -05:00
a820929582 another fix because I'm retarded
bump version up
2019-08-24 14:06:23 -05:00
dcd1c97d37 Merge branch '2.3' into 2.4-pr 2019-08-24 11:10:43 -05:00
6726217354 Make stats update after 10 kills so we don't wait quite as long
Gracefully disconnect clients on shutting down again
2019-08-24 10:02:53 -05:00
c1a825f8f2 Merge branch '2.3' into 2.4-pr 2019-08-23 21:28:09 -05:00
563c73221e actually fix it here 2019-08-23 21:27:36 -05:00
d35001049f Merge branch '2.3' into 2.4-pr 2019-08-23 19:12:28 -05:00
652f3fb86b Fix small issue with saving client kills multithreaded 2019-08-23 19:11:36 -05:00
f877ba73a9 Merge branch '2.3' into 2.4-pr 2019-08-23 18:36:28 -05:00
91078eec0f Update to some stat stuff to fix some latent issues 2019-08-23 18:34:31 -05:00
a57c982270 Merge branch '2.3' into 2.4-pr 2019-08-18 11:20:31 -05:00
ed5d8faf5c tweak for showing the generated graph color properly in other browsers.
apparently the "style" hack doesn't work, but using "title" does
remove return in customcallbacks
2019-08-18 11:20:19 -05:00
f6857ac635 bugfix for issue #81 (linked accounts being demoted) 2019-08-18 11:18:20 -05:00
320b01d15c Merge branch '2.3' into 2.4-pr 2019-08-12 20:09:31 -05:00
001ecc5961 prevent flagging banned players 2019-08-12 20:00:40 -05:00
4bdd240122 update callback
set player color history to the correct darken amount
2019-08-12 19:04:25 -05:00
5fef69d697 slight tweak to log reader to expire old keys 2019-08-10 17:58:20 -05:00
8fc85ef4c1 have graph color generated by css so that MS Edge doesn't freak out when using rgba
don't do simple word check on offensive name
2019-08-10 17:35:34 -05:00
85d88815f1 top stats info is per server instead of total when selecting each tab
fix issue with ingame name failing to match when using color codes
only show live radar for servers that support it
2019-08-10 09:08:26 -05:00
a0266c5e69 Merge branch '2.3' into 2.4-pr 2019-08-08 15:59:00 -05:00
2ba0b1e7d3 prevent same level clients from demoting each other 2019-08-08 15:58:23 -05:00
3051d44b0d show trigger regex for profanity determent plugin 2019-08-08 15:30:06 -05:00
b8a310bb07 prevent flag icon from showing on banned profiles
implement automated penalty info for profanity determent issue #75
2019-08-06 13:36:37 -05:00
d11a5f862b add missing dragunov to the live radar weapons
color code process names in chat context
2019-08-04 21:25:56 -05:00
08d250156c fix login issue
strip colors for logging
feature implementation for issue #76
2019-08-04 20:38:55 -05:00
75378400e7 Add flag icon on client profile 2019-08-04 17:06:07 -05:00
bb42861a92 finish color code support (I think) 2019-08-02 18:04:34 -05:00
dfecb99d07 Merge branch '2.3' into 2.4-pr 2019-08-01 19:45:44 -05:00
1c66ac9117 fix issue with log reader
fix issue with searching names on webfront that could be parsed to hex
2019-08-01 19:42:44 -05:00
55fb36863c fix copy paste error in penalty loader
start allowing color codes from ingame
2019-08-01 09:37:33 -05:00
034d887abd modify how reading from file works to prevent accidental overreads 2019-07-31 20:15:29 -05:00
9f3f344daa add a bit more logged for when live radar fail to update
update killhouse map offsets (it's still wrong though)
2019-07-29 12:08:25 -05:00
ebe85a9ded Merge branch '2.3' into 2.4-pr 2019-07-27 17:50:25 -05:00
06af995202 prevent certain shotguns, and shotgun attachments from being used for no recoil detection 2019-07-27 17:46:48 -05:00
92e71ae2f4 finish custom accent color feature 2019-07-27 15:23:45 -05:00
f8505781a0 fix issue with teknomw3 GUIDs 2019-07-27 10:02:46 -05:00
3b9b99a07e start work to allow custom accent colors 2019-07-27 08:18:49 -05:00
ab4ce41015 Merge 2.3 into 2.4-pr 2019-07-26 10:25:05 -05:00
f613f0aace finish tweaks to log reader
add some more shared guids
2019-07-26 10:22:02 -05:00
2b8d8fc4b7 Merge 2.3 into 2.4-pr 2019-07-25 10:01:20 -05:00
ac32034910 optimize index for rating history
update log server to prevent delays or missed information
2019-07-24 19:15:07 -05:00
9665d2d457 fix issue with duplicate js function names for loader
hide flagged status of users on webfront unless logged in (will still show the level if they report someone because cba to update the view component w/out auth status)
add terminal to the radar maps
2019-07-24 10:36:37 -05:00
d73d68d9f4 increase master history to 7 day, up from 1 day 2019-07-21 17:14:44 -05:00
50ba71c6fb small code cleanups 2019-07-19 14:54:39 -05:00
38f1169061 finished server selection for live radar and adding it as button to home screen
only update flag for recent players if country code is available
2019-07-19 10:33:00 -05:00
5c90228320 Move folder structure for radar plugin 2019-07-17 13:26:48 -05:00
03db194046 Add unstaged files 2019-07-17 13:16:45 -05:00
68382d3f61 Remove double track images 2019-07-17 13:16:25 -05:00
4e99046874 merge 2019-07-17 13:09:25 -05:00
64b320614b add images for radar to source control
cleanup some nuget packages
2019-07-17 13:00:30 -05:00
7b5f3e8e83 move some stuff for live radar for compiled views
add chat icon to send messages to servers on server view
2019-07-17 12:38:02 -05:00
748841776f More radar tweaks 2019-07-17 12:38:02 -05:00
edfbb92a3f can you say more radar updates? 2019-07-17 12:38:01 -05:00
1a9a0e48b7 lots more live radar updates 2019-07-17 12:38:00 -05:00
d27f1ded36 tweak initial live radar 2019-07-17 12:38:00 -05:00
e5cd824c99 start work for live radar 2019-07-17 12:37:24 -05:00
2542b7de12 Clean up some old files 2019-07-17 12:29:51 -05:00
f42a66e756 add most recent players dropdown option to webfront
remove unneeded compiled bootstrap file
2019-07-16 15:27:19 -05:00
d301915273 Merge 2019-07-13 20:50:10 -05:00
fc43e47874 move some stuff for live radar for compiled views
add chat icon to send messages to servers on server view
2019-07-13 20:45:25 -05:00
b0365a5a43 More radar tweaks 2019-07-08 20:21:20 -05:00
2a63a55359 can you say more radar updates? 2019-07-08 20:21:19 -05:00
0e9fd144f1 lots more live radar updates 2019-07-08 20:21:18 -05:00
d81646087e tweak initial live radar 2019-07-08 20:21:17 -05:00
7f1da4d1fc start work for live radar 2019-07-08 20:21:17 -05:00
68f6be23a6 require minimum kills before recoil threshold evaluated
make sure edit configuration link on webfront visible when accessing via localhost
2019-07-07 17:57:06 -05:00
0b282b2664 lots more live radar updates 2019-07-05 20:53:03 -05:00
665218f641 tweak initial live radar 2019-07-02 17:30:05 -05:00
b64bce2936 start work for live radar 2019-06-30 13:39:40 -05:00
042327840f fix bug with wrong locale when master is down
fix issue with reapplying penalties
show subset of penalties that are linked on client profile
2019-06-28 16:57:01 -05:00
3d468e32b9 clean up some penalty stuff
force log file to be written if none supplied
fix issue with not all meta loading
2019-06-27 20:06:30 -05:00
16d2ec82b8 make sure flags are excluded from active penalties on player profile
modify how flags "expire"
2019-06-25 18:01:47 -05:00
421e90cf70 fix old bug of auto unflag not working
fix wrong thresholds on recoil
2019-06-24 18:32:14 -05:00
8119ff9f83 adjust detection thresholds for recoil and offset
make sure we don't keep adding penalties after first
add "Other" penalty for future plugin use
2019-06-24 16:56:47 -05:00
253c7c8721 allow reports to be filed against anyone
fix rare issue with alias (maybe)
update some tests
2019-06-24 11:01:34 -05:00
cb80def122 update version
make sure ac snapshots are saved
2019-06-16 14:49:04 -05:00
e669d0be82 don't count bots on master list
don't save every ac snapshot oops..
2019-06-16 12:19:23 -05:00
495197c19d add no recoil detection 2019-06-15 17:37:43 -05:00
a5414c2c57 Merge branch '2.3' of https://github.com/RaidMax/IW4M-Admin into 2.3 2019-06-15 08:53:15 -05:00
cbfb3919fc fix GUID parsing on T6 2019-06-15 08:52:59 -05:00
d789542d0f Update README.md 2019-06-14 18:48:14 -05:00
c6c2ec7784 fix start scripts on linux (dos2unix)
fix permissions on linux (why were/are they carrying over from windows? )
2019-06-14 18:16:47 -05:00
4645bd84e8 prevent partial client updates from setting things they shouldn't be *cough* mask *cough*
setup shared library for NuGet package
fix a couple things with offset detection calc
get cod4x working again
2019-06-13 19:10:08 -05:00
10829b32ad update anti-cheat offset calculation 2019-06-12 10:27:15 -05:00
e86904b11e add a check to make sure we're not breaking EFClient entries when updating
make sure the alias is updated before banning the player as we want to link them together
update CoD4x parser to fix their breaking change
2019-06-11 08:00:14 -05:00
82390340c9 fix duplicate meta data when restarting
fix issue with parsing anticheat info in non en-US culture
fix rare issue with client spots "swapping"
don't copy referenced shared library assemeblies from plugins
2019-06-09 09:50:58 -05:00
163523d586 convert GetPort to auto property
don't force disconnect player if someone is "in" their spot
increase gamelogserver max time before purge
2019-05-31 10:17:01 -05:00
95d64df321 combined Penalty and EFPenalty
moved some classes around
2019-05-29 16:55:35 -05:00
0b0290a871 fix issue with restarting via web
replace some hard coded string in javascript with localization
break things to fix things
2019-05-17 09:02:09 -05:00
5f588bb0f7 clean up the profanity determent plugin by using the Get/Set Additional properties
cleaned up the base event parser to not need the server object to generate the event
Hopefully prevent anticheat from freaking out when database connection is lost
2019-05-13 10:36:11 -05:00
b99cc424e7 fixes for things that should have been in the previous release
console output reenabled
server start task actually runs
2019-05-09 20:00:00 -05:00
1dc0f5a240 fix aggregate issue with KDR on global top stats
refactor some of the main application code to have a cleaner code flow
add enviroment flag to opt out of .net core telemetry in start script
fixed "a moment" missing the "ago"
fixed case sensitive client searches on postgresql
clean up command code flow
Add missing map "mp_cairo" to default settings
2019-05-08 20:34:17 -05:00
43c4d4af38 force bots to all use the same profile
use C# 7.1 for projects
2019-05-04 09:17:18 -05:00
db11a5f480 upgrade packages, and delete a few unneeded ones
fix search for client resulting in invalid GUID parse
simplify output from dvar not being found
make sure to prompt if not all servers could be reached
2019-05-03 20:13:51 -05:00
b51af7ca9a fix penalty list javascript loading duplicates
make bad GUID parse throw an exception so we don't have a client connect with GUID of 0
no longer print out ac debug messages
fix small issue of trying to parse empty chat messages
fix issue with set level on accounts with multi guid, same IP
2019-05-02 22:33:38 -05:00
2cceb2f3e7 make database seed code less verbose
disable killserver command
fix issue with default parser not saving during setup
fix issue with unban reason displayed when player is rebanned
2019-04-28 20:54:11 -05:00
599a14b646 optimize the find client query 2019-04-25 21:05:35 -05:00
572 changed files with 35149 additions and 13891 deletions

10
.gitignore vendored
View File

@ -221,10 +221,12 @@ DEPLOY
global.min.css
global.min.js
bootstrap-custom.min.css
bootstrap-custom.css
**/Master/static
**/Master/dev_env
/WebfrontCore/Views/Plugins/*
/WebfrontCore/wwwroot/**/dds
/WebfrontCore/wwwroot/images/radar/*
/DiscordWebhook/env
/DiscordWebhook/config.dev.json
@ -234,3 +236,11 @@ launchSettings.json
/Plugins/ScriptPlugins/VpnDetectionPrivate.js
**/Master/env_master
/GameLogServer/log_env
**/*.css
/Master/master/persistence
/WebfrontCore/wwwroot/fonts
/WebfrontCore/wwwroot/font
/Plugins/Tests/TestSourceFiles
/Tests/ApplicationTests/Files/GameEvents.json
/Tests/ApplicationTests/Files/replay.json
/GameLogServer/game_log_server_env

View File

@ -6,7 +6,7 @@ namespace IW4MAdmin.Application.API.GameLogServer
[Header("User-Agent", "IW4MAdmin-RestEase")]
public interface IGameLogServer
{
[Get("log/{path}")]
Task<LogInfo> Log([Path] string path);
[Get("log/{path}/{key}")]
Task<LogInfo> Log([Path] string path, [Path] string key);
}
}

View File

@ -13,5 +13,7 @@ namespace IW4MAdmin.Application.API.GameLogServer
public int Length { get; set; }
[JsonProperty("data")]
public string Data { get; set; }
[JsonProperty("next_key")]
public string NextKey { get; set; }
}
}

View File

@ -1,19 +1,36 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Generic;
using Newtonsoft.Json;
using RestEase;
using SharedLibraryCore.Helpers;
namespace IW4MAdmin.Application.API.Master
{
/// <summary>
/// Defines the structure of the IW4MAdmin instance for the master API
/// </summary>
public class ApiInstance
{
/// <summary>
/// Unique ID of the instance
/// </summary>
[JsonProperty("id")]
public string Id { get; set; }
/// <summary>
/// Indicates how long the instance has been running
/// </summary>
[JsonProperty("uptime")]
public int Uptime { get; set; }
/// <summary>
/// Specifices the version of the instance
/// </summary>
[JsonProperty("version")]
public float Version { get; set; }
[JsonConverter(typeof(BuildNumberJsonConverter))]
public BuildNumber Version { get; set; }
/// <summary>
/// List of servers the instance is monitoring
/// </summary>
[JsonProperty("servers")]
public List<ApiServer> Servers { get; set; }
}

View File

@ -1,22 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using RestEase;
using SharedLibraryCore;
namespace IW4MAdmin.Application.API.Master
{
public class HeartbeatState
{
public bool Connected { get; set; }
public CancellationToken Token { get; set; }
}
/// <summary>
/// Defines the heartbeat functionality for IW4MAdmin
/// </summary>
public class Heartbeat
{
/// <summary>
/// Sends heartbeat to master server
/// </summary>
/// <param name="mgr"></param>
/// <param name="firstHeartbeat"></param>
/// <returns></returns>
public static async Task Send(ApplicationManager mgr, bool firstHeartbeat = false)
{
var api = Endpoint.Get();
@ -35,7 +34,7 @@ namespace IW4MAdmin.Application.API.Master
{
Id = mgr.GetApplicationSettings().Configuration().Id,
Uptime = (int)(DateTime.UtcNow - mgr.StartTime).TotalSeconds,
Version = (float)Program.Version,
Version = Program.Version,
Servers = mgr.Servers.Select(s =>
new ApiServer()
{
@ -47,19 +46,26 @@ namespace IW4MAdmin.Application.API.Master
Map = s.CurrentMap.Name,
MaxClientNum = s.MaxClients,
Id = s.EndPoint,
Port = (short)s.GetPort(),
Port = (short)s.Port,
IPAddress = s.IP
}).ToList()
};
Response<ResultMessage> response = null;
if (firstHeartbeat)
{
var message = await api.AddInstance(instance);
response = await api.AddInstance(instance);
}
else
{
var message = await api.UpdateInstance(instance.Id, instance);
response = await api.UpdateInstance(instance.Id, instance);
}
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
{
mgr.Logger.WriteWarning($"Response code from master is {response.ResponseMessage.StatusCode}, message is {response.StringContent}");
}
}
}

View File

@ -4,6 +4,7 @@ using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using RestEase;
using SharedLibraryCore.Helpers;
namespace IW4MAdmin.Application.API.Master
{
@ -22,9 +23,12 @@ namespace IW4MAdmin.Application.API.Master
public class VersionInfo
{
[JsonProperty("current-version-stable")]
public float CurrentVersionStable { get; set; }
[JsonConverter(typeof(BuildNumberJsonConverter))]
public BuildNumber CurrentVersionStable { get; set; }
[JsonProperty("current-version-prerelease")]
public float CurrentVersionPrerelease { get; set; }
[JsonConverter(typeof(BuildNumberJsonConverter))]
public BuildNumber CurrentVersionPrerelease { get; set; }
}
public class ResultMessage
@ -38,11 +42,14 @@ namespace IW4MAdmin.Application.API.Master
#if !DEBUG
private static readonly IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
#else
private static IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
private static readonly IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
#endif
public static IMasterApi Get() => api;
}
/// <summary>
/// Defines the capabilities of the master API
/// </summary>
[Header("User-Agent", "IW4MAdmin-RestEase")]
public interface IMasterApi
{
@ -53,13 +60,15 @@ namespace IW4MAdmin.Application.API.Master
Task<TokenId> Authenticate([Body] AuthenticationId Id);
[Post("instance/")]
Task<ResultMessage> AddInstance([Body] ApiInstance instance);
[AllowAnyStatusCode]
Task<Response<ResultMessage>> AddInstance([Body] ApiInstance instance);
[Put("instance/{id}")]
Task<ResultMessage> UpdateInstance([Path] string id, [Body] ApiInstance instance);
[AllowAnyStatusCode]
Task<Response<ResultMessage>> UpdateInstance([Path] string id, [Body] ApiInstance instance);
[Get("version")]
Task<VersionInfo> GetVersion();
[Get("version/{apiVersion}")]
Task<VersionInfo> GetVersion([Path] int apiVersion);
[Get("localization")]
Task<List<SharedLibraryCore.Localization.Layout>> GetLocalization();

View File

@ -2,16 +2,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<TargetFramework>netcoreapp3.1</TargetFramework>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2.2.6.5</Version>
<Version>2.3.2.0</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Product>IW4MAdmin</Product>
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated servers</Description>
<Copyright>2019</Copyright>
<Copyright>2020</Copyright>
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl>
<RepositoryUrl>https://github.com/RaidMax/IW4M-Admin</RepositoryUrl>
@ -21,28 +20,39 @@
<Configurations>Debug;Release;Prerelease</Configurations>
<Win32Resource />
<RootNamespace>IW4MAdmin.Application</RootNamespace>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RestEase" Version="1.4.7" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" />
<PackageReference Include="Jint" Version="3.0.0-beta-1632" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.3" />
<PackageReference Include="RestEase" Version="1.4.10" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" />
</ItemGroup>
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation>
<AssemblyVersion>2.2.6.5</AssemblyVersion>
<FileVersion>2.2.6.5</FileVersion>
<LangVersion>7.1</LangVersion>
<StartupObject></StartupObject>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
<ErrorReport>none</ErrorReport>
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>true</Private>
</ProjectReference>
<ProjectReference Include="..\WebfrontCore\WebfrontCore.csproj">
<Private>true</Private>
<CopyLocalSatelliteAssemblies>true</CopyLocalSatelliteAssemblies>
</ProjectReference>
<ProjectReference Include="..\WebfrontCore\WebfrontCore.csproj" />
</ItemGroup>
<ItemGroup>
@ -51,22 +61,18 @@
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.2.2" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="call $(ProjectDir)BuildScripts\PreBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" />
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PreBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" />
</Target>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
<Output TaskParameter="Assemblies" ItemName="CurrentAssembly" />
</GetAssemblyIdentity>
<Exec Command="call $(ProjectDir)BuildScripts\PostBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir) %(CurrentAssembly.Version)" />
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PostBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir) %25(CurrentAssembly.Version)" />
</Target>
<Target Name="PostPublish" AfterTargets="Publish">
<Exec Command="call $(ProjectDir)BuildScripts\PostPublish.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(ConfigurationName)" />
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PostPublish.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(ConfigurationName)" />
</Target>
</Project>

View File

@ -1,96 +1,112 @@
using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RconParsers;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Configuration.Validation;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Events;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Services;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static SharedLibraryCore.GameEvent;
namespace IW4MAdmin.Application
{
public class ApplicationManager : IManager
{
private List<Server> _servers;
private readonly ConcurrentBag<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
public Dictionary<int, EFClient> PrivilegedClients { get; set; }
public ILogger Logger => GetLogger(0);
public bool Running { get; private set; }
public bool IsRunning { get; private set; }
public bool IsInitialized { get; private set; }
// define what the delagate function looks like
public delegate void OnServerEventEventHandler(object sender, GameEventArgs e);
// expose the event handler so we can execute the events
public OnServerEventEventHandler OnServerEvent { get; set; }
public DateTime StartTime { get; private set; }
public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString();
public IList<IRConParser> AdditionalRConParsers { get; }
public IList<IEventParser> AdditionalEventParsers { get; }
public ITokenAuthentication TokenAuthenticator => Authenticator;
public ITokenAuthentication Authenticator => _authenticator;
public ITokenAuthentication TokenAuthenticator { get; }
public CancellationToken CancellationToken => _tokenSource.Token;
public string ExternalIPAddress { get; private set; }
static ApplicationManager Instance;
readonly List<AsyncStatus> TaskStatuses;
List<Command> Commands;
readonly List<MessageToken> MessageTokens;
ClientService ClientSvc;
public bool IsRestartRequested { get; private set; }
public IMiddlewareActionHandler MiddlewareActionHandler { get; }
public event EventHandler<GameEvent> OnGameEventExecuted;
private readonly List<IManagerCommand> _commands;
private readonly ILogger _logger;
private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc;
readonly AliasService AliasSvc;
readonly PenaltyService PenaltySvc;
public BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
GameEventHandler Handler;
ManualResetEventSlim OnQuit;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList;
readonly SemaphoreSlim ProcessingEvent = new SemaphoreSlim(1, 1);
readonly Dictionary<long, ILogger> Loggers = new Dictionary<long, ILogger>();
readonly ITokenAuthentication _authenticator;
private readonly Dictionary<long, ILogger> _loggers = new Dictionary<long, ILogger>();
private readonly MetaService _metaService;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
private readonly IGameServerInstanceFactory _serverInstanceFactory;
private readonly IParserRegexFactory _parserRegexFactory;
private readonly IEnumerable<IRegisterEvent> _customParserEvents;
private readonly IEventHandler _eventHandler;
private readonly IScriptCommandFactory _scriptCommandFactory;
private ApplicationManager()
public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory)
{
_servers = new List<Server>();
Commands = new List<Command>();
TaskStatuses = new List<AsyncStatus>();
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>();
ClientSvc = new ClientService();
ClientSvc = new ClientService(contextFactory);
AliasSvc = new AliasService();
PenaltySvc = new PenaltyService();
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow;
OnQuit = new ManualResetEventSlim();
PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>();
AdditionalRConParsers = new List<IRConParser>();
OnServerEvent += OnGameEvent;
OnServerEvent += EventApi.OnGameEvent;
_authenticator = new TokenAuthentication();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication();
_logger = logger;
_metaService = new MetaService();
_tokenSource = new CancellationTokenSource();
_loggers.Add(0, logger);
_commands = commands.ToList();
_translationLookup = translationLookup;
_commandConfiguration = commandConfiguration;
_serverInstanceFactory = serverInstanceFactory;
_parserRegexFactory = parserRegexFactory;
_customParserEvents = customParserEvents;
_eventHandler = eventHandler;
_scriptCommandFactory = scriptCommandFactory;
Plugins = plugins;
}
private async void OnGameEvent(object sender, GameEventArgs args)
public IEnumerable<IPlugin> Plugins { get; }
public async Task ExecuteEvent(GameEvent newEvent)
{
#if DEBUG == true
Logger.WriteDebug($"Entering event process for {args.Event.Id}");
Logger.WriteDebug($"Entering event process for {newEvent.Id}");
#endif
var newEvent = args.Event;
// the event has failed already
if (newEvent.Failed)
{
@ -103,44 +119,58 @@ namespace IW4MAdmin.Application
// save the event info to the database
var changeHistorySvc = new ChangeHistoryService();
await changeHistorySvc.Add(args.Event);
await changeHistorySvc.Add(newEvent);
#if DEBUG
Logger.WriteDebug($"Processed event with id {newEvent.Id}");
#endif
}
catch (TaskCanceledException)
{
Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early");
}
catch (OperationCanceledException)
{
Logger.WriteInfo($"Received quit signal for event id {newEvent.Id}, so we are aborting early");
}
// this happens if a plugin requires login
catch (AuthorizationException ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Permission;
newEvent.FailReason = EventFailReason.Permission;
newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {ex.Message}");
}
catch (NetworkException ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Exception;
newEvent.FailReason = EventFailReason.Exception;
Logger.WriteError(ex.Message);
Logger.WriteDebug(ex.GetExceptionInfo());
}
catch (ServerException ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Exception;
newEvent.FailReason = EventFailReason.Exception;
Logger.WriteWarning(ex.Message);
}
catch (Exception ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Exception;
newEvent.FailReason = EventFailReason.Exception;
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner));
Logger.WriteDebug(ex.GetExceptionInfo());
}
skip:
// tell anyone waiting for the output that we're done
newEvent.OnProcessed.Set();
newEvent.Complete();
OnGameEventExecuted?.Invoke(this, newEvent);
#if DEBUG == true
Logger.WriteDebug($"Exiting event process for {newEvent.Id}");
#endif
}
public IList<Server> GetServers()
@ -148,22 +178,17 @@ namespace IW4MAdmin.Application
return Servers;
}
public IList<Command> GetCommands()
public IList<IManagerCommand> GetCommands()
{
return Commands;
return _commands;
}
public static ApplicationManager GetInstance()
{
return Instance ?? (Instance = new ApplicationManager());
}
public async Task UpdateServerStates(CancellationToken token)
public async Task UpdateServerStates()
{
// store the server hash code and task for it
var runningUpdateTasks = new Dictionary<long, Task>();
while (Running)
while (!_tokenSource.IsCancellationRequested)
{
// select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks
@ -194,10 +219,11 @@ namespace IW4MAdmin.Application
{
try
{
await server.ProcessUpdatesAsync(token);
await server.ProcessUpdatesAsync(_tokenSource.Token);
if (server.Throttled)
{
await Task.Delay((int)_throttleTimeout.TotalMilliseconds);
await Task.Delay((int)_throttleTimeout.TotalMilliseconds, _tokenSource.Token);
}
}
@ -221,31 +247,57 @@ namespace IW4MAdmin.Application
#endif
try
{
await Task.Delay(ConfigHandler.Configuration().RConPollRate, token);
await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
}
// if a cancellation is received, we want to return immediately after shutting down
catch
{
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
{
await server.ProcessUpdatesAsync(_tokenSource.Token);
}
break;
}
// if a cancellation is received, we want to return immediately
catch { break; }
}
}
public async Task Init()
{
Running = true;
IsRunning = true;
ExternalIPAddress = await Utilities.GetExternalIP();
#region PLUGINS
SharedLibraryCore.Plugins.PluginImporter.Load(this);
foreach (var Plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
foreach (var plugin in Plugins)
{
try
{
await Plugin.OnLoadAsync(this);
if (plugin is ScriptPlugin scriptPlugin)
{
await scriptPlugin.Initialize(this, _scriptCommandFactory);
scriptPlugin.Watcher.Changed += async (sender, e) =>
{
try
{
await scriptPlugin.Initialize(this, _scriptCommandFactory);
}
catch (Exception ex)
{
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_PLUGIN"]} {Plugin.Name}");
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_IMPORTER_ERROR"].FormatExt(scriptPlugin.Name));
Logger.WriteDebug(ex.Message);
}
};
}
else
{
await plugin.OnLoadAsync(this);
}
}
catch (Exception ex)
{
Logger.WriteError($"{_translationLookup["SERVER_ERROR_PLUGIN"]} {plugin.Name}");
Logger.WriteDebug(ex.GetExceptionInfo());
}
}
@ -270,7 +322,7 @@ namespace IW4MAdmin.Application
if (newConfig.Servers == null)
{
ConfigHandler.Set(newConfig);
newConfig.Servers = new List<ServerConfiguration>();
newConfig.Servers = new ServerConfiguration[1];
do
{
@ -285,8 +337,8 @@ namespace IW4MAdmin.Application
serverConfig.AddEventParser(parser);
}
newConfig.Servers.Add((ServerConfiguration)serverConfig.Generate());
} while (Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["SETUP_SERVER_SAVE"]));
newConfig.Servers = newConfig.Servers.Where(_servers => _servers != null).Append((ServerConfiguration)serverConfig.Generate()).ToArray();
} while (Utilities.PromptBool(_translationLookup["SETUP_SERVER_SAVE"]));
config = newConfig;
await ConfigHandler.Save();
@ -307,6 +359,18 @@ namespace IW4MAdmin.Application
await ConfigHandler.Save();
}
var validator = new ApplicationConfigurationValidator();
var validationResult = validator.Validate(config);
if (!validationResult.IsValid)
{
throw new ConfigurationException("MANAGER_CONFIGURATION_ERROR")
{
Errors = validationResult.Errors.Select(_error => _error.ErrorMessage).ToArray(),
ConfigurationFileName = ConfigHandler.FileName
};
}
foreach (var serverConfig in config.Servers)
{
Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
@ -329,7 +393,7 @@ namespace IW4MAdmin.Application
}
}
if (config.Servers.Count == 0)
if (config.Servers.Length == 0)
{
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
}
@ -345,59 +409,43 @@ namespace IW4MAdmin.Application
{
await new ContextSeed(db).Seed();
}
PrivilegedClients = (await ClientSvc.GetPrivilegedClients()).ToDictionary(_client => _client.ClientId);
#endregion
#region COMMANDS
if (ClientSvc.GetOwners().Result.Count == 0)
if (ClientSvc.GetOwners().Result.Count > 0)
{
Commands.Add(new COwner());
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
}
Commands.Add(new CQuit());
Commands.Add(new CKick());
Commands.Add(new CSay());
Commands.Add(new CTempBan());
Commands.Add(new CBan());
Commands.Add(new CWhoAmI());
Commands.Add(new CList());
Commands.Add(new CHelp());
Commands.Add(new CFastRestart());
Commands.Add(new CMapRotate());
Commands.Add(new CSetLevel());
Commands.Add(new CUsage());
Commands.Add(new CUptime());
Commands.Add(new CWarn());
Commands.Add(new CWarnClear());
Commands.Add(new CUnban());
Commands.Add(new CListAdmins());
Commands.Add(new CLoadMap());
Commands.Add(new CFindPlayer());
Commands.Add(new CListRules());
Commands.Add(new CPrivateMessage());
Commands.Add(new CFlag());
Commands.Add(new CUnflag());
Commands.Add(new CReport());
Commands.Add(new CListReports());
Commands.Add(new CListBanInfo());
Commands.Add(new CListAlias());
Commands.Add(new CExecuteRCON());
Commands.Add(new CPlugins());
Commands.Add(new CIP());
Commands.Add(new CMask());
Commands.Add(new CPruneAdmins());
Commands.Add(new CKillServer());
Commands.Add(new CSetPassword());
Commands.Add(new CPing());
Commands.Add(new CSetGravatar());
Commands.Add(new CNextMap());
Commands.Add(new RequestTokenCommand());
List<IManagerCommand> commandsToAddToConfig = new List<IManagerCommand>();
var cmdConfig = _commandConfiguration.Configuration();
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
if (cmdConfig == null)
{
Commands.Add(C);
cmdConfig = new CommandConfiguration();
commandsToAddToConfig.AddRange(_commands);
}
else
{
var unsavedCommands = _commands.Where(_cmd => !cmdConfig.Commands.Keys.Contains(_cmd.CommandConfigNameForType()));
commandsToAddToConfig.AddRange(unsavedCommands);
}
foreach (var cmd in commandsToAddToConfig)
{
cmdConfig.Commands.Add(cmd.CommandConfigNameForType(),
new CommandProperties()
{
Name = cmd.Name,
Alias = cmd.Alias,
MinimumPermission = cmd.Permission,
AllowImpersonation = cmd.AllowImpersonation
});
}
_commandConfiguration.Set(cmdConfig);
await _commandConfiguration.Save();
#endregion
#region META
@ -441,6 +489,12 @@ namespace IW4MAdmin.Application
var client = await GetClientService().Get(clientId);
if (client == null)
{
_logger.WriteWarning($"No client found with id {clientId} when generating profile meta");
return metaList;
}
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
@ -522,26 +576,34 @@ namespace IW4MAdmin.Application
MetaService.AddRuntimeMeta(getPenaltyMeta);
#endregion
#region INIT
int failedServers = 0;
#region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
{
foreach (var parser in AdditionalEventParsers)
{
parser.RegisterCustomEvent(customEvent.Item1, customEvent.Item2, customEvent.Item3);
}
}
#endregion
await InitializeServers();
}
private async Task InitializeServers()
{
var config = ConfigHandler.Configuration();
int successServers = 0;
Exception lastException = null;
async Task Init(ServerConfiguration Conf)
{
// setup the event handler after the class is initialized
Handler = new GameEventHandler(this);
try
{
var ServerInstance = new IW4MServer(this, Conf);
// todo: this might not always be an IW4MServer
var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
await ServerInstance.Initialize();
lock (_servers)
{
_servers.Add(ServerInstance);
}
Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname));
// add the start event for this server
@ -553,7 +615,7 @@ namespace IW4MAdmin.Application
Owner = ServerInstance
};
Handler.AddEvent(e);
AddEvent(e);
successServers++;
}
@ -563,48 +625,46 @@ namespace IW4MAdmin.Application
if (e.GetType() == typeof(DvarException))
{
Logger.WriteDebug($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt((e as DvarException).Data["dvar_name"])} ({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})");
Logger.WriteDebug($"{e.Message} {(e.GetType() == typeof(DvarException) ? $"({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})" : "")}");
}
else if (e.GetType() == typeof(NetworkException))
{
Logger.WriteDebug(e.Message);
}
failedServers++;
lastException = e;
}
}
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
if (successServers - failedServers <= 0)
if (successServers == 0)
{
throw lastException;
}
if (successServers != config.Servers.Length)
{
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
{
throw lastException;
}
}
#endregion
}
private async Task SendHeartbeat(object state)
private async Task SendHeartbeat()
{
var heartbeatState = (HeartbeatState)state;
bool connected = false;
while (Running)
while (!_tokenSource.IsCancellationRequested)
{
if (!heartbeatState.Connected)
if (!connected)
{
try
{
await Heartbeat.Send(this, true);
heartbeatState.Connected = true;
connected = true;
}
catch (Exception e)
{
heartbeatState.Connected = false;
connected = false;
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
}
}
@ -630,7 +690,7 @@ namespace IW4MAdmin.Application
{
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
heartbeatState.Connected = false;
connected = false;
}
}
}
@ -640,9 +700,10 @@ namespace IW4MAdmin.Application
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
heartbeatState.Connected = false;
connected = false;
}
}
catch (Exception e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
@ -652,54 +713,45 @@ namespace IW4MAdmin.Application
try
{
await Task.Delay(30000, heartbeatState.Token);
await Task.Delay(30000, _tokenSource.Token);
}
catch { break; }
}
}
public void Start()
public async Task Start()
{
var tokenSource = new CancellationTokenSource();
// this needs to be run seperately from the main thread
_ = Task.Run(() => SendHeartbeat(new HeartbeatState() { Token = tokenSource.Token }));
_ = Task.Run(() => UpdateServerStates(tokenSource.Token));
while (Running)
await Task.WhenAll(new[]
{
OnQuit.Wait();
tokenSource.Cancel();
OnQuit.Reset();
}
SendHeartbeat(),
UpdateServerStates()
});
}
public void Stop()
{
Running = false;
OnQuit.Set();
_tokenSource.Cancel();
IsRunning = false;
}
public void Restart()
{
IsRestartRequested = true;
Stop();
}
public ILogger GetLogger(long serverId)
{
if (Loggers.ContainsKey(serverId))
if (_loggers.ContainsKey(serverId))
{
return Loggers[serverId];
return _loggers[serverId];
}
else
{
Logger newLogger;
var newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
if (serverId == 0)
{
newLogger = new Logger("IW4MAdmin-Manager");
}
else
{
newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
}
Loggers.Add(serverId, newLogger);
_loggers.Add(serverId, newLogger);
return newLogger;
}
}
@ -711,7 +763,8 @@ namespace IW4MAdmin.Application
public IList<EFClient> GetActiveClients()
{
return _servers.SelectMany(s => s.Clients).Where(p => p != null).ToList();
// we're adding another to list here so we don't get a collection modified exception..
return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList();
}
public ClientService GetClientService()
@ -734,29 +787,9 @@ namespace IW4MAdmin.Application
return ConfigHandler;
}
public IDictionary<int, EFClient> GetPrivilegedClients()
public void AddEvent(GameEvent gameEvent)
{
return PrivilegedClients;
}
public bool ShutdownRequested()
{
return !Running;
}
public IEventHandler GetEventHandler()
{
return Handler;
}
public void SetHasEvent()
{
}
public IList<Assembly> GetPluginAssemblies()
{
return SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies.Union(SharedLibraryCore.Plugins.PluginImporter.Assemblies).ToList();
_eventHandler.HandleEvent(this, gameEvent);
}
public IPageList GetPageList()
@ -764,14 +797,43 @@ namespace IW4MAdmin.Application
return PageList;
}
public IRConParser GenerateDynamicRConParser()
public IRConParser GenerateDynamicRConParser(string name)
{
return new DynamicRConParser();
return new DynamicRConParser(_parserRegexFactory)
{
Name = name
};
}
public IEventParser GenerateDynamicEventParser()
public IEventParser GenerateDynamicEventParser(string name)
{
return new DynamicEventParser();
}
return new DynamicEventParser(_parserRegexFactory, _logger)
{
Name = name
};
}
public async Task<IList<T>> ExecuteSharedDatabaseOperation<T>(string operationName)
{
var result = await _operationLookup[operationName];
return (IList<T>)result;
}
public void RegisterSharedDatabaseOperation(Task<IList> operation, string operationName)
{
_operationLookup.Add(operationName, operation);
}
public void AddAdditionalCommand(IManagerCommand command)
{
if (_commands.Any(_command => _command.Name == command.Name || _command.Alias == command.Alias))
{
throw new InvalidOperationException($"Duplicate command name or alias ({command.Name}, {command.Alias})");
}
_commands.Add(command);
}
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
}
}

View File

@ -25,10 +25,3 @@ xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Pl
echo Copying script plugins for publish
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\Windows\Plugins\"
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
echo Copying GSC files for publish
xcopy /Y "%SolutionDir%_customcallbacks.gsc" "%SolutionDir%Publish\Windows\userraw\scripts\"
xcopy /Y "%SolutionDir%_customcallbacks.gsc" "%SolutionDir%Publish\WindowsPrerelease\userraw\scripts\"
xcopy /Y "%SolutionDir%_commands.gsc" "%SolutionDir%Publish\Windows\userraw\scripts\"
xcopy /Y "%SolutionDir%_commands.gsc" "%SolutionDir%Publish\WindowsPrerelease\userraw\scripts\"

View File

@ -1,115 +1,41 @@
set SolutionDir=%1
set ProjectDir=%2
set TargetDir=%3
set CurrentConfiguration=%4
set PublishDir=%1
set SourceDir=%2
SET COPYCMD=/Y
echo Deleting extra language files
echo deleting extra runtime files
if exist "%PublishDir%\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-arm'
if exist "%PublishDir%\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-arm64'
if exist "%PublishDir%\runtimes\linux-armel" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-armel'
if exist "%PublishDir%\runtimes\osx" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\osx'
if exist "%PublishDir%\runtimes\osx-x64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\osx-x64'
if exist "%PublishDir%\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\win-arm'
if exist "%PublishDir%\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\win-arm64'
if exist "%PublishDir%\runtimes\alpine-x64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\alpine-x64'
if exist "%PublishDir%\runtimes\linux-musl-x64" powershell Remove-Item -Force -Recurse '%PublishDir%\runtimes\linux-musl-x64'
if exist "%SolutionDir%Publish\Windows\en-US\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\en-US'
if exist "%SolutionDir%Publish\Windows\de\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\de'
if exist "%SolutionDir%Publish\Windows\es\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\es'
if exist "%SolutionDir%Publish\Windows\fr\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\fr'
if exist "%SolutionDir%Publish\Windows\it\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\it'
if exist "%SolutionDir%Publish\Windows\ja\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\ja'
if exist "%SolutionDir%Publish\Windows\ko\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\ko'
if exist "%SolutionDir%Publish\Windows\ru\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\ru'
if exist "%SolutionDir%Publish\Windows\zh-Hans\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\zh-Hans'
if exist "%SolutionDir%Publish\Windows\zh-Hant\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\zh-Hant'
echo deleting misc files
if exist "%PublishDir%\web.config" del "%PublishDir%\web.config"
if exist "%PublishDir%\libman.json" del "%PublishDir%\libman.json"
del "%PublishDir%\*.exe"
del "%PublishDir%\*.pdb"
if exist "%SolutionDir%Publish\WindowsPrerelease\en-US\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\en-US'
if exist "%SolutionDir%Publish\WindowsPrerelease\de\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\de'
if exist "%SolutionDir%Publish\WindowsPrerelease\es\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\es'
if exist "%SolutionDir%Publish\WindowsPrerelease\fr\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\fr'
if exist "%SolutionDir%Publish\WindowsPrerelease\it\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\it'
if exist "%SolutionDir%Publish\WindowsPrerelease\ja\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ja'
if exist "%SolutionDir%Publish\WindowsPrerelease\ko\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ko'
if exist "%SolutionDir%Publish\WindowsPrerelease\ru\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ru'
if exist "%SolutionDir%Publish\WindowsPrerelease\zh-Hans\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\zh-Hans'
if exist "%SolutionDir%Publish\WindowsPrerelease\zh-Hant\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\zh-Hant'
echo Deleting extra runtime files
if exist "%SolutionDir%Publish\Windows\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-arm'
if exist "%SolutionDir%Publish\Windows\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-arm64'
if exist "%SolutionDir%Publish\Windows\runtimes\linux-armel" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-armel'
if exist "%SolutionDir%Publish\Windows\runtimes\osx" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\osx'
if exist "%SolutionDir%Publish\Windows\runtimes\osx-x64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\osx-x64'
if exist "%SolutionDir%Publish\Windows\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\win-arm'
if exist "%SolutionDir%Publish\Windows\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\win-arm64'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm64'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-armel" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-armel'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\osx" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\osx'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\osx-x64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\osx-x64'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm'
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm64'
echo Deleting misc files
if exist "%SolutionDir%Publish\Windows\web.config" del "%SolutionDir%Publish\Windows\web.config"
del "%SolutionDir%Publish\Windows\*pdb"
if exist "%SolutionDir%Publish\WindowsPrerelease\web.config" del "%SolutionDir%Publish\WindowsPrerelease\web.config"
del "%SolutionDir%Publish\WindowsPrerelease\*pdb"
echo setting up library folders
if "%CurrentConfiguration%" == "Prerelease" (
echo PR-Config
if not exist "%SolutionDir%Publish\WindowsPrerelease\Configuration" md "%SolutionDir%Publish\WindowsPrerelease\Configuration"
move "%SolutionDir%Publish\WindowsPrerelease\DefaultSettings.json" "%SolutionDir%Publish\WindowsPrerelease\Configuration\"
)
if "%CurrentConfiguration%" == "Release" (
echo R-Config
if not exist "%SolutionDir%Publish\Windows\Configuration" md "%SolutionDir%Publish\Windows\Configuration"
if exist "%SolutionDir%Publish\Windows\DefaultSettings.json" move "%SolutionDir%Publish\Windows\DefaultSettings.json" "%SolutionDir%Publish\Windows\Configuration\DefaultSettings.json"
)
if "%CurrentConfiguration%" == "Prerelease" (
echo PR-LIB
if not exist "%SolutionDir%Publish\WindowsPrerelease\Lib\" md "%SolutionDir%Publish\WindowsPrerelease\Lib\"
move "%SolutionDir%Publish\WindowsPrerelease\*.dll" "%SolutionDir%Publish\WindowsPrerelease\Lib\"
move "%SolutionDir%Publish\WindowsPrerelease\*.json" "%SolutionDir%Publish\WindowsPrerelease\Lib\"
)
if "%CurrentConfiguration%" == "Release" (
echo R-LIB
if not exist "%SolutionDir%Publish\Windows\Lib\" md "%SolutionDir%Publish\Windows\Lib\"
move "%SolutionDir%Publish\Windows\*.dll" "%SolutionDir%Publish\Windows\Lib\"
move "%SolutionDir%Publish\Windows\*.json" "%SolutionDir%Publish\Windows\Lib\"
)
if "%CurrentConfiguration%" == "Prerelease" (
echo PR-RT
move "%SolutionDir%Publish\WindowsPrerelease\runtimes" "%SolutionDir%Publish\WindowsPrerelease\Lib\runtimes"
if exist "%SolutionDir%Publish\WindowsPrerelease\refs" move "%SolutionDir%Publish\WindowsPrerelease\refs" "%SolutionDir%Publish\WindowsPrerelease\Lib\refs"
)
if "%CurrentConfiguration%" == "Release" (
echo R-RT
move "%SolutionDir%Publish\Windows\runtimes" "%SolutionDir%Publish\Windows\Lib\runtimes"
if exist "%SolutionDir%Publish\Windows\refs" move "%SolutionDir%Publish\Windows\refs" "%SolutionDir%Publish\Windows\Lib\refs"
)
if "%CurrentConfiguration%" == "Prerelease" (
echo PR-LOC
if not exist "%SolutionDir%Publish\WindowsPrerelease\Localization" md "%SolutionDir%Publish\WindowsPrerelease\Localization"
)
if "%CurrentConfiguration%" == "Release" (
echo R-LOC
if not exist "%SolutionDir%Publish\Windows\Localization" md "%SolutionDir%Publish\Windows\Localization"
)
echo setting up default folders
if not exist "%PublishDir%\Configuration" md "%PublishDir%\Configuration"
move "%PublishDir%\DefaultSettings.json" "%PublishDir%\Configuration\"
if not exist "%PublishDir%\Lib\" md "%PublishDir%\Lib\"
move "%PublishDir%\*.dll" "%PublishDir%\Lib\"
move "%PublishDir%\*.json" "%PublishDir%\Lib\"
move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes"
if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs"
echo making start scripts
@(echo @echo off && echo @title IW4MAdmin && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.cmd"
@(echo @echo off && echo @title IW4MAdmin && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%SolutionDir%Publish\Windows\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 dotnet Lib/IW4MAdmin.dll) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.sh"
@(echo #!/bin/bash && echo dotnet Lib/IW4MAdmin.dll) > "%SolutionDir%Publish\Windows\StartIW4MAdmin.sh"
echo moving front-end library dependencies
if not exist "%PublishDir%\wwwroot\font" mkdir "%PublishDir%\wwwroot\font"
move "WebfrontCore\wwwroot\lib\open-iconic\font\fonts\*.*" "%PublishDir%\wwwroot\font\"
if exist "%PublishDir%\wwwroot\lib" rd /s /q "%PublishDir%\wwwroot\lib"
echo setting permissions...
cacls "%PublishDir%" /t /e /p Everyone:F

View File

@ -16,7 +16,7 @@
"Keep grenade launcher use to a minimum",
"Balance teams at ALL times"
],
"DisallowedClientNames": ["Unknown Soldier", "VickNet", "UnknownSoldier", "CHEATER"],
"DisallowedClientNames": [ "Unknown Soldier", "VickNet", "UnknownSoldier", "CHEATER", "Play77" ],
"QuickMessages": [
{
"Game": "IW4",
@ -517,6 +517,10 @@
"Alias": "Hanoi",
"Name": "mp_hanoi"
},
{
"Alias": "Havana",
"Name": "mp_cairo"
},
{
"Alias": "Hazard",
"Name": "mp_golfcourse"

View File

@ -2,41 +2,47 @@
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.EventParsers
{
class BaseEventParser : IEventParser
public class BaseEventParser : IEventParser
{
public BaseEventParser()
private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations;
private readonly ILogger _logger;
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger)
{
Configuration = new DynamicEventParserConfiguration()
_customEventRegistrations = new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
_logger = logger;
Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
{
GameDirectory = "main",
};
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32});([0-9]+);(.+);(.*)$";
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);([^;]*);(.*)$";
Configuration.Say.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Say.AddMapping(ParserRegex.GroupType.Message, 5);
Configuration.Quit.Pattern = @"^(Q);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);([0-9]+);(.*)$";
Configuration.Quit.Pattern = @"^(Q);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(.*)$";
Configuration.Quit.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Join.Pattern = @"^(J);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);([0-9]+);(.*)$";
Configuration.Join.Pattern = @"^(J);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(.*)$";
Configuration.Join.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world);(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world);(.{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)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -51,7 +57,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world);(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world);(.{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)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -65,6 +71,8 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Kill.AddMapping(ParserRegex.GroupType.Damage, 11);
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Time.Pattern = @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )";
}
public IEventParserConfiguration Configuration { get; set; }
@ -75,50 +83,63 @@ namespace IW4MAdmin.Application.EventParsers
public string URLProtocolFormat { get; set; } = "CoD://{{ip}}:{{port}}";
public virtual GameEvent GetEvent(Server server, string logLine)
public string Name { get; set; } = "Call of Duty";
public virtual GameEvent GenerateGameEvent(string logLine)
{
logLine = Regex.Replace(logLine, @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim();
var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
int gameTime = 0;
if (timeMatch.Success)
{
gameTime = timeMatch
.Values
.Skip(2)
// this converts the timestamp into seconds passed
.Select((_value, index) => int.Parse(_value.ToString()) * (index == 0 ? 60 : 1))
.Sum();
// we want to strip the time from the log line
logLine = logLine.Substring(timeMatch.Values.First().Length);
}
string[] lineSplit = logLine.Split(';');
string eventType = lineSplit[0];
if (eventType == "JoinTeam")
{
var origin = server.GetClientsAsList()
.FirstOrDefault(c => c.NetworkId == lineSplit[1].ConvertLong());
return new GameEvent()
{
Type = GameEvent.EventType.JoinTeam,
Data = logLine,
Origin = origin,
Owner = server
};
}
if (eventType == "say" || eventType == "sayteam")
{
var matchResult = Regex.Match(logLine, Configuration.Say.Pattern);
var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
if (matchResult.Success)
{
string message = matchResult
.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
string message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.ToString()
.Replace("\x15", "")
.Trim();
var origin = server.GetClientsAsList()
.First(c => c.NetworkId == matchResult.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong());
if (message.Length > 0)
{
string originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
// todo: these need to defined outside of here
if (message[0] == '!' || message[0] == '@')
{
return new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = origin,
Owner = server,
Message = message
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
@ -126,165 +147,151 @@ namespace IW4MAdmin.Application.EventParsers
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = origin,
Owner = server,
Message = message
Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
}
if (eventType == "K")
{
if (!server.CustomCallback)
{
var match = Regex.Match(logLine, Configuration.Kill.Pattern);
var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (match.Success)
{
string originId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].Value.ToString();
string targetId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].Value.ToString();
string originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
string originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
string targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
var origin = !string.IsNullOrEmpty(originId) ? server.GetClientsAsList()
.First(c => c.NetworkId == originId.ConvertLong()) :
Utilities.IW4MAdminClient(server);
long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
long targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var target = !string.IsNullOrEmpty(targetId) ? server.GetClientsAsList()
.First(c => c.NetworkId == targetId.ConvertLong()) :
Utilities.IW4MAdminClient(server);
int originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = origin,
Target = target,
Owner = server
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
}
if (eventType == "ScriptKill")
{
long originId = lineSplit[1].ConvertLong();
long targetId = lineSplit[2].ConvertLong();
var origin = originId == long.MinValue ? Utilities.IW4MAdminClient(server) :
server.GetClientsAsList().First(c => c.NetworkId == originId);
var target = targetId == long.MinValue ? Utilities.IW4MAdminClient(server) :
server.GetClientsAsList().FirstOrDefault(c => c.NetworkId == targetId) ?? Utilities.IW4MAdminClient(server);
return new GameEvent()
{
Type = GameEvent.EventType.ScriptKill,
Data = logLine,
Origin = origin,
Target = target,
Owner = server
};
}
if (eventType == "ScriptDamage")
{
long originId = lineSplit[1].ConvertLong();
long targetId = lineSplit[2].ConvertLong();
var origin = originId == long.MinValue ? Utilities.IW4MAdminClient(server) :
server.GetClientsAsList().First(c => c.NetworkId == originId);
var target = targetId == long.MinValue ? Utilities.IW4MAdminClient(server) :
server.GetClientsAsList().FirstOrDefault(c => c.NetworkId == targetId) ?? Utilities.IW4MAdminClient(server);
return new GameEvent()
{
Type = GameEvent.EventType.ScriptDamage,
Data = logLine,
Origin = origin,
Target = target,
Owner = server
};
}
// damage
if (eventType == "D")
{
if (!server.CustomCallback)
var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (match.Success)
{
var regexMatch = Regex.Match(logLine, Configuration.Damage.Pattern);
string originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
string originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
string targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
if (regexMatch.Success)
{
string originId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string targetId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
long targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
var origin = !string.IsNullOrEmpty(originId) ? server.GetClientsAsList()
.First(c => c.NetworkId == originId.ConvertLong()) :
Utilities.IW4MAdminClient(server);
var target = !string.IsNullOrEmpty(targetId) ? server.GetClientsAsList()
.First(c => c.NetworkId == targetId.ConvertLong()) :
Utilities.IW4MAdminClient(server);
int originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = logLine,
Origin = origin,
Target = target,
Owner = server
Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
}
// join
if (eventType == "J")
{
var regexMatch = Regex.Match(logLine, Configuration.Join.Pattern);
if (regexMatch.Success)
var match = Configuration.Join.PatternMatcher.Match(logLine);
if (match.Success)
{
bool isBot = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().Contains("bot");
string originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
long networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new GameEvent()
{
Type = GameEvent.EventType.PreConnect,
Data = logLine,
Owner = server,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().StripColors(),
Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine(),
},
NetworkId = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong(),
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting,
CurrentServer = server,
IsBot = isBot
}
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
if (eventType == "Q")
{
var regexMatch = Regex.Match(logLine, Configuration.Quit.Pattern);
if (regexMatch.Success)
var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (match.Success)
{
string originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
long networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new GameEvent()
{
Type = GameEvent.EventType.PreDisconnect,
Data = logLine,
Owner = server,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().StripColors()
Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine()
},
NetworkId = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong(),
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
NetworkId = networkId,
ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Disconnecting
}
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
}
@ -294,10 +301,12 @@ namespace IW4MAdmin.Application.EventParsers
return new GameEvent()
{
Type = GameEvent.EventType.MapEnd,
Data = lineSplit[0],
Origin = Utilities.IW4MAdminClient(server),
Target = Utilities.IW4MAdminClient(server),
Owner = server
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
@ -308,21 +317,74 @@ namespace IW4MAdmin.Application.EventParsers
return new GameEvent()
{
Type = GameEvent.EventType.MapChange,
Data = lineSplit[0],
Origin = Utilities.IW4MAdminClient(server),
Target = Utilities.IW4MAdminClient(server),
Owner = server,
Extra = dump.DictionaryFromKeyValue()
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
Extra = dump.DictionaryFromKeyValue(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
if (_customEventRegistrations.ContainsKey(eventType))
{
var eventModifier = _customEventRegistrations[eventType];
try
{
return eventModifier.Item2(logLine, Configuration, new GameEvent()
{
Type = GameEvent.EventType.Other,
Data = logLine,
Subtype = eventModifier.Item1,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
});
}
catch (Exception e)
{
_logger.WriteWarning($"Could not handle custom event generation - {e.GetExceptionInfo()}");
}
}
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Origin = Utilities.IW4MAdminClient(server),
Target = Utilities.IW4MAdminClient(server),
Owner = server
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
};
}
/// <inheritdoc/>
public void RegisterCustomEvent(string eventSubtype, string eventTriggerValue, Func<string, IEventParserConfiguration, GameEvent, GameEvent> eventModifier)
{
if (string.IsNullOrWhiteSpace(eventSubtype))
{
throw new ArgumentException("Event subtype cannot be empty");
}
if (string.IsNullOrWhiteSpace(eventTriggerValue))
{
throw new ArgumentException("Event trigger value cannot be empty");
}
if (eventModifier == null)
{
throw new ArgumentException("Event modifier must be specified");
}
if (_customEventRegistrations.ContainsKey(eventTriggerValue))
{
throw new ArgumentException($"Event trigger value '{eventTriggerValue}' is already registered");
}
_customEventRegistrations.Add(eventTriggerValue, (eventSubtype, eventModifier));
}
}
}

View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using static SharedLibraryCore.Server;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.EventParsers
{
@ -10,6 +7,9 @@ namespace IW4MAdmin.Application.EventParsers
/// allows script plugins to generate dynamic event parsers
/// </summary>
sealed internal class DynamicEventParser : BaseEventParser
{
public DynamicEventParser(IParserRegexFactory parserRegexFactory, ILogger logger) : base(parserRegexFactory, logger)
{
}
}
}

View File

@ -1,4 +1,5 @@
using SharedLibraryCore.Interfaces;
using System.Globalization;
namespace IW4MAdmin.Application.EventParsers
{
@ -9,11 +10,24 @@ namespace IW4MAdmin.Application.EventParsers
sealed internal class DynamicEventParserConfiguration : IEventParserConfiguration
{
public string GameDirectory { get; set; }
public ParserRegex Say { get; set; } = new ParserRegex();
public ParserRegex Join { get; set; } = new ParserRegex();
public ParserRegex Quit { get; set; } = new ParserRegex();
public ParserRegex Kill { get; set; } = new ParserRegex();
public ParserRegex Damage { get; set; } = new ParserRegex();
public ParserRegex Action { get; set; } = new ParserRegex();
public ParserRegex Say { get; set; }
public ParserRegex Join { get; set; }
public ParserRegex Quit { get; set; }
public ParserRegex Kill { get; set; }
public ParserRegex Damage { get; set; }
public ParserRegex Action { get; set; }
public ParserRegex Time { get; set; }
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public DynamicEventParserConfiguration(IParserRegexFactory parserRegexFactory)
{
Say = parserRegexFactory.CreateParserRegex();
Join = parserRegexFactory.CreateParserRegex();
Quit = parserRegexFactory.CreateParserRegex();
Kill = parserRegexFactory.CreateParserRegex();
Damage = parserRegexFactory.CreateParserRegex();
Action = parserRegexFactory.CreateParserRegex();
Time = parserRegexFactory.CreateParserRegex();
}
}
}

View File

@ -0,0 +1,35 @@
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace IW4MAdmin.Application.EventParsers
{
/// <summary>
/// implementation of the IParserPatternMatcher for windows (really it's the only implementation)
/// </summary>
public class ParserPatternMatcher : IParserPatternMatcher
{
private Regex regex;
/// <inheritdoc/>
public void Compile(string pattern)
{
regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
/// <inheritdoc/>
public IMatchResult Match(string input)
{
var match = regex.Match(input);
return new ParserMatchResult()
{
Success = match.Success,
Values = (match.Groups as IEnumerable<object>)?
.Select(_item => _item.ToString()).ToArray() ?? new string[0]
};
}
}
}

View File

@ -0,0 +1,21 @@
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces;
using System.Linq;
namespace IW4MAdmin.Application.Extensions
{
public static class CommandExtensions
{
/// <summary>
/// determines the command configuration name for given manager command
/// </summary>
/// <param name="command">command to determine config name for</param>
/// <returns></returns>
public static string CommandConfigNameForType(this IManagerCommand command)
{
return command.GetType() == typeof(ScriptCommand) ?
$"{char.ToUpper(command.Name[0])}{command.Name.Substring(1)}Command" :
command.GetType().Name;
}
}
}

View File

@ -0,0 +1,23 @@
using IW4MAdmin.Application.Misc;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// implementation of IConfigurationHandlerFactory
/// provides base functionality to create configuration handlers
/// </summary>
public class ConfigurationHandlerFactory : IConfigurationHandlerFactory
{
/// <summary>
/// creates a base configuration handler
/// </summary>
/// <typeparam name="T">base configuration type</typeparam>
/// <param name="name">name of the config file</param>
/// <returns></returns>
public IConfigurationHandler<T> GetConfigurationHandler<T>(string name) where T : IBaseConfiguration
{
return new BaseConfigurationHandler<T>(name);
}
}
}

View File

@ -0,0 +1,21 @@
using SharedLibraryCore.Database;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// implementation of the IDatabaseContextFactory interface
/// </summary>
public class DatabaseContextFactory : IDatabaseContextFactory
{
/// <summary>
/// creates a new database context
/// </summary>
/// <param name="enableTracking">indicates if entity tracking should be enabled</param>
/// <returns></returns>
public DatabaseContext CreateContext(bool? enableTracking = true)
{
return enableTracking.HasValue ? new DatabaseContext(disableTracking: !enableTracking.Value) : new DatabaseContext();
}
}
}

View File

@ -0,0 +1,33 @@
using IW4MAdmin.Application.IO;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore.Interfaces;
using System;
namespace IW4MAdmin.Application.Factories
{
public class GameLogReaderFactory : IGameLogReaderFactory
{
private readonly IServiceProvider _serviceProvider;
public GameLogReaderFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IGameLogReader CreateGameLogReader(Uri[] logUris, IEventParser eventParser)
{
var baseUri = logUris[0];
if (baseUri.Scheme == Uri.UriSchemeHttp)
{
return new GameLogReaderHttp(logUris, eventParser, _serviceProvider.GetRequiredService<ILogger>());
}
else if (baseUri.Scheme == Uri.UriSchemeFile)
{
return new GameLogReader(baseUri.LocalPath, eventParser, _serviceProvider.GetRequiredService<ILogger>());
}
throw new NotImplementedException($"No log reader implemented for Uri scheme \"{baseUri.Scheme}\"");
}
}
}

View File

@ -0,0 +1,40 @@
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System.Collections;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// implementation of IGameServerInstanceFactory
/// </summary>
internal class GameServerInstanceFactory : IGameServerInstanceFactory
{
private readonly ITranslationLookup _translationLookup;
private readonly IRConConnectionFactory _rconConnectionFactory;
private readonly IGameLogReaderFactory _gameLogReaderFactory;
/// <summary>
/// base constructor
/// </summary>
/// <param name="translationLookup"></param>
/// <param name="rconConnectionFactory"></param>
public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory)
{
_translationLookup = translationLookup;
_rconConnectionFactory = rconConnectionFactory;
_gameLogReaderFactory = gameLogReaderFactory;
}
/// <summary>
/// creates an IW4MServer instance
/// </summary>
/// <param name="config">server configuration</param>
/// <param name="manager">application manager</param>
/// <returns></returns>
public Server CreateServer(ServerConfiguration config, IManager manager)
{
return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory, _gameLogReaderFactory);
}
}
}

View File

@ -0,0 +1,26 @@
using SharedLibraryCore.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// Implementation of the IParserRegexFactory
/// </summary>
public class ParserRegexFactory : IParserRegexFactory
{
private readonly IServiceProvider _serviceProvider;
/// <inheritdoc/>
public ParserRegexFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <inheritdoc/>
public ParserRegex CreateParserRegex()
{
return new ParserRegex(_serviceProvider.GetService<IParserPatternMatcher>());
}
}
}

View File

@ -0,0 +1,36 @@
using IW4MAdmin.Application.RCon;
using SharedLibraryCore.Interfaces;
using System.Text;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// implementation of IRConConnectionFactory
/// </summary>
internal class RConConnectionFactory : IRConConnectionFactory
{
private static readonly Encoding gameEncoding = Encoding.GetEncoding("windows-1252");
private readonly ILogger _logger;
/// <summary>
/// Base constructor
/// </summary>
/// <param name="logger"></param>
public RConConnectionFactory(ILogger logger)
{
_logger = logger;
}
/// <summary>
/// creates a new rcon connection instance
/// </summary>
/// <param name="ipAddress">ip address of the server</param>
/// <param name="port">port of the server</param>
/// <param name="password">rcon password of the server</param>
/// <returns></returns>
public IRConConnection CreateConnection(string ipAddress, int port, string password)
{
return new RConConnection(ipAddress, port, password, _logger, gameEncoding);
}
}
}

View File

@ -0,0 +1,40 @@
using IW4MAdmin.Application.Misc;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using static SharedLibraryCore.Database.Models.EFClient;
namespace IW4MAdmin.Application.Factories
{
/// <summary>
/// implementation of IScriptCommandFactory
/// </summary>
public class ScriptCommandFactory : IScriptCommandFactory
{
private CommandConfiguration _config;
private readonly ITranslationLookup _transLookup;
public ScriptCommandFactory(CommandConfiguration config, ITranslationLookup transLookup)
{
_config = config;
_transLookup = transLookup;
}
/// <inheritdoc/>
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction)
{
var permissionEnum = Enum.Parse<Permission>(permission);
var argsArray = args.Select(_arg => new CommandArgument
{
Name = _arg.Item1,
Required = _arg.Item2
}).ToArray();
return new ScriptCommand(name, alias, description, permissionEnum, argsArray, executeAction, _config, _transLookup);
}
}
}

View File

@ -1,23 +1,55 @@
using SharedLibraryCore;
using IW4MAdmin.Application.Misc;
using Newtonsoft.Json;
using SharedLibraryCore;
using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace IW4MAdmin.Application
{
class GameEventHandler : IEventHandler
public class GameEventHandler : IEventHandler
{
readonly IManager Manager;
public GameEventHandler(IManager mgr)
private readonly EventLog _eventLog;
private static readonly GameEvent.EventType[] overrideEvents = new[]
{
Manager = mgr;
GameEvent.EventType.Connect,
GameEvent.EventType.Disconnect,
GameEvent.EventType.Quit,
GameEvent.EventType.Stop
};
public GameEventHandler()
{
_eventLog = new EventLog();
}
public void AddEvent(GameEvent gameEvent)
public void HandleEvent(IManager manager, GameEvent gameEvent)
{
((Manager as ApplicationManager).OnServerEvent)(this, new GameEventArgs(null, false, gameEvent));
#if DEBUG
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
gameEvent.Owner.Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
{
#if DEBUG
gameEvent.Owner.Logger.WriteDebug($"Adding event with id {gameEvent.Id}");
#endif
EventApi.OnGameEvent(gameEvent);
Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent));
}
#if DEBUG
else
{
gameEvent.Owner.Logger.WriteDebug($"Skipping event as we're shutting down {gameEvent.Id}");
}
#endif
}
}
}

View File

@ -1,36 +1,30 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Threading;
using System.Linq;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO
{
class GameLogEventDetection
public class GameLogEventDetection
{
Server Server;
long PreviousFileSize;
IGameLogReader Reader;
readonly string GameLogFile;
private long previousFileSize;
private readonly Server _server;
private readonly IGameLogReader _reader;
private readonly bool _ignoreBots;
class EventState
public GameLogEventDetection(Server server, Uri[] gameLogUris, IGameLogReaderFactory gameLogReaderFactory)
{
public ILogger Log { get; set; }
public string ServerId { get; set; }
}
public GameLogEventDetection(Server server, string gameLogPath, Uri gameLogServerUri)
{
GameLogFile = gameLogPath;
Reader = gameLogServerUri != null ? new GameLogReaderHttp(gameLogServerUri, gameLogPath, server.EventParser) : Reader = new GameLogReader(gameLogPath, server.EventParser);
Server = server;
_reader = gameLogReaderFactory.CreateGameLogReader(gameLogUris, server.EventParser);
_server = server;
_ignoreBots = server?.Manager.GetApplicationSettings().Configuration().IgnoreBots ?? false;
}
public async Task PollForChanges()
{
while (!Server.Manager.ShutdownRequested())
while (!_server.Manager.CancellationToken.IsCancellationRequested)
{
if (Server.IsInitialized)
if (_server.IsInitialized)
{
try
{
@ -39,39 +33,84 @@ namespace IW4MAdmin.Application.IO
catch (Exception e)
{
Server.Logger.WriteWarning($"Failed to update log event for {Server.EndPoint}");
Server.Logger.WriteDebug($"Exception: {e.Message}");
Server.Logger.WriteDebug($"StackTrace: {e.StackTrace}");
}
}
Thread.Sleep(Reader.UpdateInterval);
_server.Logger.WriteWarning($"Failed to update log event for {_server.EndPoint}");
_server.Logger.WriteDebug(e.GetExceptionInfo());
}
}
private async Task UpdateLogEvents()
await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken);
}
_server.Logger.WriteDebug("Stopped polling for changes");
}
public async Task UpdateLogEvents()
{
long fileSize = Reader.Length;
long fileSize = _reader.Length;
if (PreviousFileSize == 0)
PreviousFileSize = fileSize;
if (previousFileSize == 0)
{
previousFileSize = fileSize;
}
long fileDiff = fileSize - PreviousFileSize;
long fileDiff = fileSize - previousFileSize;
// this makes the http log get pulled
if (fileDiff < 1 && fileSize != -1)
return;
PreviousFileSize = fileSize;
var events = await Reader.ReadEventsFromLog(Server, fileDiff, 0);
foreach (var ev in events)
{
Server.Manager.GetEventHandler().AddEvent(ev);
await ev.WaitAsync();
previousFileSize = fileSize;
return;
}
PreviousFileSize = fileSize;
var events = await _reader.ReadEventsFromLog(fileDiff, previousFileSize);
foreach (var gameEvent in events)
{
try
{
#if DEBUG
_server.Logger.WriteVerbose(gameEvent.Data);
#endif
gameEvent.Owner = _server;
// we don't want to add the event if ignoreBots is on and the event comes from a bot
if (!_ignoreBots || (_ignoreBots && !((gameEvent.Origin?.IsBot ?? false) || (gameEvent.Target?.IsBot ?? false))))
{
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != Utilities.WORLD_ID)
{
gameEvent.Origin = _server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId);;
}
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target)
{
gameEvent.Target = _server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Target?.NetworkId);
}
if (gameEvent.Origin != null)
{
gameEvent.Origin.CurrentServer = _server;
}
if (gameEvent.Target != null)
{
gameEvent.Target.CurrentServer = _server;
}
_server.Manager.AddEvent(gameEvent);
}
}
catch (InvalidOperationException)
{
if (!_ignoreBots)
{
_server.Logger.WriteWarning("Could not find client in client list when parsing event line");
_server.Logger.WriteDebug(gameEvent.Data);
}
}
}
previousFileSize = fileSize;
}
}
}

View File

@ -3,6 +3,7 @@ using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -10,57 +11,71 @@ namespace IW4MAdmin.Application.IO
{
class GameLogReader : IGameLogReader
{
IEventParser Parser;
readonly string LogFile;
private readonly IEventParser _parser;
private readonly string _logFile;
private readonly ILogger _logger;
public long Length => new FileInfo(LogFile).Length;
public long Length => new FileInfo(_logFile).Length;
public int UpdateInterval => 300;
public GameLogReader(string logFile, IEventParser parser)
public GameLogReader(string logFile, IEventParser parser, ILogger logger)
{
LogFile = logFile;
Parser = parser;
_logFile = logFile;
_parser = parser;
_logger = logger;
}
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(long fileSizeDiff, long startPosition)
{
// allocate the bytes for the new log lines
List<string> logLines = new List<string>();
// open the file as a stream
using (var rd = new StreamReader(new FileStream(LogFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), Utilities.EncodingType))
using (FileStream fs = new FileStream(_logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
// todo: max async
// take the old start position and go back the number of new characters
rd.BaseStream.Seek(-fileSizeDiff, SeekOrigin.End);
byte[] buff = new byte[fileSizeDiff];
fs.Seek(startPosition, SeekOrigin.Begin);
await fs.ReadAsync(buff, 0, (int)fileSizeDiff);
var stringBuilder = new StringBuilder();
char[] charBuff = Utilities.EncodingType.GetChars(buff);
string newLine;
while (!string.IsNullOrEmpty(newLine = await rd.ReadLineAsync()))
foreach (char c in charBuff)
{
logLines.Add(newLine);
if (c == '\n')
{
logLines.Add(stringBuilder.ToString());
stringBuilder = new StringBuilder();
}
else if (c != '\r')
{
stringBuilder.Append(c);
}
}
if (stringBuilder.Length > 0)
{
logLines.Add(stringBuilder.ToString());
}
}
List<GameEvent> events = new List<GameEvent>();
// parse each line
foreach (string eventLine in logLines)
{
if (eventLine.Length > 0)
foreach (string eventLine in logLines.Where(_line => _line.Length > 0))
{
try
{
// todo: catch elsewhere
events.Add(Parser.GetEvent(server, eventLine));
var gameEvent = _parser.GenerateGameEvent(eventLine);
events.Add(gameEvent);
}
catch (Exception e)
{
server.Logger.WriteWarning("Could not properly parse event line");
server.Logger.WriteDebug(e.Message);
server.Logger.WriteDebug(eventLine);
}
_logger.WriteWarning("Could not properly parse event line");
_logger.WriteDebug(e.Message);
_logger.WriteDebug(eventLine);
}
}

View File

@ -4,66 +4,67 @@ using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Linq;
using System.Threading.Tasks;
using static SharedLibraryCore.Utilities;
namespace IW4MAdmin.Application.IO
{
/// <summary>
/// provides capibility of reading log files over HTTP
/// provides capability of reading log files over HTTP
/// </summary>
class GameLogReaderHttp : IGameLogReader
{
readonly IEventParser Parser;
readonly IGameLogServer Api;
readonly string logPath;
private readonly IEventParser _eventParser;
private readonly IGameLogServer _logServerApi;
private readonly ILogger _logger;
private readonly string _safeLogPath;
private string lastKey = "next";
public GameLogReaderHttp(Uri gameLogServerUri, string logPath, IEventParser parser)
public GameLogReaderHttp(Uri[] gameLogServerUris, IEventParser parser, ILogger logger)
{
this.logPath = logPath.ToBase64UrlSafeString(); ;
Parser = parser;
Api = RestClient.For<IGameLogServer>(gameLogServerUri);
_eventParser = parser;
_logServerApi = RestClient.For<IGameLogServer>(gameLogServerUris[0].ToString());
_safeLogPath = gameLogServerUris[1].LocalPath.ToBase64UrlSafeString();
_logger = logger;
}
public long Length => -1;
public int UpdateInterval => 350;
public int UpdateInterval => 500;
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(long fileSizeDiff, long startPosition)
{
#if DEBUG == true
server.Logger.WriteDebug($"Begin reading from http log");
#endif
var events = new List<GameEvent>();
string b64Path = logPath;
var response = await Api.Log(b64Path);
var response = await _logServerApi.Log(_safeLogPath, lastKey);
lastKey = response.NextKey;
if (!response.Success)
if (!response.Success && string.IsNullOrEmpty(lastKey))
{
server.Logger.WriteError($"Could not get log server info of {logPath}/{b64Path} ({server.LogPath})");
_logger.WriteError($"Could not get log server info of {_safeLogPath}");
return events;
}
// parse each line
foreach (string eventLine in response.Data.Split(Environment.NewLine))
else if (!string.IsNullOrWhiteSpace(response.Data))
{
if (eventLine.Length > 0)
// parse each line
var lines = response.Data
.Split(Environment.NewLine)
.Where(_line => _line.Length > 0);
foreach (string eventLine in lines)
{
try
{
var e = Parser.GetEvent(server, eventLine);
#if DEBUG == true
server.Logger.WriteDebug($"Parsed event with id {e.Id} from http");
#endif
events.Add(e);
// this trim end should hopefully fix the nasty runaway regex
var gameEvent = _eventParser.GenerateGameEvent(eventLine.TrimEnd('\r'));
events.Add(gameEvent);
}
catch (Exception e)
{
server.Logger.WriteWarning("Could not properly parse event line");
server.Logger.WriteDebug(e.Message);
server.Logger.WriteDebug(eventLine);
_logger.WriteError("Could not properly parse event line from http");
_logger.WriteDebug(e.Message);
_logger.WriteDebug(eventLine);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,29 @@
using IW4MAdmin.Application.API.Master;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Localization
{
public class Configure
{
public static void Initialize(string customLocale = null)
public static ITranslationLookup Initialize(bool useLocalTranslation, string customLocale = null)
{
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
if (!Program.ServerManager.GetApplicationSettings()?.Configuration()?.UseLocalTranslations ?? false)
if (!useLocalTranslation)
{
try
{
var api = Endpoint.Get();
var localization = api.GetLocalization(currentLocale).Result;
Utilities.CurrentLocalization = localization;
return;
return localization.LocalizationIndex;
}
catch (Exception)
@ -73,6 +72,8 @@ namespace IW4MAdmin.Application.Localization
{
LocalizationName = currentLocale,
};
return Utilities.CurrentLocalization.LocalizationIndex;
}
}
}

View File

@ -1,8 +1,17 @@
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Factories;
using IW4MAdmin.Application.Helpers;
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore;
using SharedLibraryCore.Localization;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Repositories;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -11,61 +20,166 @@ namespace IW4MAdmin.Application
{
public class Program
{
static public double Version { get; private set; }
static public ApplicationManager ServerManager;
private static ManualResetEventSlim OnShutdownComplete = new ManualResetEventSlim();
public static BuildNumber Version { get; private set; } = BuildNumber.Parse(Utilities.GetVersionAsString());
public static ApplicationManager ServerManager;
private static Task ApplicationTask;
private static readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99");
private static ServiceProvider serviceProvider;
public static void Main(string[] args)
/// <summary>
/// entrypoint of the application
/// </summary>
/// <returns></returns>
public static async Task Main(string[] args)
{
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray;
Version = Utilities.GetVersionAsDouble();
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
Console.WriteLine("=====================================================");
Console.WriteLine(" IW4M ADMIN");
Console.WriteLine(" IW4MAdmin");
Console.WriteLine(" by RaidMax ");
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("=====================================================");
Index loc = null;
await LaunchAsync(args);
}
/// <summary>
/// event callback executed when the control + c combination is detected
/// gracefully stops the server manager and waits for all tasks to finish
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
{
ServerManager?.Stop();
await ApplicationTask;
}
/// <summary>
/// task that initializes application and starts the application monitoring and runtime tasks
/// </summary>
/// <returns></returns>
private static async Task LaunchAsync(string[] args)
{
restart:
ITranslationLookup translationLookup = null;
try
{
ServerManager = ApplicationManager.GetInstance();
var configuration = ServerManager.GetApplicationSettings().Configuration();
// do any needed housekeeping file/folder migrations
ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories();
if (configuration != null)
var services = ConfigureServices(args);
serviceProvider = services.BuildServiceProvider();
ServerManager = (ApplicationManager)serviceProvider.GetRequiredService<IManager>();
translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>();
ServerManager.Logger.WriteInfo(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_VERSION"].FormatExt(Version));
await CheckVersion(translationLookup);
await ServerManager.Init();
}
catch (Exception e)
{
Localization.Configure.Initialize(configuration.EnableCustomLocale ? (configuration.CustomLocale ?? "windows-1252") : "windows-1252");
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
string exitMessage = translationLookup == null ? "Press enter to exit..." : translationLookup["MANAGER_EXIT"];
Console.WriteLine(failMessage);
while (e.InnerException != null)
{
e = e.InnerException;
}
if (e is ConfigurationException configException)
{
if (translationLookup != null)
{
Console.WriteLine(translationLookup[configException.Message].FormatExt(configException.ConfigurationFileName));
}
foreach (string error in configException.Errors)
{
Console.WriteLine(error);
}
}
else
{
Localization.Configure.Initialize();
Console.WriteLine(e.Message);
}
loc = Utilities.CurrentLocalization.LocalizationIndex;
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
Console.WriteLine(exitMessage);
await Console.In.ReadAsync(new char[1], 0, 1);
return;
}
CheckDirectories();
// do any needed migrations
// todo: move out
ConfigurationMigration.MoveConfigFolder10518(null);
try
{
ApplicationTask = RunApplicationTasksAsync();
await ApplicationTask;
}
ServerManager.Logger.WriteInfo($"Version is {Version}");
catch { }
if (ServerManager.IsRestartRequested)
{
goto restart;
}
serviceProvider.Dispose();
}
/// <summary>
/// runs the core application tasks
/// </summary>
/// <returns></returns>
private static async Task RunApplicationTasksAsync()
{
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ?
WebfrontCore.Program.Init(ServerManager, serviceProvider, ServerManager.CancellationToken) :
Task.CompletedTask;
// we want to run this one on a manual thread instead of letting the thread pool handle it,
// because we can't exit early from waiting on console input, and it prevents us from restarting
var inputThread = new Thread(async () => await ReadConsoleInput());
inputThread.Start();
var tasks = new[]
{
ServerManager.Start(),
webfrontTask,
};
await Task.WhenAll(tasks);
ServerManager.Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
}
/// <summary>
/// checks for latest version of the application
/// notifies user if an update is available
/// </summary>
/// <returns></returns>
private static async Task CheckVersion(ITranslationLookup translationLookup)
{
var api = API.Master.Endpoint.Get();
var loc = translationLookup;
var version = new API.Master.VersionInfo()
{
CurrentVersionStable = 99.99f
CurrentVersionStable = _fallbackVersion
};
try
{
version = api.GetVersion().Result;
version = await api.GetVersion(1);
}
catch (Exception e)
@ -79,7 +193,7 @@ namespace IW4MAdmin.Application
ServerManager.Logger.WriteDebug(e.Message);
}
if (version.CurrentVersionStable == 99.99f)
if (version.CurrentVersionStable == _fallbackVersion)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
@ -90,16 +204,16 @@ namespace IW4MAdmin.Application
else if (version.CurrentVersionStable > Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString("0.0")}]");
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString("0.0")}]"));
Console.WriteLine($"IW4MAdmin {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString()}]");
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString()}]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#else
else if (version.CurrentVersionPrerelease > Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin-Prerelease {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString("0.0")}-pr]");
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString("0.0")}-pr]"));
Console.WriteLine($"IW4MAdmin-Prerelease {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString()}-pr]");
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString()}-pr]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#endif
@ -109,97 +223,131 @@ namespace IW4MAdmin.Application
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
}
ServerManager.Init().Wait();
var consoleTask = Task.Run(async () =>
/// <summary>
/// reads input from the console and executes entered commands on the default server
/// </summary>
/// <returns></returns>
private static async Task ReadConsoleInput()
{
string userInput;
string lastCommand;
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
do
try
{
userInput = Console.ReadLine();
if (userInput?.ToLower() == "quit")
while (!ServerManager.CancellationToken.IsCancellationRequested)
{
ServerManager.Stop();
}
lastCommand = await Console.In.ReadLineAsync();
if (ServerManager.Servers.Count == 0)
if (lastCommand?.Length > 0)
{
Console.WriteLine(loc["MANAGER_CONSOLE_NOSERV"]);
continue;
}
if (userInput?.Length > 0)
if (lastCommand?.Length > 0)
{
GameEvent E = new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = userInput,
Data = lastCommand,
Origin = Origin,
Owner = ServerManager.Servers[0]
};
ServerManager.GetEventHandler().AddEvent(E);
await E.WaitAsync(30 * 1000);
}
ServerManager.AddEvent(E);
await E.WaitAsync(Utilities.DefaultCommandTimeout, ServerManager.CancellationToken);
Console.Write('>');
} while (ServerManager.Running);
});
}
}
}
}
catch (OperationCanceledException)
{ }
}
catch (Exception e)
/// <summary>
/// Configures the dependency injection services
/// </summary>
private static IServiceCollection ConfigureServices(string[] args)
{
string failMessage = loc == null ? "Failed to initalize IW4MAdmin" : loc["MANAGER_INIT_FAIL"];
string exitMessage = loc == null ? "Press any key to exit..." : loc["MANAGER_EXIT"];
var defaultLogger = new Logger("IW4MAdmin-Manager");
var pluginImporter = new PluginImporter(defaultLogger);
Console.WriteLine(failMessage);
while (e.InnerException != null)
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection)
.AddSingleton(new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings") as IConfigurationHandler<ApplicationConfiguration>)
.AddSingleton(new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as IConfigurationHandler<CommandConfiguration>)
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration())
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>().Configuration() ?? new CommandConfiguration())
.AddSingleton<ILogger>(_serviceProvider => defaultLogger)
.AddSingleton<IPluginImporter, PluginImporter>()
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
.AddSingleton<IRConConnectionFactory, RConConnectionFactory>()
.AddSingleton<IGameServerInstanceFactory, GameServerInstanceFactory>()
.AddSingleton<IConfigurationHandlerFactory, ConfigurationHandlerFactory>()
.AddSingleton<IParserRegexFactory, ParserRegexFactory>()
.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>()
.AddSingleton<IGameLogReaderFactory, GameLogReaderFactory>()
.AddSingleton<IScriptCommandFactory, ScriptCommandFactory>()
.AddSingleton<IAuditInformationRepository, AuditInformationRepository>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton(_serviceProvider =>
{
e = e.InnerException;
var config = _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration();
return Localization.Configure.Initialize(useLocalTranslation: config?.UseLocalTranslations ?? false,
customLocale: config?.EnableCustomLocale ?? false ? (config.CustomLocale ?? "en-US") : "en-US");
})
.AddSingleton<IManager, ApplicationManager>();
if (args.Contains("serialevents"))
{
serviceCollection.AddSingleton<IEventHandler, SerialGameEventHandler>();
}
Console.WriteLine(e.Message);
Console.WriteLine(exitMessage);
Console.ReadKey();
return;
else
{
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
}
if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront)
// register the native commands
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
.Where(_command => _command.BaseType == typeof(Command)))
{
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
defaultLogger.WriteInfo($"Registered native command type {commandType.Name}");
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
OnShutdownComplete.Reset();
ServerManager.Start();
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
OnShutdownComplete.Set();
// register the plugin implementations
var pluginImplementations = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in pluginImplementations.Item1)
{
defaultLogger.WriteInfo($"Registered plugin type {pluginType.FullName}");
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
}
private static void OnCancelKey(object sender, ConsoleCancelEventArgs e)
// register the plugin commands
foreach (var commandType in pluginImplementations.Item2)
{
ServerManager.Stop();
OnShutdownComplete.Wait();
defaultLogger.WriteInfo($"Registered plugin command type {commandType.FullName}");
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
static void CheckDirectories()
// register any script plugins
foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins())
{
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Plugins")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Plugins"));
serviceCollection.AddSingleton(scriptPlugin);
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Database")))
// register any eventable types
foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))
.Union(pluginImplementations
.Item1.SelectMany(_asm => _asm.Assembly.GetTypes())
.Distinct()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Database"));
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
serviceCollection.AddSingleton(instance);
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Log")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log"));
}
return serviceCollection;
}
}
}

View File

@ -15,6 +15,32 @@ namespace IW4MAdmin.Application.Migration
/// </summary>
class ConfigurationMigration
{
/// <summary>
/// ensures required directories are created
/// </summary>
public static void CheckDirectories()
{
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Plugins")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Plugins"));
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Database")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Database"));
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Log")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log"));
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Localization")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Localization"));
}
}
/// <summary>
/// moves existing configs from the root folder into a configs folder
/// </summary>

View File

@ -0,0 +1,72 @@
using Newtonsoft.Json;
using SharedLibraryCore;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using System;
using System.IO;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// default implementation of IConfigurationHandler
/// </summary>
/// <typeparam name="T">base configuration type</typeparam>
public class BaseConfigurationHandler<T> : IConfigurationHandler<T> where T : IBaseConfiguration
{
T _configuration;
public BaseConfigurationHandler(string fn)
{
FileName = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{fn}.json");
Build();
}
public string FileName { get; }
public void Build()
{
try
{
var configContent = File.ReadAllText(FileName);
_configuration = JsonConvert.DeserializeObject<T>(configContent);
}
catch (FileNotFoundException)
{
_configuration = default;
}
catch (Exception e)
{
throw new ConfigurationException("MANAGER_CONFIGURATION_ERROR")
{
Errors = new[] { e.Message },
ConfigurationFileName = FileName
};
}
}
public Task Save()
{
var settings = new JsonSerializerSettings()
{
Formatting = Formatting.Indented
};
settings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
var appConfigJSON = JsonConvert.SerializeObject(_configuration, settings);
return File.WriteAllTextAsync(FileName, appConfigJSON);
}
public T Configuration()
{
return _configuration;
}
public void Set(T config)
{
_configuration = config;
}
}
}

View File

@ -0,0 +1,27 @@
using Newtonsoft.Json;
using SharedLibraryCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application.Misc
{
public class EventLog : Dictionary<long, IList<GameEvent>>
{
private static JsonSerializerSettings serializationSettings;
public static JsonSerializerSettings BuildVcrSerializationSettings()
{
if (serializationSettings == null)
{
serializationSettings = new JsonSerializerSettings() { Formatting = Formatting.Indented, ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
serializationSettings.Converters.Add(new IPAddressConverter());
serializationSettings.Converters.Add(new IPEndPointConverter());
serializationSettings.Converters.Add(new GameEventConverter());
serializationSettings.Converters.Add(new ClientEntityConverter());
}
return serializationSettings;
}
}
}

View File

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

View File

@ -0,0 +1,45 @@
using System.Runtime.InteropServices;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// dto class for handling log path generation
/// </summary>
public class LogPathGeneratorInfo
{
/// <summary>
/// directory under the paths where data comes from by default
/// <remarks>fs_basegame</remarks>
/// </summary>
public string BaseGameDirectory { get; set; } = "";
/// <summary>
/// base game root path
/// <remarks>fs_basepath</remarks>
/// </summary>
public string BasePathDirectory { get; set; } = "";
/// <summary>
/// overide game directory
/// <remarks>plugin driven</remarks>
/// </summary>
public string GameDirectory { get; set; } = "";
/// <summary>
/// game director
/// <remarks>fs_game</remarks>
/// </summary>
public string ModDirectory { get; set; } = "";
/// <summary>
/// log file name
/// <remarks>g_log</remarks>
/// </summary>
public string LogFile { get; set; } = "";
/// <summary>
/// indicates if running on windows
/// </summary>
public bool IsWindows { get; set; } = true;
}
}

View File

@ -1,12 +1,14 @@
using SharedLibraryCore;
using IW4MAdmin.Application.IO;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
namespace IW4MAdmin.Application
{
class Logger : SharedLibraryCore.Interfaces.ILogger
public class Logger : ILogger
{
enum LogType
{
@ -19,16 +21,21 @@ namespace IW4MAdmin.Application
}
readonly string FileName;
readonly SemaphoreSlim OnLogWriting;
readonly ReaderWriterLockSlim WritingLock;
static readonly short MAX_LOG_FILES = 10;
public Logger(string fn)
{
FileName = Path.Join(Utilities.OperatingDirectory, "Log", $"{fn}.log");
OnLogWriting = new SemaphoreSlim(1, 1);
WritingLock = new ReaderWriterLockSlim();
RotateLogs();
}
~Logger()
{
WritingLock.Dispose();
}
/// <summary>
/// rotates logs when log is initialized
/// </summary>
@ -55,9 +62,10 @@ namespace IW4MAdmin.Application
void Write(string msg, LogType type)
{
OnLogWriting.Wait();
WritingLock.EnterWriteLock();
string stringType = type.ToString();
msg = msg.StripColors();
try
{
@ -71,12 +79,12 @@ namespace IW4MAdmin.Application
{
#if DEBUG
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
Console.WriteLine(LogLine);
File.AppendAllText(FileName, LogLine + Environment.NewLine);
Console.WriteLine(msg);
#else
if (type == LogType.Error || type == LogType.Verbose)
{
Console.WriteLine(LogLine);
//if (type != LogType.Debug)
}
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
#endif
}
@ -87,7 +95,7 @@ namespace IW4MAdmin.Application
Console.WriteLine(ex.GetExceptionInfo());
}
OnLogWriting.Release(1);
WritingLock.ExitWriteLock();
}
public void WriteVerbose(string msg)

View File

@ -0,0 +1,74 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Misc
{
class MiddlewareActionHandler : IMiddlewareActionHandler
{
private readonly IDictionary<string, IList<object>> _actions;
private readonly ILogger _logger;
public MiddlewareActionHandler(ILogger logger)
{
_actions = new Dictionary<string, IList<object>>();
_logger = logger;
}
/// <summary>
/// Executes the action with the given name
/// </summary>
/// <typeparam name="T">Execution return type</typeparam>
/// <param name="value">Input value</param>
/// <param name="name">Name of action to execute</param>
/// <returns></returns>
public async Task<T> Execute<T>(T value, string name = null)
{
string key = string.IsNullOrEmpty(name) ? typeof(T).ToString() : name;
if (_actions.ContainsKey(key))
{
foreach (var action in _actions[key])
{
try
{
value = await ((IMiddlewareAction<T>)action).Invoke(value);
}
catch (Exception e)
{
_logger.WriteWarning($"Failed to invoke middleware action {name}");
_logger.WriteDebug(e.GetExceptionInfo());
}
}
return value;
}
return value;
}
/// <summary>
/// Registers an action by name
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="actionType">Action type specifier</param>
/// <param name="action">Action to perform</param>
/// <param name="name">Name of action</param>
public void Register<T>(T actionType, IMiddlewareAction<T> action, string name = null)
{
string key = string.IsNullOrEmpty(name) ? typeof(T).ToString() : name;
if (_actions.ContainsKey(key))
{
_actions[key].Add(action);
}
else
{
_actions.Add(key, new[] { action });
}
}
}
}

View File

@ -0,0 +1,21 @@
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of the IMatchResult
/// used to hold matching results
/// </summary>
public class ParserMatchResult : IMatchResult
{
/// <summary>
/// array of matched pattern groups
/// </summary>
public string[] Values { get; set; }
/// <summary>
/// indicates if the match succeeded
/// </summary>
public bool Success { get; set; }
}
}

View File

@ -0,0 +1,88 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using SharedLibraryCore.Interfaces;
using System.Linq;
using SharedLibraryCore;
using IW4MAdmin.Application.Misc;
namespace IW4MAdmin.Application.Helpers
{
/// <summary>
/// implementation of IPluginImporter
/// discovers plugins and script plugins
/// </summary>
public class PluginImporter : IPluginImporter
{
private static readonly string PLUGIN_DIR = "Plugins";
private readonly ILogger _logger;
public PluginImporter(ILogger logger)
{
_logger = logger;
}
/// <summary>
/// discovers all the script plugins in the plugins dir
/// </summary>
/// <returns></returns>
public IEnumerable<IPlugin> DiscoverScriptPlugins()
{
string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
if (Directory.Exists(pluginDir))
{
string[] scriptPluginFiles = Directory.GetFiles(pluginDir, "*.js");
_logger.WriteInfo($"Discovered {scriptPluginFiles.Length} potential script plugins");
if (scriptPluginFiles.Length > 0)
{
foreach (string fileName in scriptPluginFiles)
{
_logger.WriteInfo($"Discovered script plugin {fileName}");
var plugin = new ScriptPlugin(fileName);
yield return plugin;
}
}
}
}
/// <summary>
/// discovers all the C# assembly plugins and commands
/// </summary>
/// <returns></returns>
public (IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
{
string pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
var pluginTypes = Enumerable.Empty<Type>();
var commandTypes = Enumerable.Empty<Type>();
if (Directory.Exists(pluginDir))
{
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
_logger.WriteInfo($"Discovered {dllFileNames.Length} potential plugin assemblies");
if (dllFileNames.Length > 0)
{
var assemblies = dllFileNames.Select(_name => Assembly.LoadFrom(_name));
pluginTypes = assemblies
.SelectMany(_asm => _asm.GetTypes())
.Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null);
_logger.WriteInfo($"Discovered {pluginTypes.Count()} plugin implementations");
commandTypes = assemblies
.SelectMany(_asm => _asm.GetTypes())
.Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command));
_logger.WriteInfo($"Discovered {commandTypes.Count()} plugin commands");
}
}
return (pluginTypes, commandTypes);
}
}
}

View File

@ -0,0 +1,41 @@
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Threading.Tasks;
using static SharedLibraryCore.Database.Models.EFClient;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// generic script command implementation
/// </summary>
public class ScriptCommand : Command
{
private readonly Action<GameEvent> _executeAction;
public ScriptCommand(string name, string alias, string description, Permission permission,
CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout)
: base(config, layout)
{
_executeAction = executeAction;
Name = name;
Alias = alias;
Description = description;
Permission = permission;
Arguments = args;
}
public override Task ExecuteAsync(GameEvent E)
{
if (_executeAction == null)
{
throw new InvalidOperationException($"No execute action defined for command \"{Name}\"");
}
return Task.Run(() => _executeAction(E));
}
}
}

View File

@ -0,0 +1,293 @@
using Jint;
using Jint.Native;
using Jint.Runtime;
using Microsoft.CSharp.RuntimeBinder;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IPlugin
/// used to proxy script plugin requests
/// </summary>
public class ScriptPlugin : IPlugin
{
public string Name { get; set; }
public float Version { get; set; }
public string Author { get; set; }
/// <summary>
/// indicates if the plugin is a parser
/// </summary>
public bool IsParser { get; private set; }
public FileSystemWatcher Watcher { get; private set; }
private Engine _scriptEngine;
private readonly string _fileName;
private readonly SemaphoreSlim _onProcessing;
private bool successfullyLoaded;
private readonly List<string> _registeredCommandNames;
public ScriptPlugin(string filename, string workingDirectory = null)
{
_fileName = filename;
Watcher = new FileSystemWatcher()
{
Path = workingDirectory == null ? $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}" : workingDirectory,
NotifyFilter = NotifyFilters.Size,
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
};
Watcher.EnableRaisingEvents = true;
_onProcessing = new SemaphoreSlim(1, 1);
_registeredCommandNames = new List<string>();
}
~ScriptPlugin()
{
Watcher.Dispose();
_onProcessing.Dispose();
}
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory)
{
await _onProcessing.WaitAsync();
try
{
// for some reason we get an event trigger when the file is not finished being modified.
// this must have been a change in .NET CORE 3.x
// so if the new file is empty we can't process it yet
if (new FileInfo(_fileName).Length == 0L)
{
return;
}
bool firstRun = _scriptEngine == null;
// it's been loaded before so we need to call the unload event
if (!firstRun)
{
await OnUnloadAsync();
foreach (string commandName in _registeredCommandNames)
{
manager.GetLogger(0).WriteDebug($"Removing plugin registered command \"{commandName}\"");
manager.RemoveCommandByName(commandName);
}
_registeredCommandNames.Clear();
}
successfullyLoaded = false;
string script;
using (var stream = new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (var reader = new StreamReader(stream, Encoding.Default))
{
script = await reader.ReadToEndAsync();
}
}
_scriptEngine = new Engine(cfg =>
cfg.AllowClr(new[]
{
typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly,
typeof(Utilities).Assembly,
typeof(Encoding).Assembly
})
.CatchClrExceptions());
_scriptEngine.Execute(script);
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
Author = pluginObject.author;
Name = pluginObject.name;
Version = (float)pluginObject.version;
var commands = _scriptEngine.GetValue("commands");
if (commands != JsValue.Undefined)
{
try
{
foreach (var command in GenerateScriptCommands(commands, scriptCommandFactory))
{
manager.GetLogger(0).WriteDebug($"Adding plugin registered command \"{command.Name}\"");
manager.AddAdditionalCommand(command);
_registeredCommandNames.Add(command.Name);
}
}
catch (RuntimeBinderException e)
{
throw new PluginException($"Not all required fields were found: {e.Message}") { PluginFile = _fileName };
}
}
await OnLoadAsync(manager);
try
{
if (pluginObject.isParser)
{
IsParser = true;
IEventParser eventParser = (IEventParser)_scriptEngine.GetValue("eventParser").ToObject();
IRConParser rconParser = (IRConParser)_scriptEngine.GetValue("rconParser").ToObject();
manager.AdditionalEventParsers.Add(eventParser);
manager.AdditionalRConParsers.Add(rconParser);
}
}
catch (RuntimeBinderException) { }
if (!firstRun)
{
await OnLoadAsync(manager);
}
successfullyLoaded = true;
}
catch
{
throw;
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
public async Task OnEventAsync(GameEvent E, Server S)
{
if (successfullyLoaded)
{
await _onProcessing.WaitAsync();
try
{
_scriptEngine.SetValue("_gameEvent", E);
_scriptEngine.SetValue("_server", S);
_scriptEngine.SetValue("_IW4MAdminClient", Utilities.IW4MAdminClient(S));
_scriptEngine.Execute("plugin.onEventAsync(_gameEvent, _server)").GetCompletionValue();
}
catch
{
throw;
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
}
public Task OnLoadAsync(IManager manager)
{
manager.GetLogger(0).WriteDebug($"OnLoad executing for {Name}");
_scriptEngine.SetValue("_manager", manager);
return Task.FromResult(_scriptEngine.Execute("plugin.onLoadAsync(_manager)").GetCompletionValue());
}
public Task OnTickAsync(Server S)
{
_scriptEngine.SetValue("_server", S);
return Task.FromResult(_scriptEngine.Execute("plugin.onTickAsync(_server)").GetCompletionValue());
}
public async Task OnUnloadAsync()
{
if (successfullyLoaded)
{
await Task.FromResult(_scriptEngine.Execute("plugin.onUnloadAsync()").GetCompletionValue());
}
}
/// <summary>
/// finds declared script commands in the script plugin
/// </summary>
/// <param name="commands">commands value from jint parser</param>
/// <param name="scriptCommandFactory">factory to create the command from</param>
/// <returns></returns>
public IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory)
{
List<IManagerCommand> commandList = new List<IManagerCommand>();
// go through each defined command
foreach (var command in commands.AsArray())
{
dynamic dynamicCommand = command.ToObject();
string name = dynamicCommand.name;
string alias = dynamicCommand.alias;
string description = dynamicCommand.description;
string permission = dynamicCommand.permission;
List<(string, bool)> args = new List<(string, bool)>();
dynamic arguments = null;
try
{
arguments = dynamicCommand.arguments;
}
catch (RuntimeBinderException)
{
// arguments are optional
}
if (arguments != null)
{
foreach (var arg in dynamicCommand.arguments)
{
args.Add((arg.name, (bool)arg.required));
}
}
void execute(GameEvent e)
{
_scriptEngine.SetValue("_event", e);
var jsEventObject = _scriptEngine.GetValue("_event");
try
{
dynamicCommand.execute.Target.Invoke(jsEventObject);
}
catch (JavaScriptException ex)
{
throw new PluginException($"An error occured while executing action for script plugin: {ex.Error} (Line: {ex.Location.Start.Line}, Character: {ex.Location.Start.Column})") { PluginFile = _fileName };
}
}
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, args, execute));
}
return commandList;
}
}
}

View File

@ -0,0 +1,149 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using System;
using System.Net;
using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.GameEvent;
namespace IW4MAdmin.Application.Misc
{
class IPAddressConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(IPAddress));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return IPAddress.Parse((string)reader.Value);
}
}
class IPEndPointConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(IPEndPoint));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
IPEndPoint ep = (IPEndPoint)value;
JObject jo = new JObject();
jo.Add("Address", JToken.FromObject(ep.Address, serializer));
jo.Add("Port", ep.Port);
jo.WriteTo(writer);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
IPAddress address = jo["Address"].ToObject<IPAddress>(serializer);
int port = (int)jo["Port"];
return new IPEndPoint(address, port);
}
}
class ClientEntityConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(EFClient);
public override object ReadJson(JsonReader reader, Type objectType,object existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
{
return null;
}
var jsonObject = JObject.Load(reader);
return new EFClient
{
NetworkId = (long)jsonObject["NetworkId"],
ClientNumber = (int)jsonObject["ClientNumber"],
State = Enum.Parse<ClientState>(jsonObject["state"].ToString()),
CurrentAlias = new EFAlias()
{
IPAddress = (int?)jsonObject["IPAddress"],
Name = jsonObject["Name"].ToString()
}
};
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var client = value as EFClient;
var jsonObject = new JObject
{
{ "NetworkId", client.NetworkId },
{ "ClientNumber", client.ClientNumber },
{ "IPAddress", client.CurrentAlias?.IPAddress },
{ "Name", client.CurrentAlias?.Name },
{ "State", (int)client.State }
};
jsonObject.WriteTo(writer);
}
}
class GameEventConverter : JsonConverter
{
public override bool CanConvert(Type objectType) =>objectType == typeof(GameEvent);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jsonObject = JObject.Load(reader);
return new GameEvent
{
Type = Enum.Parse<EventType>(jsonObject["Type"].ToString()),
Subtype = jsonObject["Subtype"]?.ToString(),
Source = Enum.Parse<EventSource>(jsonObject["Source"].ToString()),
RequiredEntity = Enum.Parse<EventRequiredEntity>(jsonObject["RequiredEntity"].ToString()),
Data = jsonObject["Data"].ToString(),
Message = jsonObject["Message"].ToString(),
GameTime = (int?)jsonObject["GameTime"],
Origin = jsonObject["Origin"]?.ToObject<EFClient>(serializer),
Target = jsonObject["Target"]?.ToObject<EFClient>(serializer),
ImpersonationOrigin = jsonObject["ImpersonationOrigin"]?.ToObject<EFClient>(serializer),
IsRemote = (bool)jsonObject["IsRemote"],
Extra = null, // fix
Time = (DateTime)jsonObject["Time"],
IsBlocking = (bool)jsonObject["IsBlocking"]
};
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var gameEvent = value as GameEvent;
var jsonObject = new JObject
{
{ "Type", (int)gameEvent.Type },
{ "Subtype", gameEvent.Subtype },
{ "Source", (int)gameEvent.Source },
{ "RequiredEntity", (int)gameEvent.RequiredEntity },
{ "Data", gameEvent.Data },
{ "Message", gameEvent.Message },
{ "GameTime", gameEvent.GameTime },
{ "Origin", gameEvent.Origin != null ? JToken.FromObject(gameEvent.Origin, serializer) : null },
{ "Target", gameEvent.Target != null ? JToken.FromObject(gameEvent.Target, serializer) : null },
{ "ImpersonationOrigin", gameEvent.ImpersonationOrigin != null ? JToken.FromObject(gameEvent.ImpersonationOrigin, serializer) : null},
{ "IsRemote", gameEvent.IsRemote },
{ "Extra", gameEvent.Extra?.ToString() },
{ "Time", gameEvent.Time },
{ "IsBlocking", gameEvent.IsBlocking }
};
jsonObject.WriteTo(writer);
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;
namespace IW4MAdmin.Application.RCon
{
/// <summary>
/// used to keep track of the udp connection state
/// </summary>
internal class ConnectionState
{
~ConnectionState()
{
OnComplete.Dispose();
OnSentData.Dispose();
OnReceivedData.Dispose();
}
public int ConnectionAttempts { get; set; }
const int BufferSize = 4096;
public readonly byte[] ReceiveBuffer = new byte[BufferSize];
public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1);
public readonly ManualResetEventSlim OnSentData = new ManualResetEventSlim(false);
public readonly ManualResetEventSlim OnReceivedData = new ManualResetEventSlim(false);
public List<int> BytesReadPerSegment { get; set; } = new List<int>();
public SocketAsyncEventArgs SendEventArgs { get; set; } = new SocketAsyncEventArgs();
public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new SocketAsyncEventArgs();
public DateTime LastQuery { get; set; } = DateTime.Now;
}
}

View File

@ -0,0 +1,363 @@
using SharedLibraryCore;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.RCon
{
/// <summary>
/// implementation of IRConConnection
/// </summary>
public class RConConnection : IRConConnection
{
static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new ConcurrentDictionary<EndPoint, ConnectionState>();
public IPEndPoint Endpoint { get; private set; }
public string RConPassword { get; private set; }
private IRConParserConfiguration config;
private readonly ILogger _log;
private readonly Encoding _gameEncoding;
public RConConnection(string ipAddress, int port, string password, ILogger log, Encoding gameEncoding)
{
Endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
_gameEncoding = gameEncoding;
RConPassword = password;
_log = log;
}
public void SetConfiguration(IRConParserConfiguration config)
{
this.config = config;
}
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "")
{
if (!ActiveQueries.ContainsKey(this.Endpoint))
{
ActiveQueries.TryAdd(this.Endpoint, new ConnectionState());
}
var connectionState = ActiveQueries[this.Endpoint];
#if DEBUG == true
_log.WriteDebug($"Waiting for semaphore to be released [{this.Endpoint}]");
#endif
// enter the semaphore so only one query is sent at a time per server.
await connectionState.OnComplete.WaitAsync();
var timeSinceLastQuery = (DateTime.Now - connectionState.LastQuery).TotalMilliseconds;
if (timeSinceLastQuery < StaticHelpers.FloodProtectionInterval)
{
await Task.Delay(StaticHelpers.FloodProtectionInterval - (int)timeSinceLastQuery);
}
connectionState.LastQuery = DateTime.Now;
#if DEBUG == true
_log.WriteDebug($"Semaphore has been released [{this.Endpoint}]");
_log.WriteDebug($"Query [{this.Endpoint},{type.ToString()},{parameters}]");
#endif
byte[] payload = null;
bool waitForResponse = config.WaitForResponse;
string convertEncoding(string text)
{
byte[] convertedBytes = Utilities.EncodingType.GetBytes(text);
return _gameEncoding.GetString(convertedBytes);
}
try
{
string convertedRConPassword = convertEncoding(RConPassword);
string convertedParameters = convertEncoding(parameters);
switch (type)
{
case StaticHelpers.QueryType.GET_DVAR:
waitForResponse |= true;
payload = string.Format(config.CommandPrefixes.RConGetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.SET_DVAR:
payload = string.Format(config.CommandPrefixes.RConSetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.COMMAND:
payload = string.Format(config.CommandPrefixes.RConCommand, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.GET_STATUS:
waitForResponse |= true;
payload = (config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.GET_INFO:
waitForResponse |= true;
payload = (config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray();
break;
case StaticHelpers.QueryType.COMMAND_STATUS:
waitForResponse |= true;
payload = string.Format(config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0").Select(Convert.ToByte).ToArray();
break;
}
}
// this happens when someone tries to send something that can't be converted into a 7 bit character set
// e.g: emoji -> windows-1252
catch (OverflowException)
{
connectionState.OnComplete.Release(1);
throw new NetworkException($"Invalid character encountered when converting encodings - {parameters}");
}
byte[][] response = null;
retrySend:
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
{
DontFragment = true,
Ttl = 100,
ExclusiveAddressUse = true,
})
{
connectionState.SendEventArgs.UserToken = socket;
connectionState.OnSentData.Reset();
connectionState.OnReceivedData.Reset();
connectionState.ConnectionAttempts++;
connectionState.BytesReadPerSegment.Clear();
#if DEBUG == true
_log.WriteDebug($"Sending {payload.Length} bytes to [{this.Endpoint}] ({connectionState.ConnectionAttempts}/{StaticHelpers.AllowedConnectionFails})");
#endif
try
{
response = await SendPayloadAsync(payload, waitForResponse);
if ((response.Length == 0 || response[0].Length == 0) && waitForResponse)
{
throw new NetworkException("Expected response but got 0 bytes back");
}
connectionState.ConnectionAttempts = 0;
}
catch
{
if (connectionState.ConnectionAttempts < StaticHelpers.AllowedConnectionFails)
{
await Task.Delay(StaticHelpers.FloodProtectionInterval);
goto retrySend;
}
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"].FormatExt(Endpoint));
}
finally
{
if (connectionState.OnComplete.CurrentCount == 0)
{
connectionState.OnComplete.Release(1);
}
}
}
if (response.Length == 0)
{
_log.WriteWarning($"Received empty response for request [{type.ToString()}, {parameters}, {Endpoint.ToString()}]");
return new string[0];
}
string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ?
ReassembleSegmentedStatus(response) :
_gameEncoding.GetString(response[0]) + '\n';
// note: not all games respond if the pasword is wrong or not set
if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword"))
{
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]);
}
if (responseString.Contains("rcon_password"))
{
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_NOTSET"]);
}
if (responseString.Contains(config.ServerNotRunningResponse))
{
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString()));
}
string responseHeaderMatch = Regex.Match(responseString, config.CommandPrefixes.RConResponse).Value;
string[] headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : responseHeaderMatch);
if (headerSplit.Length != 2)
{
throw new NetworkException("Unexpected response header from server");
}
string[] splitResponse = headerSplit.Last().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
return splitResponse;
}
/// <summary>
/// reassembles broken status segments into the 'correct' ordering
/// <remarks>this is primarily for T7, and is really only reliable for 2 segments</remarks>
/// </summary>
/// <param name="segments">array of segmented byte arrays</param>
/// <returns></returns>
public string ReassembleSegmentedStatus(byte[][] segments)
{
var splitStatusStrings = new List<string>();
foreach (byte[] segment in segments)
{
string responseString = _gameEncoding.GetString(segment, 0, segment.Length);
var statusHeaderMatch = config.StatusHeader.PatternMatcher.Match(responseString);
if (statusHeaderMatch.Success)
{
splitStatusStrings.Insert(0, responseString.TrimEnd('\0'));
}
else
{
splitStatusStrings.Add(responseString.Replace(config.CommandPrefixes.RConResponse, "").TrimEnd('\0'));
}
}
return string.Join("", splitStatusStrings);
}
private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse)
{
var connectionState = ActiveQueries[this.Endpoint];
var rconSocket = (Socket)connectionState.SendEventArgs.UserToken;
if (connectionState.ReceiveEventArgs.RemoteEndPoint == null &&
connectionState.SendEventArgs.RemoteEndPoint == null)
{
// setup the event handlers only once because we're reusing the event args
connectionState.SendEventArgs.Completed += OnDataSent;
connectionState.ReceiveEventArgs.Completed += OnDataReceived;
connectionState.SendEventArgs.RemoteEndPoint = this.Endpoint;
connectionState.ReceiveEventArgs.RemoteEndPoint = this.Endpoint;
connectionState.ReceiveEventArgs.DisconnectReuseSocket = true;
connectionState.SendEventArgs.DisconnectReuseSocket = true;
}
connectionState.SendEventArgs.SetBuffer(payload);
// send the data to the server
bool sendDataPending = rconSocket.SendToAsync(connectionState.SendEventArgs);
if (sendDataPending)
{
// the send has not been completed asyncronously
if (!await Task.Run(() => connectionState.OnSentData.Wait(StaticHelpers.SocketTimeout)))
{
rconSocket.Close();
throw new NetworkException("Timed out sending data", rconSocket);
}
}
if (!waitForResponse)
{
return new byte[0][];
}
connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer);
// get our response back
bool receiveDataPending = rconSocket.ReceiveFromAsync(connectionState.ReceiveEventArgs);
if (receiveDataPending)
{
if (!await Task.Run(() => connectionState.OnReceivedData.Wait(10000)))
{
rconSocket.Close();
throw new NetworkException("Timed out waiting for response", rconSocket);
}
}
rconSocket.Close();
var responseList = new List<byte[]>();
int totalBytesRead = 0;
foreach (int bytesRead in connectionState.BytesReadPerSegment)
{
responseList.Add(connectionState.ReceiveBuffer
.Skip(totalBytesRead)
.Take(bytesRead)
.ToArray());
totalBytesRead += bytesRead;
}
return responseList.ToArray();
}
private void OnDataReceived(object sender, SocketAsyncEventArgs e)
{
#if DEBUG == true
_log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}");
#endif
// this occurs when we close the socket
if (e.BytesTransferred == 0)
{
ActiveQueries[this.Endpoint].OnReceivedData.Set();
return;
}
if (sender is Socket sock)
{
var state = ActiveQueries[this.Endpoint];
state.BytesReadPerSegment.Add(e.BytesTransferred);
try
{
// we still have available data so the payload was segmented
if (sock.Available > 0)
{
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, e.BytesTransferred, state.ReceiveBuffer.Length - e.BytesTransferred);
if (!sock.ReceiveAsync(state.ReceiveEventArgs))
{
#if DEBUG == true
_log.WriteDebug($"Read {state.ReceiveEventArgs.BytesTransferred} synchronous bytes from {e.RemoteEndPoint.ToString()}");
#endif
// we need to increment this here because the callback isn't executed if there's no pending IO
state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred);
ActiveQueries[this.Endpoint].OnReceivedData.Set();
}
}
else
{
ActiveQueries[this.Endpoint].OnReceivedData.Set();
}
}
catch (ObjectDisposedException)
{
ActiveQueries[this.Endpoint].OnReceivedData.Set();
}
}
}
private void OnDataSent(object sender, SocketAsyncEventArgs e)
{
#if DEBUG == true
_log.WriteDebug($"Sent {e.Buffer?.Length} bytes to {e.ConnectSocket?.RemoteEndPoint?.ToString()}");
#endif
ActiveQueries[this.Endpoint].OnSentData.Set();
}
}
}

View File

@ -12,11 +12,13 @@ using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.RconParsers
{
class BaseRConParser : IRConParser
public class BaseRConParser : IRConParser
{
public BaseRConParser()
private const int MAX_FAULTY_STATUS_LINES = 7;
public BaseRConParser(IParserRegexFactory parserRegexFactory)
{
Configuration = new DynamicRConParserConfiguration()
Configuration = new DynamicRConParserConfiguration(parserRegexFactory)
{
CommandPrefixes = new CommandPrefix()
{
@ -31,10 +33,12 @@ namespace IW4MAdmin.Application.RconParsers
RConGetStatus = "ÿÿÿÿgetstatus",
RConGetInfo = "ÿÿÿÿgetinfo",
RConResponse = "ÿÿÿÿprint",
RconGetInfoResponseHeader = "ÿÿÿÿinfoResponse"
},
ServerNotRunningResponse = "Server is not running."
};
Configuration.Status.Pattern = @"^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) +(.{0,32}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$";
Configuration.Status.Pattern = @"^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback|unknown) +(-*[0-9]+) +([0-9]+) *$";
Configuration.Status.AddMapping(ParserRegex.GroupType.RConClientNumber, 1);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConScore, 2);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConPing, 3);
@ -48,130 +52,181 @@ namespace IW4MAdmin.Application.RconParsers
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5);
Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *";
Configuration.MapStatus.Pattern = @"map: (([a-z]|_|\d)+)";
Configuration.MapStatus.AddMapping(ParserRegex.GroupType.RConStatusMap, 1);
}
public IRConParserConfiguration Configuration { get; set; }
public string Version { get; set; } = "CoD";
public virtual string Version { get; set; } = "CoD";
public Game GameName { get; set; } = Game.COD;
public bool CanGenerateLogPath { get; set; } = true;
public string Name { get; set; } = "Call of Duty";
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command)
{
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
return response.Skip(1).ToArray();
}
public async Task<Dvar<T>> GetDvarAsync<T>(Connection connection, string dvarName)
public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default)
{
string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName);
string response = string.Join('\n', lineSplit.Skip(1));
if (!lineSplit[0].Contains(Configuration.CommandPrefixes.RConResponse))
{
throw new DvarException($"Could not retrieve DVAR \"{dvarName}\"");
}
if (response.Contains("Unknown command"))
{
throw new DvarException($"DVAR \"{dvarName}\" does not exist");
}
string response = string.Join('\n', lineSplit).TrimEnd('\0');
var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (!match.Success)
if (response.Contains("Unknown command") ||
!match.Success)
{
if (fallbackValue != null)
{
throw new DvarException($"Could not retrieve DVAR \"{dvarName}\"");
}
string value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value.StripColors();
string defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value.StripColors();
string latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value.StripColors();
return new Dvar<T>()
{
Name = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarName]].Value.StripColors(),
Value = string.IsNullOrEmpty(value) ? default(T) : (T)Convert.ChangeType(value, typeof(T)),
DefaultValue = string.IsNullOrEmpty(defaultValue) ? default(T) : (T)Convert.ChangeType(defaultValue, typeof(T)),
LatchedValue = string.IsNullOrEmpty(latchedValue) ? default(T) : (T)Convert.ChangeType(latchedValue, typeof(T)),
Domain = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDomain]].Value.StripColors()
Name = dvarName,
Value = fallbackValue
};
}
public async Task<List<EFClient>> GetStatusAsync(Connection connection)
{
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
return ClientsFromStatus(response);
throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName));
}
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
string value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value;
string defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value;
string latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value;
string removeTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", "");
value = removeTrailingColorCode(value);
defaultValue = removeTrailingColorCode(defaultValue);
latchedValue = removeTrailingColorCode(latchedValue);
return new Dvar<T>()
{
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, $"{dvarName} {dvarValue}")).Length > 0;
Name = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarName]].Value,
Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)),
DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)),
LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)),
Domain = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDomain]].Value
};
}
public virtual async Task<(List<EFClient>, string)> GetStatusAsync(IRConConnection connection)
{
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
#if DEBUG
foreach (var line in response)
{
Console.WriteLine(line);
}
#endif
return (ClientsFromStatus(response), MapFromStatus(response));
}
private string MapFromStatus(string[] response)
{
string map = null;
foreach (var line in response)
{
var regex = Regex.Match(line, Configuration.MapStatus.Pattern);
if (regex.Success)
{
map = regex.Groups[Configuration.MapStatus.GroupMapping[ParserRegex.GroupType.RConStatusMap]].ToString();
}
}
return map;
}
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue)
{
string dvarString = (dvarValue is string str)
? $"{dvarName} \"{str}\""
: $"{dvarName} {dvarValue.ToString()}";
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0;
}
private List<EFClient> ClientsFromStatus(string[] Status)
{
List<EFClient> StatusPlayers = new List<EFClient>();
if (Status.Length < 4)
{
throw new ServerException("Unexpected status response received");
}
int validMatches = 0;
bool parsedHeader = false;
foreach (string statusLine in Status)
{
string responseLine = statusLine.Trim();
var regex = Regex.Match(responseLine, Configuration.Status.Pattern, RegexOptions.IgnoreCase);
if (regex.Success)
if (Configuration.StatusHeader.PatternMatcher.Match(responseLine).Success)
{
validMatches++;
int clientNumber = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]].Value);
int score = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]].Value);
parsedHeader = true;
continue;
}
var match = Configuration.Status.PatternMatcher.Match(responseLine);
if (match.Success)
{
int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]);
int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]);
int ping = 999;
// their state can be CNCT, ZMBI etc
if (regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value.Length <= 3)
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3)
{
ping = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value);
ping = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]]);
}
long networkId = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].Value.ConvertLong();
string name = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].Value.StripColors().Trim();
int? ip = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Value.Split(':')[0].ConvertToIP();
long networkId;
string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
try
{
string networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkId = networkIdString.IsBotGuid() ?
name.GenerateGuidFromString() :
networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
}
catch (FormatException)
{
continue;
}
int? ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
var client = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = name
Name = name,
IPAddress = ip
},
NetworkId = networkId,
ClientNumber = clientNumber,
IPAddress = ip,
Ping = ping,
Score = score,
IsBot = ip == null,
State = EFClient.ClientState.Connecting
};
//// they've not fully connected yet
//if (!client.IsBot && ping == 999)
//#if DEBUG
// if (client.NetworkId < 1000 && client.NetworkId > 0)
// {
// continue;
// client.IPAddress = 2147483646;
// client.Ping = 0;
// }
//#endif
StatusPlayers.Add(client);
}
}
// this happens if status is requested while map is rotating
if (Status.Length > 5 && validMatches == 0)
// this can happen if status is requested while map is rotating and we get a log dump back
if (!parsedHeader)
{
throw new ServerException("Server is rotating map");
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNEXPECTED_STATUS"]);
}
return StatusPlayers;

View File

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

View File

@ -1,6 +1,7 @@
using SharedLibraryCore;
using IW4MAdmin.Application.Factories;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
using System.Globalization;
namespace IW4MAdmin.Application.RconParsers
{
@ -8,11 +9,23 @@ namespace IW4MAdmin.Application.RconParsers
/// generic implementation of the IRConParserConfiguration
/// allows script plugins to generate dynamic RCon configurations
/// </summary>
sealed internal class DynamicRConParserConfiguration : IRConParserConfiguration
public class DynamicRConParserConfiguration : IRConParserConfiguration
{
public CommandPrefix CommandPrefixes { get; set; }
public ParserRegex Status { get; set; } = new ParserRegex();
public ParserRegex Dvar { get; set; } = new ParserRegex();
public ParserRegex Status { get; set; }
public ParserRegex MapStatus { get; set; }
public ParserRegex Dvar { get; set; }
public ParserRegex StatusHeader { get; set; }
public string ServerNotRunningResponse { get; set; }
public bool WaitForResponse { get; set; } = true;
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
{
Status = parserRegexFactory.CreateParserRegex();
MapStatus = parserRegexFactory.CreateParserRegex();
Dvar = parserRegexFactory.CreateParserRegex();
StatusHeader = parserRegexFactory.CreateParserRegex();
}
}
}

View File

@ -0,0 +1,41 @@
using SharedLibraryCore;
using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
namespace IW4MAdmin.Application
{
class SerialGameEventHandler : IEventHandler
{
private delegate void GameEventAddedEventHandler(object sender, GameEventArgs args);
private event GameEventAddedEventHandler GameEventAdded;
private static readonly GameEvent.EventType[] overrideEvents = new[]
{
GameEvent.EventType.Connect,
GameEvent.EventType.Disconnect,
GameEvent.EventType.Quit,
GameEvent.EventType.Stop
};
public SerialGameEventHandler()
{
GameEventAdded += GameEventHandler_GameEventAdded;
}
private async void GameEventHandler_GameEventAdded(object sender, GameEventArgs args)
{
await (sender as IManager).ExecuteEvent(args.Event);
EventApi.OnGameEvent(args.Event);
}
public void HandleEvent(IManager manager, GameEvent gameEvent)
{
if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
{
GameEventAdded?.Invoke(manager, new GameEventArgs(null, false, gameEvent));
}
}
}
}

View File

@ -0,0 +1,53 @@
#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
#include maps\mp\gametypes\_playerlogic;
init()
{
SetDvarIfUninitialized( "sv_iw4madmin_command", "" );
level thread WaitForCommand();
}
WaitForCommand()
{
level endon( "game_ended" );
for(;;)
{
commandInfo = strtok( getDvar("sv_iw4madmin_command"), ";" );
command = commandInfo[0];
switch( command )
{
case "alert":
//clientId alertType sound message
SendAlert( commandInfo[1], commandInfo[2], commandInfo[3], commandInfo[4] );
break;
case "killplayer":
// clientId
KillPlayer( commandInfo[1], commandInfo[2] );
break;
}
setDvar( "sv_iw4madmin_command", "" );
wait( 1 );
}
}
SendAlert( clientId, alertType, sound, message )
{
client = getPlayerFromClientNum( int( clientId ) );
client thread playLeaderDialogOnPlayer( sound, client.team );
client playLocalSound( sound );
client iPrintLnBold( "^1" + alertType + ": ^3" + message );
}
KillPlayer( targetId, originId)
{
target = getPlayerFromClientNum( int( targetId ) );
target suicide();
origin = getPlayerFromClientNum( int( originId ) );
iPrintLnBold("^1" + origin.name + " ^7killed ^5" + target.name);
}

View File

@ -0,0 +1,256 @@
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
#include common_scripts\utility;
init()
{
SetDvarIfUninitialized( "sv_customcallbacks", true );
SetDvarIfUninitialized( "sv_framewaittime", 0.05 );
SetDvarIfUninitialized( "sv_additionalwaittime", 0.1 );
SetDvarIfUninitialized( "sv_maxstoredframes", 12 );
SetDvarIfUninitialized( "sv_printradarupdates", 0 );
SetDvarIfUninitialized( "sv_printradar_updateinterval", 500 );
SetDvarIfUninitialized( "sv_iw4madmin_url", "http://127.0.0.1:1624" );
level thread onPlayerConnect();
if (getDvarInt("sv_printradarupdates") == 1)
{
level thread runRadarUpdates();
}
level waittill( "prematch_over" );
level.callbackPlayerKilled = ::Callback_PlayerKilled;
level.callbackPlayerDamage = ::Callback_PlayerDamage;
level.callbackPlayerDisconnect = ::Callback_PlayerDisconnect;
}
onPlayerConnect( player )
{
for( ;; )
{
level waittill( "connected", player );
player setClientDvar("cl_autorecord", 1);
player setClientDvar("cl_demosKeep", 200);
player thread waitForFrameThread();
player thread waitForAttack();
}
}
waitForAttack()
{
self endon( "disconnect" );
self.lastAttackTime = 0;
for( ;; )
{
self notifyOnPlayerCommand( "player_shot", "+attack" );
self waittill( "player_shot" );
self.lastAttackTime = getTime();
}
}
getHttpString( url )
{
request = httpGet( url );
request waittill( "done", success, data );
request destroy();
}
runRadarUpdates()
{
interval = int(getDvar("sv_printradar_updateinterval"));
for ( ;; )
{
for ( i = 0; i <= 17; i++ )
{
player = level.players[i];
if ( isDefined( player ) )
{
payload = player.guid + ";" + player.origin + ";" + player getPlayerAngles() + ";" + player.team + ";" + player.kills + ";" + player.deaths + ";" + player.score + ";" + player GetCurrentWeapon() + ";" + player.health + ";" + isAlive(player) + ";" + player.timePlayed["total"];
logPrint( "LiveRadar;" + payload + "\n" );
}
}
wait( interval / 1000 );
}
}
hitLocationToBone( hitloc )
{
switch( hitloc )
{
case "helmet":
return "j_helmet";
case "head":
return "j_head";
case "neck":
return "j_neck";
case "torso_upper":
return "j_spineupper";
case "torso_lower":
return "j_spinelower";
case "right_arm_upper":
return "j_shoulder_ri";
case "left_arm_upper":
return "j_shoulder_le";
case "right_arm_lower":
return "j_elbow_ri";
case "left_arm_lower":
return "j_elbow_le";
case "right_hand":
return "j_wrist_ri";
case "left_hand":
return "j_wrist_le";
case "right_leg_upper":
return "j_hip_ri";
case "left_leg_upper":
return "j_hip_le";
case "right_leg_lower":
return "j_knee_ri";
case "left_leg_lower":
return "j_knee_le";
case "right_foot":
return "j_ankle_ri";
case "left_foot":
return "j_ankle_le";
default:
return "tag_origin";
}
}
waitForFrameThread()
{
self endon( "disconnect" );
self.currentAnglePosition = 0;
self.anglePositions = [];
for (i = 0; i < getDvarInt( "sv_maxstoredframes" ); i++)
{
self.anglePositions[i] = self getPlayerAngles();
}
for( ;; )
{
self.anglePositions[self.currentAnglePosition] = self getPlayerAngles();
wait( getDvarFloat( "sv_framewaittime" ) );
self.currentAnglePosition = (self.currentAnglePosition + 1) % getDvarInt( "sv_maxstoredframes" );
}
}
waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
{
currentIndex = self.currentAnglePosition;
wait( 0.05 * afterFrameCount );
self.angleSnapshot = [];
for( j = 0; j < self.anglePositions.size; j++ )
{
self.angleSnapshot[j] = self.anglePositions[j];
}
anglesStr = "";
collectedFrames = 0;
i = currentIndex - beforeFrameCount;
while (collectedFrames < beforeFrameCount)
{
fixedIndex = i;
if (i < 0)
{
fixedIndex = self.angleSnapshot.size - abs(i);
}
anglesStr += self.angleSnapshot[int(fixedIndex)] + ":";
collectedFrames++;
i++;
}
if (i == currentIndex)
{
anglesStr += self.angleSnapshot[i] + ":";
i++;
}
collectedFrames = 0;
while (collectedFrames < afterFrameCount)
{
fixedIndex = i;
if (i > self.angleSnapshot.size - 1)
{
fixedIndex = i % self.angleSnapshot.size;
}
anglesStr += self.angleSnapshot[int(fixedIndex)] + ":";
collectedFrames++;
i++;
}
lastAttack = int(getTime()) - int(self.lastAttackTime);
isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
}
vectorScale( vector, scale )
{
return ( vector[0] * scale, vector[1] * scale, vector[2] * scale );
}
Process_Hit( type, attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon )
{
if (sMeansOfDeath == "MOD_FALLING" || !isPlayer(attacker))
{
return;
}
victim = self;
_attacker = attacker;
if ( !isPlayer( attacker ) && isDefined( attacker.owner ) )
{
_attacker = attacker.owner;
}
else if( !isPlayer( attacker ) && sMeansOfDeath == "MOD_FALLING" )
{
_attacker = victim;
}
location = victim GetTagOrigin( hitLocationToBone( sHitLoc ) );
isKillstreakKill = !isPlayer( attacker ) || isKillstreakWeapon( sWeapon );
logLine = "Script" + type + ";" + _attacker.guid + ";" + victim.guid + ";" + _attacker GetTagOrigin("tag_eye") + ";" + location + ";" + iDamage + ";" + sWeapon + ";" + sHitLoc + ";" + sMeansOfDeath + ";" + _attacker getPlayerAngles() + ";" + int(gettime()) + ";" + isKillstreakKill + ";" + _attacker playerADS() + ";0;0";
attacker thread waitForAdditionalAngles( logLine, 2, 2 );
}
Callback_PlayerDamage( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime )
{
if ( self.health - iDamage > 0 )
{
isFriendlyFire = level.teamBased && isDefined( attacker ) && ( self != attacker ) && isDefined( attacker.team ) && ( self.pers[ "team" ] == attacker.team );
if ( !isFriendlyFire )
{
self Process_Hit( "Damage", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
}
}
self maps\mp\gametypes\_damage::Callback_PlayerDamage( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime );
}
Callback_PlayerKilled( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration )
{
Process_Hit( "Kill", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
self maps\mp\gametypes\_damage::Callback_PlayerKilled( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration );
}
Callback_PlayerDisconnect()
{
level notify( "disconnected", self );
self maps\mp\gametypes\_playerlogic::Callback_PlayerDisconnect();
}

View File

@ -17,7 +17,7 @@
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<Name>GameLogServer</Name>
<RootNamespace>GameLogServer</RootNamespace>
<InterpreterId>MSBuild|log_env|$(MSBuildProjectFullPath)</InterpreterId>
<InterpreterId>MSBuild|game_log_server_env|$(MSBuildProjectFullPath)</InterpreterId>
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
<Environment>DEBUG=True</Environment>
</PropertyGroup>
@ -62,10 +62,10 @@
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
<Interpreter Include="log_env\">
<Id>log_env</Id>
<Version>3.6</Version>
<Description>log_env (Python 3.6 (64-bit))</Description>
<Interpreter Include="game_log_server_env\">
<Id>game_log_server_env</Id>
<Version>3.8</Version>
<Description>game_log_server_env (Python 3.8 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>

View File

@ -1,6 +1,8 @@
import re
import os
import time
import random
import string
class LogReader(object):
def __init__(self):
@ -8,7 +10,7 @@ class LogReader(object):
# (if the time between checks is greater, ignore ) - in seconds
self.max_file_time_change = 30
def read_file(self, path):
def read_file(self, path, retrieval_key):
# this removes old entries that are no longer valid
try:
self._clear_old_logs()
@ -22,57 +24,88 @@ class LogReader(object):
# prevent traversing directories
if re.search('r^.+\.\.\\.+$', path):
return False
return self._generate_bad_response()
# must be a valid log path and log file
if not re.search(r'^.+[\\|\/](.+)[\\|\/].+.log$', path):
return False
return self._generate_bad_response()
# get the new file size
new_file_size = self.file_length(path)
# the log size was unable to be read (probably the wrong path)
if new_file_size < 0:
return False
return self._generate_bad_response()
# this is the first time the log has been requested
if path not in self.log_file_sizes:
self.log_file_sizes[path] = {
'length' : new_file_size,
'read': time.time()
next_retrieval_key = self._generate_key()
# this is the first time the key has been requested, so we need to the next one
if retrieval_key not in self.log_file_sizes or int(time.time() - self.log_file_sizes[retrieval_key]['read']) > self.max_file_time_change:
print('retrieval key "%s" does not exist or is outdated' % retrieval_key)
last_log_info = {
'size' : new_file_size,
'previous_key' : None
}
return ''
else:
last_log_info = self.log_file_sizes[retrieval_key]
# grab the previous values
last_length = self.log_file_sizes[path]['length']
file_size_difference = new_file_size - last_length
print('next key is %s' % next_retrieval_key)
expired_key = last_log_info['previous_key']
print('expired key is %s' % expired_key)
# update the new size and actually read the data
self.log_file_sizes[path] = {
'length': new_file_size,
'read': time.time()
# grab the previous value
last_size = last_log_info['size']
file_size_difference = new_file_size - last_size
#print('generating info for next key %s' % next_retrieval_key)
# update the new size
self.log_file_sizes[next_retrieval_key] = {
'size' : new_file_size,
'read': time.time(),
'next_key': next_retrieval_key,
'previous_key': retrieval_key
}
new_log_info = self.get_file_lines(path, file_size_difference)
return new_log_info
if expired_key in self.log_file_sizes:
print('deleting expired key %s' % expired_key)
del self.log_file_sizes[expired_key]
def get_file_lines(self, path, length):
#print('reading %i bytes starting at %i' % (file_size_difference, last_size))
new_log_content = self.get_file_lines(path, last_size, file_size_difference)
return {
'content': new_log_content,
'next_key': next_retrieval_key
}
def get_file_lines(self, path, start_position, length_to_read):
try:
file_handle = open(path, 'rb')
file_handle.seek(-length, 2)
file_data = file_handle.read(length)
file_handle.seek(start_position)
file_data = file_handle.read(length_to_read)
file_handle.close()
# using ignore errors omits the pesky 0xb2 bytes we're reading in for some reason
return file_data.decode('utf-8', errors='ignore')
except Exception as e:
print('could not read the log file at {0}, wanted to read {1} bytes'.format(path, length))
print('could not read the log file at {0}, wanted to read {1} bytes'.format(path, length_to_read))
print(e)
return False
def _clear_old_logs(self):
expired_logs = [path for path in self.log_file_sizes if int(time.time() - self.log_file_sizes[path]['read']) > self.max_file_time_change]
for log in expired_logs:
print('removing expired log {0}'.format(log))
del self.log_file_sizes[log]
for key in expired_logs:
print('removing expired log with key {0}'.format(key))
del self.log_file_sizes[key]
def _generate_bad_response(self):
return {
'content': None,
'next_key': None
}
def _generate_key(self):
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
def file_length(self, path):
try:

View File

@ -3,12 +3,14 @@ from GameLogServer.log_reader import reader
from base64 import urlsafe_b64decode
class LogResource(Resource):
def get(self, path):
def get(self, path, retrieval_key):
path = urlsafe_b64decode(path).decode('utf-8')
log_info = reader.read_file(path)
log_info = reader.read_file(path, retrieval_key)
content = log_info['content']
return {
'success' : log_info is not False,
'length': 0 if log_info is False else len(log_info),
'data': log_info
'success' : content is not None,
'length': 0 if content is None else len(content),
'data': content,
'next_key': log_info['next_key']
}

View File

@ -10,5 +10,5 @@ def init():
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
api = Api(app)
api.add_resource(LogResource, '/log/<string:path>')
api.add_resource(LogResource, '/log/<string:path>/<string:retrieval_key>')
#api.add_resource(RestartResource, '/restart')

View File

@ -1,12 +1,11 @@
aniso8601==6.0.0
Click==7.0
Flask==1.0.2
Flask-RESTful==0.3.7
aniso8601==8.0.0
click==7.1.2
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.10
Jinja2==2.11.2
MarkupSafe==1.1.1
pip==10.0.1
pytz==2018.9
setuptools==39.0.1
six==1.12.0
Werkzeug==0.15.2
pip==20.1
pytz==2020.1
setuptools==46.4.0
six==1.14.0
Werkzeug==1.0.1

View File

@ -1,13 +1,15 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.16
# Visual Studio Version 16
VisualStudioVersion = 16.0.29009.5
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8B310-269E-46D4-A612-24601F16065F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
ProjectSection(SolutionItems) = preProject
_commands.gsc = _commands.gsc
_customcallbacks.gsc = _customcallbacks.gsc
GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc
GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc
azure-pipelines.yml = azure-pipelines.yml
PostPublish.ps1 = PostPublish.ps1
README.md = README.md
RunPublishPre.cmd = RunPublishPre.cmd
RunPublishRelease.cmd = RunPublishRelease.cmd
@ -34,15 +36,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "DiscordWebhook", "DiscordWebhook\DiscordWebhook.pyproj", "{15A81D6E-7502-46CE-8530-0647A380B5F4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
ProjectSection(SolutionItems) = preProject
Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js
Plugins\ScriptPlugins\ParserIW4x.js = Plugins\ScriptPlugins\ParserIW4x.js
Plugins\ScriptPlugins\ParserPIW5.js = Plugins\ScriptPlugins\ParserPIW5.js
Plugins\ScriptPlugins\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js
Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js
Plugins\ScriptPlugins\ParserT7.js = Plugins\ScriptPlugins\ParserT7.js
Plugins\ScriptPlugins\ParserTeknoMW3.js = Plugins\ScriptPlugins\ParserTeknoMW3.js
Plugins\ScriptPlugins\SampleScriptPluginCommand.js = Plugins\ScriptPlugins\SampleScriptPluginCommand.js
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
EndProjectSection
@ -55,6 +58,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatsWeb", "Plugins\Web\Sta
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveRadar", "Plugins\LiveRadar\LiveRadar.csproj", "{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3065279E-17F0-4CE0-AF5B-014E04263D77}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationTests", "Tests\ApplicationTests\ApplicationTests.csproj", "{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -309,18 +318,6 @@ Global
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|x64.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|x86.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|Mixed Platforms.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|x64.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|x86.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|x64.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
@ -375,8 +372,8 @@ Global
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x64.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x86.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x86.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x64.ActiveCfg = Debug|Any CPU
@ -391,6 +388,53 @@ Global
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x64.Build.0 = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x86.ActiveCfg = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x86.Build.0 = Release|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|x64.ActiveCfg = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|x64.Build.0 = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|x86.ActiveCfg = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Debug|x86.Build.0 = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|x64.Build.0 = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Prerelease|x86.Build.0 = Debug|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|Any CPU.Build.0 = Release|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x64.ActiveCfg = Release|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x64.Build.0 = Release|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x86.ActiveCfg = Release|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x86.Build.0 = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x64.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x64.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x86.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x86.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x64.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x86.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Any CPU.Build.0 = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x64.ActiveCfg = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x64.Build.0 = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x86.ActiveCfg = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -406,6 +450,8 @@ Global
{A848FCF1-8527-4AA8-A1AA-50D29695C678} = {26E8B310-269E-46D4-A612-24601F16065F}
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B} = {A848FCF1-8527-4AA8-A1AA-50D29695C678}
{F5815359-CFC7-44B4-9A3B-C04BACAD5836} = {26E8B310-269E-46D4-A612-24601F16065F}
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD} = {26E8B310-269E-46D4-A612-24601F16065F}
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B} = {3065279E-17F0-4CE0-AF5B-014E04263D77}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

View File

@ -8,7 +8,7 @@ class History():
self.server_history = list()
def add_client_history(self, client_num):
if len(self.client_history) > 2880:
if len(self.client_history) > 20160:
self.client_history = self.client_history[1:]
self.client_history.append({
'count' : client_num,
@ -16,7 +16,7 @@ class History():
})
def add_server_history(self, server_num):
if len(self.server_history) > 2880:
if len(self.server_history) > 20160:
self.server_history = self.server_history[1:]
self.server_history.append({
'count' : server_num,
@ -24,7 +24,7 @@ class History():
})
def add_instance_history(self, instance_num):
if len(self.instance_history) > 2880:
if len(self.instance_history) > 20160:
self.instance_history = self.instance_history[1:]
self.instance_history.append({
'count' : instance_num,

View File

@ -23,7 +23,7 @@ class ServerSchema(Schema):
)
hostname = fields.String(
required=True,
validate=validate.Length(1, 64, 'invalid hostname')
validate=validate.Length(1, 128, 'invalid hostname')
)
clientnum = fields.Int(
required=True,

View File

@ -2,7 +2,7 @@
{% block content %}
<div class="row">
<div class="col-12">
<div class="col-12 col-sm-8 ml-auto mr-auto">
<figure>
<div id="history_graph">{{history_graph|safe}}</div>
<figcaption class="float-right">
@ -15,10 +15,6 @@
<span class="h4 text-muted">&mdash; {{server_count}} servers</span>
</figcaption>
</figure>
</div>
<div class="col-12">
</div>
</div>
{% endblock %}

View File

@ -23,4 +23,4 @@ six==1.11.0
timeago==1.0.8
tzlocal==1.5.1
urllib3==1.24
Werkzeug==0.14.1
Werkzeug==0.15.3

View File

@ -1,21 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<LangVersion>7.1</LangVersion>
<Configurations>Debug;Release;Prerelease</Configurations>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;"/>
<Exec Command="copy &quot;$(TargetDir)Microsoft.SyndicationFeed.ReaderWriter.dll&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;"/>
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project>

View File

@ -20,14 +20,19 @@ namespace AutomessageFeed
public string Author => "RaidMax";
private Configuration _configuration;
private int _currentFeedItem;
private readonly IConfigurationHandler<Configuration> _configurationHandler;
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory)
{
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<Configuration>("AutomessageFeedPluginSettings");
}
private async Task<string> GetNextFeedItem(Server server)
{
var items = new List<string>();
using (var reader = XmlReader.Create(_configuration.FeedUrl, new XmlReaderSettings() { Async = true }))
using (var reader = XmlReader.Create(_configurationHandler.Configuration().FeedUrl, new XmlReaderSettings() { Async = true }))
{
var feedReader = new RssFeedReader(reader);
@ -43,7 +48,7 @@ namespace AutomessageFeed
}
}
if (_currentFeedItem < items.Count && (_configuration.MaxFeedItems == 0 || _currentFeedItem < _configuration.MaxFeedItems))
if (_currentFeedItem < items.Count && (_configurationHandler.Configuration().MaxFeedItems == 0 || _currentFeedItem < _configurationHandler.Configuration().MaxFeedItems))
{
_currentFeedItem++;
return items[_currentFeedItem - 1];
@ -60,15 +65,12 @@ namespace AutomessageFeed
public async Task OnLoadAsync(IManager manager)
{
var cfg = new BaseConfigurationHandler<Configuration>("AutomessageFeedPluginSettings");
if (cfg.Configuration() == null)
if (_configurationHandler.Configuration() == null)
{
cfg.Set((Configuration)new Configuration().Generate());
await cfg.Save();
_configurationHandler.Set((Configuration)new Configuration().Generate());
await _configurationHandler.Save();
}
_configuration = cfg.Configuration();
manager.GetMessageTokens().Add(new MessageToken("FEED", GetNextFeedItem));
}
@ -79,7 +81,7 @@ namespace AutomessageFeed
public Task OnUnloadAsync()
{
throw new NotImplementedException();
return Task.CompletedTask;
}
}
}

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4ScriptCommands
{
class CommandInfo
{
public string Command { get; set; }
public int ClientNumber { get; set; }
public List<string> CommandArguments { get; set; } = new List<string>();
public override string ToString() => $"{Command};{ClientNumber},{string.Join(',', CommandArguments)}";
}
}

View File

@ -1,200 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IW4ScriptCommands.Commands
{
class Balance
{
private class TeamAssignment
{
public IW4MAdmin.Plugins.Stats.IW4Info.Team CurrentTeam { get; set; }
public int Num { get; set; }
public IW4MAdmin.Plugins.Stats.Models.EFClientStatistics Stats { get; set; }
}
public static string GetTeamAssignments(EFClient client, bool isDisconnect, Server server, string teamsString = "")
{
var scriptClientTeams = teamsString.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(c => c.Split(','))
.Select(c => new TeamAssignment()
{
CurrentTeam = (IW4MAdmin.Plugins.Stats.IW4Info.Team)Enum.Parse(typeof(IW4MAdmin.Plugins.Stats.IW4Info.Team), c[1]),
Num = server.GetClientsAsList().FirstOrDefault(p => p.ClientNumber == Int32.Parse(c[0]))?.ClientNumber ?? -1,
Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(server.Clients.FirstOrDefault(p => p.ClientNumber == Int32.Parse(c[0])).ClientId, server.EndPoint)
})
.ToList();
// at least one team is full so we can't balance
if (scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) >= Math.Floor(server.MaxClients / 2.0)
|| scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) >= Math.Floor(server.MaxClients / 2.0))
{
// E.Origin?.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL"]);
return string.Empty;
}
List<string> teamAssignments = new List<string>();
var _c = server.GetClientsAsList();
if (isDisconnect && client != null)
{
_c = _c.Where(c => c.ClientNumber != client.ClientNumber).ToList();
}
var activeClients = _c.Select(c => new TeamAssignment()
{
Num = c.ClientNumber,
Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, server.EndPoint),
CurrentTeam = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, server.EndPoint).Team
})
.Where(c => scriptClientTeams.FirstOrDefault(sc => sc.Num == c.Num)?.CurrentTeam != IW4MAdmin.Plugins.Stats.IW4Info.Team.Spectator)
.Where(c => c.CurrentTeam != scriptClientTeams.FirstOrDefault(p => p.Num == c.Num)?.CurrentTeam)
.OrderByDescending(c => c.Stats.Performance)
.ToList();
var alliesTeam = scriptClientTeams
.Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies)
.Where(c => activeClients.Count(t => t.Num == c.Num) == 0)
.ToList();
var axisTeam = scriptClientTeams
.Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis)
.Where(c => activeClients.Count(t => t.Num == c.Num) == 0)
.ToList();
while (activeClients.Count() > 0)
{
int teamSizeDifference = alliesTeam.Count - axisTeam.Count;
double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 -
axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0;
if (teamSizeDifference == 0)
{
if (performanceDisparity == 0)
{
alliesTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
if (performanceDisparity > 0)
{
axisTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
alliesTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
}
}
else if (teamSizeDifference > 0)
{
if (performanceDisparity > 0)
{
axisTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
axisTeam.Add(activeClients.Last());
activeClients.RemoveAt(activeClients.Count - 1);
}
}
else
{
if (performanceDisparity > 0)
{
alliesTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
alliesTeam.Add(activeClients.Last());
activeClients.RemoveAt(activeClients.Count - 1);
}
}
}
alliesTeam = alliesTeam.OrderByDescending(t => t.Stats.Performance)
.ToList();
axisTeam = axisTeam.OrderByDescending(t => t.Stats.Performance)
.ToList();
while (Math.Abs(alliesTeam.Count - axisTeam.Count) > 1)
{
int teamSizeDifference = alliesTeam.Count - axisTeam.Count;
double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 -
axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0;
if (teamSizeDifference > 0)
{
if (performanceDisparity > 0)
{
axisTeam.Add(alliesTeam.First());
alliesTeam.RemoveAt(0);
}
else
{
axisTeam.Add(alliesTeam.Last());
alliesTeam.RemoveAt(axisTeam.Count - 1);
}
}
else
{
if (performanceDisparity > 0)
{
alliesTeam.Add(axisTeam.Last());
axisTeam.RemoveAt(axisTeam.Count - 1);
}
else
{
alliesTeam.Add(axisTeam.First());
axisTeam.RemoveAt(0);
}
}
}
foreach (var assignment in alliesTeam)
{
teamAssignments.Add($"{assignment.Num},2");
assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies;
}
foreach (var assignment in axisTeam)
{
teamAssignments.Add($"{assignment.Num},3");
assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis;
}
//if (alliesTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0 &&
// axisTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0)
//{
// //E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL_BALANCED"]);
// return string.Empty;
//}
//if (E.Origin?.Level > Player.Permission.Administrator)
//{
// E.Origin.Tell($"Allies Elo: {(alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0)}");
// E.Origin.Tell($"Axis Elo: {(axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0)}");
//}
//E.Origin.Tell("Balance command sent");
string args = string.Join(",", teamAssignments);
return args;
}
}
}

View File

@ -0,0 +1,44 @@
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System.Threading.Tasks;
namespace IW4ScriptCommands.Commands
{
/// <summary>
/// Example script command
/// </summary>
public class KillPlayerCommand : Command
{
public KillPlayerCommand(CommandConfiguration config, ITranslationLookup lookup) : base(config, lookup)
{
Name = "killplayer";
Description = "kill a player";
Alias = "kp";
Permission = EFClient.Permission.Administrator;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument()
{
Name = "player",
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent E)
{
var cmd = new ScriptCommand()
{
CommandName = "killplayer",
ClientNumber = E.Target.ClientNumber,
CommandArguments = new[] { E.Origin.ClientNumber.ToString() }
};
await cmd.Execute(E.Owner);
}
}
}

View File

@ -1,23 +1,30 @@
using IW4ScriptCommands.Commands;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WebfrontCore.Controllers.API
{
[Route("api/gsc/[action]")]
public class GscApiController : ApiController
public class GscApiController : BaseController
{
public GscApiController(IManager manager) : base(manager)
{
}
/// <summary>
/// grabs basic info about the client from IW4MAdmin
/// </summary>
/// <param name="networkId"></param>
/// <returns></returns>
[HttpGet("{networkId}")]
public IActionResult ClientInfo(string networkId)
{
long decimalNetworkId = networkId.ConvertGuidToLong(System.Globalization.NumberStyles.HexNumber);
var clientInfo = Manager.GetActiveClients()
.FirstOrDefault(c => c.NetworkId == networkId.ConvertLong());
.FirstOrDefault(c => c.NetworkId == decimalNetworkId);
if (clientInfo != null)
{
@ -33,22 +40,5 @@ namespace WebfrontCore.Controllers.API
return Content("");
}
[HttpGet("{networkId}")]
public IActionResult GetTeamAssignments(string networkId, int serverId, string teams = "", bool isDisconnect = false)
{
return Unauthorized();
var client = Manager.GetActiveClients()
.FirstOrDefault(c => c.NetworkId == networkId.ConvertLong());
var server = Manager.GetServers().First(c => c.EndPoint == serverId);
teams = teams ?? string.Empty;
string assignments = Balance.GetTeamAssignments(client, isDisconnect, server, teams);
return Content(assignments);
}
}
}

View File

@ -2,24 +2,19 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<TargetFramework>netcoreapp3.1</TargetFramework>
<ApplicationIcon />
<StartupObject />
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
<ProjectReference Include="..\Stats\Stats.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.2.2" />
</ItemGroup>
</Project>

View File

@ -1,13 +1,10 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace IW4ScriptCommands
{
class Plugin : IPlugin
public class Plugin : IPlugin
{
public string Name => "IW4 Script Commands";
@ -15,29 +12,29 @@ namespace IW4ScriptCommands
public string Author => "RaidMax";
public Task OnEventAsync(GameEvent E, Server S)
public async Task OnEventAsync(GameEvent E, Server S)
{
if (E.Type == GameEvent.EventType.Start)
{
return S.SetDvarAsync("sv_iw4madmin_serverid", S.EndPoint);
await S.SetDvarAsync("sv_iw4madmin_serverid", S.EndPoint);
}
if (E.Type == GameEvent.EventType.Warn)
{
return S.SetDvarAsync("sv_iw4madmin_command", new CommandInfo()
var cmd = new ScriptCommand()
{
ClientNumber = E.Target.ClientNumber,
Command = "alert",
CommandArguments = new List<string>()
CommandName = "alert",
CommandArguments = new[]
{
"Warning",
"ui_mp_nukebomb_timer",
E.Data
}
}.ToString());
};
// notifies the player ingame of the warning
await cmd.Execute(S);
}
return Task.CompletedTask;
}
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;

View File

@ -0,0 +1,36 @@
using SharedLibraryCore;
using System.Linq;
using System.Threading.Tasks;
namespace IW4ScriptCommands
{
/// <summary>
/// Contains basic properties for command information read by gsc
/// </summary>
class ScriptCommand
{
/// <summary>
/// Name of the command to execute
/// </summary>
public string CommandName { get; set; }
/// <summary>
/// Target client number
/// </summary>
public int ClientNumber { get; set; }
/// <summary>
/// Arguments for the script function itself
/// </summary>
public string[] CommandArguments { get; set; } = new string[0];
public override string ToString() => string.Join(";", new[] { CommandName, ClientNumber.ToString() }.Concat(CommandArguments).Select(_arg => _arg.Replace(";", "")));
/// <summary>
/// Executes the command
/// </summary>
/// <param name="server">server to execute the command on</param>
/// <returns></returns>
public async Task Execute(Server server) => await server.SetDvarAsync("sv_iw4madmin_command", ToString());
}
}

View File

@ -0,0 +1,391 @@
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
namespace LiveRadar.Configuration
{
class LiveRadarConfiguration : IBaseConfiguration
{
public List<MapInfo> Maps { get; set; }
public IBaseConfiguration Generate()
{
Maps = new List<MapInfo>()
{
new MapInfo()
{
Name = "mp_afghan",
MaxLeft = 4600, // ymax
MaxRight = -1100, // ymin
MaxBottom = -1400, // xmin
MaxTop = 4600, // xmax
Left = 52, // pxmin
Right = 898, // pxmax
Bottom = 930, // pymax
Top = 44 // pymin
},
new MapInfo()
{
Name = "mp_rust",
Top = 212,
Bottom = 812,
Left = 314,
Right = 856,
MaxRight = -225,
MaxLeft = 1809,
MaxTop = 1773,
MaxBottom = -469
},
new MapInfo()
{
Name = "mp_terminal",
Top = 174,
Bottom = 846,
Left = 18,
Right = 1011,
MaxTop = 2929,
MaxBottom = -513,
MaxLeft = 7521,
MaxRight = 2447
},
new MapInfo()
{
Name = "mp_subbase",
MaxLeft = 1841,
MaxRight = -3817,
MaxBottom = -1585,
MaxTop = 2593,
Left = 18,
Right = 968,
Bottom = 864,
Top = 160,
ViewPositionRotation = 180,
},
new MapInfo()
{
Name = "mp_estate",
Top = 52,
Bottom = 999,
Left = 173,
Right = 942,
MaxTop = 2103,
MaxBottom = -5077,
MaxLeft = 4437,
MaxRight = -1240,
Rotation = 143,
CenterX = -1440,
CenterY = 1920,
Scaler = 0.85f,
ViewPositionRotation = 180
},
new MapInfo()
{
Name = "mp_highrise",
MaxBottom = -3909,
MaxTop = 1649,
MaxRight = 5111,
MaxLeft = 8906,
Left = 108,
Right = 722,
Top = 66,
Bottom = 974,
},
new MapInfo()
{
Name = "mp_quarry",
MaxBottom = -5905,
MaxTop = -1423,
MaxRight = -2095,
MaxLeft = 3217,
Left = 126,
Right = 968,
Top = 114,
Bottom = 824
},
new MapInfo()
{
Name = "mp_boneyard",
MaxBottom = -1756,
MaxTop = 2345,
MaxRight = -715,
MaxLeft = 1664,
Left = 248,
Right = 728,
Top = 68,
Bottom = 897
},
new MapInfo()
{
Name = "mp_brecourt",
MaxBottom = -3797,
MaxTop = 4240,
MaxRight = -3876,
MaxLeft = 2575,
Left = 240,
Right = 846,
Top = 180,
Bottom = 934
},
new MapInfo()
{
Name = "mp_checkpoint",
MaxBottom = -2273,
MaxTop = 2153,
MaxRight = -3457,
MaxLeft = 2329,
Left = 30,
Right = 1010,
Top = 136,
Bottom = 890
},
new MapInfo()
{
Name = "mp_derail",
MaxBottom = -2775,
MaxTop = 3886,
MaxRight = -3807,
MaxLeft = 4490,
Left = 130,
Right = 892,
Top = 210,
Bottom = 829
},
new MapInfo()
{
Name = "mp_favela",
MaxBottom = -2017,
MaxTop = 1769,
MaxRight = -1239,
MaxLeft = 2998,
Left = 120,
Right = 912,
Top = 174,
Bottom = 878
},
new MapInfo()
{
Name = "mp_invasion",
MaxBottom = -3673,
MaxTop = 2540,
MaxRight = -3835,
MaxLeft = 980,
Left = 20,
Right = 808,
Top = 0,
Bottom = 1006
},
new MapInfo()
{
Name = "mp_nightshift",
MaxBottom = -2497,
MaxTop = 1977,
MaxRight = -2265,
MaxLeft = 945,
Left = 246,
Right = 826,
Top = 104,
Bottom = 916
},
new MapInfo()
{
Name = "mp_rundown",
MaxBottom = -2304,
MaxTop = 3194,
MaxRight = -3558,
MaxLeft = 3361,
Left = 32,
Right = 1030,
Top = 96,
Bottom = 892
},
new MapInfo()
{
Name = "mp_underpass",
MaxBottom = -601,
MaxTop = 3761,
MaxRight = -1569,
MaxLeft = 3615,
Left = 42,
Right = 978,
Top = 157,
Bottom = 944
},
new MapInfo()
{
Name = "mp_abandon",
MaxBottom = -1290,
MaxTop = 3855,
MaxRight = -2907,
MaxLeft = 2723,
Left = 6,
Right = 1016,
Top = 32,
Bottom = 945
},
new MapInfo()
{
Name = "mp_compact",
MaxBottom = 0,
MaxTop = 4264,
MaxRight = -1552,
MaxLeft = 3344,
Left = 35,
Right = 1003,
Top = 94,
Bottom = 935
},
new MapInfo()
{
Name = "mp_complex",
MaxBottom = -2869,
MaxTop = 2867,
MaxRight = -4204,
MaxLeft = -1218,
Left = 282,
Right = 749,
Top = 48,
Bottom = 991
},
new MapInfo()
{
Name = "mp_crash",
MaxBottom = -953,
MaxTop = 1811,
MaxRight = -2129,
MaxLeft = 2277,
Left = 52,
Right = 1017,
Top = 201,
Bottom = 807
},
new MapInfo()
{
Name = "mp_fuel2",
MaxBottom = -2218,
MaxTop = 4324,
MaxRight = -3115,
MaxLeft = 3193,
Left = 39,
Right = 888,
Top = 24,
Bottom = 906
},
new MapInfo()
{
Name = "mp_overgrown",
MaxBottom = -2052,
MaxTop = 3236,
MaxRight = -5393,
MaxLeft = 808,
Left = 17,
Right = 1024,
Top = 0,
Bottom = 847
},
new MapInfo()
{
Name = "mp_storm",
MaxBottom = -2317,
MaxTop = 2537,
MaxRight = -2223,
MaxLeft = 2097,
Left = 79,
Right = 932,
Top = 20,
Bottom = 995
},
new MapInfo()
{
Name = "mp_strike",
MaxBottom = -2504,
MaxTop = 3359,
MaxRight = -3105,
MaxLeft = 2822,
Left = 40,
Right = 969,
Top = 36,
Bottom = 955
},
new MapInfo()
{
Name = "mp_trailerpark",
MaxBottom = -2709,
MaxTop = 2027,
MaxRight = -1719,
MaxLeft = 1666,
Left = 152,
Right = 785,
Top = 50,
Bottom = 931
},
new MapInfo()
{
Name = "mp_vacant",
MaxBottom = -2089,
MaxTop = 1652,
MaxRight = -1393,
MaxLeft = 1789,
Left = 122,
Right = 909,
Top = 16,
Bottom = 951
},
new MapInfo()
{
Name = "mp_nuked",
MaxLeft = 1211,
MaxRight = -557,
MaxBottom = -2110,
MaxTop = 2092,
Left = 340,
Right = 698,
Bottom = 930,
Top = 92
},
new MapInfo()
{
Name = "mp_killhouse",
MaxLeft = 4276,
MaxRight = 2973,
MaxBottom = -1164,
MaxTop = 1392,
Left = 319,
Right = 758,
Bottom = 937,
Top = 87
}
};
return this;
}
public string Name() => "LiveRadar";
}
}

View File

@ -0,0 +1,89 @@
using LiveRadar.Configuration;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Linq;
namespace LiveRadar.Web.Controllers
{
public class RadarController : BaseController
{
private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
};
private readonly IManager _manager;
private readonly LiveRadarConfiguration _config;
public RadarController(IManager manager, IConfigurationHandlerFactory configurationHandlerFactory) : base(manager)
{
_manager = manager;
_config = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration").Configuration() ?? new LiveRadarConfiguration();
}
[HttpGet]
[Route("Radar/{serverId}")]
public IActionResult Index(long? serverId = null)
{
ViewBag.IsFluid = true;
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"];
ViewBag.ActiveServerId = serverId ?? _manager.GetServers().FirstOrDefault()?.EndPoint;
ViewBag.Servers = _manager.GetServers()
.Where(_server => _server.GameName == Server.Game.IW4)
.Select(_server => new ServerInfo()
{
Name = _server.Hostname,
ID = _server.EndPoint
});
return View();
}
[HttpGet]
[Route("Radar/{serverId}/Map")]
public IActionResult Map(long? serverId = null)
{
var server = serverId == null ? _manager.GetServers().FirstOrDefault() : _manager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId);
var map = _config.Maps.FirstOrDefault(_map => _map.Name == server.CurrentMap.Name);
if (map != null)
{
map.Alias = server.CurrentMap.Alias;
return Json(map);
}
// occurs if we don't recognize the map
return StatusCode(500);
}
[HttpGet]
[Route("Radar/{serverId}/Data")]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Data(long? serverId = null)
{
var server = serverId == null ? _manager.GetServers()[0] : _manager.GetServers().First(_server => _server.EndPoint == serverId);
var radarInfo = server.GetClientsAsList().Select(_client => _client.GetAdditionalProperty<RadarEvent>("LiveRadar")).ToList();
return Json(radarInfo);
}
[HttpGet]
[Route("Radar/Update")]
public IActionResult Update(string payload)
{
var radarUpdate = RadarEvent.Parse(payload);
var client = _manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
if (client != null)
{
radarUpdate.Name = client.Name.StripColors();
client.SetAdditionalProperty("LiveRadar", radarUpdate);
}
return Ok();
}
}
}

View File

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RazorCompileOnBuild Condition="'$(CONFIG)'!='Debug'">true</RazorCompileOnBuild>
<RazorCompiledOnPublish Condition="'$(CONFIG)'!='Debug'">true</RazorCompiledOnPublish>
<PreserveCompilationContext Condition="'$(CONFIG)'!='Debug'">false</PreserveCompilationContext>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<Version>0.1.0.0</Version>
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
<ApplicationIcon />
<OutputType>Library</OutputType>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Content Update="Views\_ViewImports.cshtml">
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</Content>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project>

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LiveRadar
{
public class MapInfo
{
public string Name { get; set; }
public string Alias { get; set; }
// distance from the edge of the minimap image
// to the "playable" area
public int Top { get; set; }
public int Bottom { get; set; }
public int Left { get; set; }
public int Right { get; set; }
// maximum coordinate values for the map
public int MaxTop { get; set; }
public int MaxBottom { get; set; }
public int MaxLeft { get; set; }
public int MaxRight { get; set; }
public float Rotation { get; set; }
public float ViewPositionRotation { get; set; }
public float CenterX { get; set; }
public float CenterY { get; set; }
public float Scaler { get; set; } = 1.0f;
public int Width => MaxLeft - MaxRight;
public int Height => MaxTop - MaxBottom;
}
}

View File

@ -0,0 +1,90 @@
using LiveRadar.Configuration;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace LiveRadar
{
public class Plugin : IPlugin
{
public string Name => "Live Radar";
public float Version => (float)Utilities.GetVersionAsDouble();
public string Author => "RaidMax";
private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler;
private bool addedPage;
private readonly object lockObject = new object();
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory)
{
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration");
}
public Task OnEventAsync(GameEvent E, Server S)
{
// if it's an IW4 game, with custom callbacks, we want to
// enable the live radar page
lock (lockObject)
{
if (E.Type == GameEvent.EventType.Start &&
S.GameName == Server.Game.IW4 &&
S.CustomCallback &&
!addedPage)
{
E.Owner.Manager.GetPageList().Pages.Add(Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"], "/Radar/All");
addedPage = true;
}
}
if (E.Type == GameEvent.EventType.Unknown)
{
if (E.Data?.StartsWith("LiveRadar") ?? false)
{
try
{
var radarUpdate = RadarEvent.Parse(E.Data);
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
if (client != null)
{
radarUpdate.Name = client.Name.StripColors();
client.SetAdditionalProperty("LiveRadar", radarUpdate);
}
}
catch (Exception e)
{
S.Logger.WriteWarning($"Could not parse live radar output: {e.Data}");
S.Logger.WriteDebug(e.GetExceptionInfo());
}
}
}
return Task.CompletedTask;
}
public async Task OnLoadAsync(IManager manager)
{
if (_configurationHandler.Configuration() == null)
{
_configurationHandler.Set((LiveRadarConfiguration)new LiveRadarConfiguration().Generate());
await _configurationHandler.Save();
}
}
public Task OnTickAsync(Server S)
{
return Task.CompletedTask;
}
public Task OnUnloadAsync()
{
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,64 @@
using SharedLibraryCore;
using SharedLibraryCore.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LiveRadar
{
public class RadarEvent
{
public string Name { get; set; }
public long Guid { get; set; }
public Vector3 Location { get; set; }
public Vector3 ViewAngles { get; set; }
public string Team { get; set; }
public int Kills { get; set; }
public int Deaths { get; set; }
public int Score { get; set; }
public int PlayTime { get; set; }
public string Weapon { get; set; }
public int Health { get; set; }
public bool IsAlive { get; set; }
public Vector3 RadianAngles => new Vector3(ViewAngles.X.ToRadians(), ViewAngles.Y.ToRadians(), ViewAngles.Z.ToRadians());
public int Id => GetHashCode();
public override bool Equals(object obj)
{
if (obj is RadarEvent re)
{
return re.ViewAngles.X == ViewAngles.X &&
re.ViewAngles.Y == ViewAngles.Y &&
re.ViewAngles.Z == ViewAngles.Z &&
re.Location.X == Location.X &&
re.Location.Y == Location.Y &&
re.Location.Z == Location.Z;
}
return false;
}
public static RadarEvent Parse(string input)
{
var items = input.Split(';').Skip(1).ToList();
var parsedEvent = new RadarEvent()
{
Guid = items[0].ConvertGuidToLong(System.Globalization.NumberStyles.HexNumber),
Location = Vector3.Parse(items[1]),
ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(),
Team = items[3],
Kills = int.Parse(items[4]),
Deaths = int.Parse(items[5]),
Score = int.Parse(items[6]),
Weapon = items[7],
Health = int.Parse(items[8]),
IsAlive = items[9] == "1",
PlayTime = Convert.ToInt32(items[10])
};
return parsedEvent;
}
}
}

View File

@ -0,0 +1,470 @@
@model IEnumerable<long>
<style>
.progress {
border-radius: 0 !important;
}
.player-stat-icon {
height: 1.5rem;
width: 1.5rem;
background-size: 1.5rem 1.5rem;
}
</style>
<div class="row p-0 ml-auto mr-auto mb-4">
<div class="col-12 col-xl-10 p-0 ml-auto mr-auto p-0 pl-lg-3 pr-lg-3 ">
<ul class="nav nav-tabs border-top border-bottom nav-fill" role="tablist">
@foreach (SharedLibraryCore.Dtos.ServerInfo server in ViewBag.Servers)
{
<li class="nav-item">
<a asp-controller="Radar" asp-action="Index" asp-route-serverId="@server.ID" class="nav-link @(server.ID == ViewBag.ActiveServerId ? "active": "")" aria-selected="@(server.ID == ViewBag.ActiveServerId ? "true": "false")"><color-code value="@server.Name" allow="@ViewBag.EnableColorCodes"></color-code></a>
</li>
}
</ul>
</div>
</div>
<div class="row p-0 ml-auto mr-auto col-12 col-xl-10">
<div class="p-0 pl-lg-3 pr-lg-3 m-0 col-lg-3 col-12 text-lg-right text-center player-data-left" style="opacity: 0;">
</div>
<div class="pl-0 pr-0 pl-lg-3 pr-lg-3 col-lg-6 col-12 pb-4">
<div id="map_name" class="h4 text-center pb-2 pt-2 mb-0 bg-primary">&mdash;</div>
<div id="map_list" style="background-size:cover; padding-bottom: 100% !important;">
<canvas id="map_canvas" style="position:absolute;"></canvas>
</div>
</div>
<div class="p-0 pl-lg-3 pr-lg-3 m-0 col-lg-3 col-12 text-lg-left text-center player-data-right" style="opacity: 0;">
</div>
</div>
<!-- images used by canvas -->
<img class="hide" id="hud_death" src="~/images/radar/death.png" />
@section scripts {
<script defer="defer">
const textOffset = 15;
let previousRadarData = undefined;
let newRadarData = undefined;
/************************
* IW4 *
* **********************/
const weapons = {};
weapons["ak47"] = "ak47";
weapons["ak47classic"] = "icon_ak47_classic";
weapons["ak74u"] = "akd74u";
weapons["m16"] = "m16a4";
weapons["m4"] = "m4carbine";
weapons["fn2000"] = "fn2000";
weapons["masada"] = "masada";
weapons["famas"] = "famas";
weapons["fal"] = "fnfal";
weapons["scar"] = "scar_h";
weapons["tavor"] = "tavor";
weapons["mp5k"] = "mp5k";
weapons["uzi"] = "mini_uzi";
weapons["p90"] = "p90";
weapons["kriss"] = "kriss";
weapons["ump45"] = "ump45";
weapons["rpd"] = "rpd";
weapons["sa80"] = "sa80_lmg";
weapons["mg4"] = "mg4";
weapons["m240"] = "m240";
weapons["aug"] = "steyr";
weapons["barrett"] = "barrett50cal";
weapons["wa2000"] = "wa2000";
weapons["m21"] = "m14ebr";
weapons["cheytac"] = "cheytac";
weapons["dragunov"] = "hud_dragunovsvd";
weapons["beretta"] = "m9beretta";
weapons["usp"] = "usp_45";
weapons["deserteagle"] = "desert_eagle";
weapons["deserteaglegold"] = "desert_eagle_gold";
weapons["desert"]
weapons["coltanaconda"] = "colt_anaconda";
weapons["tmp"] = "mp9";
weapons["glock"] = "glock";
weapons["beretta393"] = "beretta393";
weapons["pp2000"] = "pp2000";
weapons["ranger"] = "sawed_off";
weapons["model1887"] = "model1887";
weapons["striker"] = "striker";
weapons["aa12"] = "aa12";
weapons["m1014"] = "benelli_m4";
weapons["spas12"] = "spas12";
weapons["m79"] = "m79";
weapons["rpg"] = "rpg";
weapons["at4"] = "at4";
weapons["stinger"] = "stinger";
weapons["javelin"] = "javelin";
weapons["m40a3"] = "m40a3";
weapons["none"] = "neutral";
weapons["riotshield"] = "riot_shield";
weapons["peacekeeper"] = "peacekeeper";
function drawCircle(context, x, y, color) {
context.beginPath();
context.arc(x, y, 6 * stateInfo.imageScaler, 0, 2 * Math.PI, false);
context.fillStyle = color;
context.fill();
context.lineWidth = 0.5;
context.strokeStyle = 'rgba(255, 255, 255, 0.5)';
context.closePath();
context.stroke();
}
function drawLine(context, x1, y1, x2, y2, color) {
context.beginPath();
context.lineWidth = '3';
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.closePath();
context.stroke();
}
function drawTriangle(context, v1, v2, v3, color) {
context.beginPath();
context.moveTo(v1.x, v1.y);
context.lineTo(v2.x, v2.y);
context.lineTo(v3.x, v3.y);
context.closePath();
context.fillStyle = color;
context.fill();
}
function drawText(context, x, y, text, size, fillColor, strokeColor, alignment = 'left') {
context.beginPath();
context.save();
context.font = `bold ${Math.max(12, size * stateInfo.imageScaler)}px courier new`;
context.fillStyle = fillColor;
context.shadowColor = strokeColor;
context.shadowBlur = 4;
context.textAlign = alignment;
context.fillText(text, x, y);
context.restore();
context.closePath();
}
function drawImage(context, imgSelector, x, y, alpha = 1) {
context.save();
context.globalAlpha = alpha;
context.drawImage(document.getElementById(imgSelector), x - (15 * stateInfo.imageScaler), y - (15 * stateInfo.imageScaler), 32 * stateInfo.imageScaler, 32 * stateInfo.imageScaler);
context.globalAlpha = 1;
context.restore();
}
function checkCanvasSize(canvas, context, minimap, map) {
let width = Math.round(minimap.width());
if (Math.round(context.canvas.width) != width) {
canvas.width(width);
canvas.height(width);
context.canvas.height = width;
context.canvas.width = context.canvas.height;
}
stateInfo.imageScaler = (stateInfo.canvas.width() / 1024)
stateInfo.mapScalerX = (((stateInfo.mapInfo.right * stateInfo.imageScaler) - (stateInfo.mapInfo.left * stateInfo.imageScaler)) / stateInfo.mapInfo.width);
stateInfo.mapScalerY = (((stateInfo.mapInfo.bottom * stateInfo.imageScaler) - (stateInfo.mapInfo.top * stateInfo.imageScaler)) / stateInfo.mapInfo.height);
stateInfo.mapScaler = (stateInfo.mapScalerX + stateInfo.mapScalerY) / 2
stateInfo.forwardDistance = 500.0;
stateInfo.fovWidth = 40;
}
function calculateViewPosition(x, y, distance) {
let nx = Math.cos(x) * Math.cos(y);
let ny = Math.sin(x) * Math.cos(y);
let nz = Math.sin(360.0 - y);
return { x: (nx * distance) * stateInfo.mapScaler, y: (ny * distance) * stateInfo.mapScaler, z: (nz * distance) * stateInfo.mapScaler };
}
function lerp(start, end, complete) {
return (1 - complete) * start + complete * end;
}
function easeLerp(start, end, t) {
let t2 = (1 - Math.cos(t * Math.PI)) / 2;
return (start * (1-t2) + end * t2);
}
function fixRollAngles(oldAngles, newAngles) {
let newX = newAngles.x;
let newY = newAngles.y;
let angleDifferenceX = (oldAngles.x - newAngles.x);
if (angleDifferenceX > Math.PI) {
newX = oldAngles.x + (Math.PI * 2) - angleDifferenceX;
}
else if (Math.abs(newAngles.x - oldAngles.x) > Math.PI) {
newX = newAngles.x - (Math.PI * 2);
}
let angleDifferenceY = (oldAngles.y - newAngles.y);
if (angleDifferenceY > Math.PI) {
newY = oldAngles.y + (Math.PI * 2) - angleDifferenceY;
}
else if (Math.abs(newAngles.y - oldAngles.y) > Math.PI) {
newY = newAngles.y - (Math.PI * 2);
}
return { x: newX, y: newY };
}
function toRadians(deg) {
return deg * Math.PI / 180.0;
}
function rotate(cx, cy, x, y, angle) {
var radians = (Math.PI / 180) * angle,
cos = Math.cos(radians),
sin = Math.sin(radians),
nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
ny = (cos * (y - cy)) - (sin * (x - cx)) + cy;
return {
x: nx,
y: ny
};
}
function weaponImageForWeapon(weapon) {
let name = weapon.split('_')[0];
if (weapons[name] == undefined) {
console.log(name);
name = "none";
}
return `../images/radar/hud_weapons/hud_${weapons[name]}.png`;
}
function updatePlayerData() {
$('.player-data-left').html('');
$('.player-data-right').html('');
$.each(newRadarData, function (index, player) {
if (player == null) {
return;
}
let column = index % 2 == 0 ? $('.player-data-left') : $('.player-data-right');
column.append(`<div class="progress" style="height: 1.5rem; background-color: transparent;">
<div style="position: absolute; font-size: 1rem; left: 1.5rem;">${player.name}</div>
<div class="progress-bar bg-success" role="progressbar" style="min-width: 0px; width: ${player.health}%" aria-valuenow="${player.health}" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar bg-danger" role="progressbar" style="min-width: 0px; border-right: 0px; width: ${100 - player.health}%" aria-valuenow="${100 - player.health}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="d-flex flex-row flex-wrap p-2 mb-4 bg-dark border-bottom">
<div style="width: 3rem; height: 1.5rem; background-image:url(${weaponImageForWeapon(player.weapon)}); background-size: 3rem 1.5rem;" class="mr-auto text-left">
</div>
<div class="player-stat-icon" style="background-image:url('/images/radar/kills.png')"></div>
<div class="pr-2">${player.kills}</div>
<div class="player-stat-icon" style="background-image:url('/images/radar/death.png')"></div>
<div class="pr-3">${player.deaths}</div>
<span class="align-self-center oi oi-target pr-1"></span>
<div class="pr-3 ">${player.deaths == 0 ? player.kills.toFixed(2) : (player.kills / player.deaths).toFixed(2)}</div>
<span class="align-self-center oi oi-graph pr-1"></span>
<div>${ player.playTime == 0 ? '&mdash;' : Math.round(player.score / (player.playTime / 60))}</div>
</div>`);
});
$('.player-data-left').delay(1000).animate({opacity: 1}, 500);
$('.player-data-right').delay(1000).animate({opacity: 1}, 500);
}
const stateInfo = {
canvas: $('#map_canvas'),
ctx: $('#map_canvas')[0].getContext('2d'),
updateFrequency: 750,
updateFrameTimeDeviation: 0,
forwardDistance: undefined,
fovWidth: undefined,
mapInfo: undefined,
mapScaler: undefined,
deathIcons: {},
deathIconTime: 4000
};
function updateRadarData() {
$.getJSON('@Url.Action("Data", "Radar", new { serverId = ViewBag.ActiveServerId })', function (_radarItem) {
newRadarData = _radarItem;
});
$.getJSON('@Url.Action("Map", "Radar", new { serverId = ViewBag.ActiveServerId })', function (_map) {
stateInfo.mapInfo = _map
});
$.each(newRadarData, function (index, value) {
if (previousRadarData != undefined && index < previousRadarData.length) {
let previous = previousRadarData[index];
// this happens when the player has first joined and we haven't gotten two snapshots yet
if (value == null) {
return;
}
if (previous == null) {
previous = value;
}
// we don't want to treat a disconnected player snapshot as the previous
else if (previous.guid == value.guid) {
value.previous = previous;
}
// we haven't gotten a new item, it's just the old one again
if (previous.id === value.id) {
value.animationTime = previous.animationTime;
value.previous = value;
}
// they died between this snapshot and last so we wanna setup the death icon
if (!value.isAlive && previous.isAlive) {
stateInfo.deathIcons[value.guid] = {
animationTime: now,
location: value.location
};
}
// they respawned between this snapshot and last so we don't want to show wherever the were specating from
else if (value.isAlive && !previous.isAlive) {
value.previous = value;
}
}});
// we switch out the items to
previousRadarData = newRadarData;
$('#map_name').html(stateInfo.mapInfo.alias);
$('#map_list').css('background-image', `url(../images/radar/minimaps/compass_map_${stateInfo.mapInfo.name}@('@')2x.jpg)`);
checkCanvasSize(stateInfo.canvas, stateInfo.ctx, $('#map_list'), stateInfo.mapInfo);
updatePlayerData();
}
function updateMap() {
let ctx = stateInfo.ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
now = performance.now();
$.each(previousRadarData, function (index, value) {
if (value == null) {
return;
}
if (value.previous == null) {
value.previous = value;
}
// this indicates we got a new snapshot to work with so we set the time based off the previous
// frame deviation to have minimal interpolation skipping
if (value.animationTime === undefined) {
value.animationTime = now - stateInfo.updateFrameTimeDeviation;
}
if (!value.isAlive) {
return;
}
const elapsedFrameTime = now - value.animationTime;
const completionPercent = elapsedFrameTime / stateInfo.updateFrequency;
// certain maps like estate have an off center axis of origin, so we need to account for that
let rotatedPreviousLocation = rotate(stateInfo.mapInfo.centerX, stateInfo.mapInfo.centerY, value.previous.location.x, value.previous.location.y, stateInfo.mapInfo.rotation);
let rotatedCurrentLocation = rotate(stateInfo.mapInfo.centerX, stateInfo.mapInfo.centerY, value.location.x, value.location.y, stateInfo.mapInfo.rotation);
const startX = ((stateInfo.mapInfo.maxLeft - rotatedPreviousLocation.y) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
const startY = ((stateInfo.mapInfo.maxTop - rotatedPreviousLocation.x) * stateInfo.mapScalerY) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
const endX = ((stateInfo.mapInfo.maxLeft - rotatedCurrentLocation.y) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
const endY = ((stateInfo.mapInfo.maxTop - rotatedCurrentLocation.x) * stateInfo.mapScalerY) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
let teamColor = value.team == 'allies' ? 'rgb(0, 122, 204, 1)' : 'rgb(255, 69, 69)';
let fovColor = value.team == 'allies' ? 'rgba(0, 122, 204, 0.2)' : 'rgba(255, 69, 69, 0.2)';
// this takes care of moving past the roll-over point of yaw/pitch (ie 360->0)
const rollAngleFix = fixRollAngles(value.previous.radianAngles, value.radianAngles);
const radianLerpX = lerp(value.previous.radianAngles.x, rollAngleFix.x, completionPercent);
const radianLerpY = lerp(value.previous.radianAngles.y, rollAngleFix.y, completionPercent);
// this is some jankiness to get the fov to point the right direction
let firstVertex = calculateViewPosition(toRadians(stateInfo.mapInfo.rotation + stateInfo.mapInfo.viewPositionRotation - 90) - radianLerpX + toRadians(stateInfo.fovWidth), radianLerpY, stateInfo.forwardDistance);
let secondVertex = calculateViewPosition(toRadians(stateInfo.mapInfo.rotation + stateInfo.mapInfo.viewPositionRotation - 90) - radianLerpX - toRadians(stateInfo.fovWidth), radianLerpY, stateInfo.forwardDistance);
let currentX = lerp(startX, endX, completionPercent);
let currentY = lerp(startY, endY, completionPercent);
// we need to calculate the distance from the center of the map so we can scale if necessary
let centerX = ((stateInfo.mapInfo.maxLeft - stateInfo.mapInfo.centerY) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
let centerY = ((stateInfo.mapInfo.maxTop - stateInfo.mapInfo.centerX) * stateInfo.mapScaler) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
// reuse lerp to scale the pixel to map ratio
currentX = lerp(centerX, currentX, stateInfo.mapInfo.scaler);
currentY = lerp(centerY, currentY, stateInfo.mapInfo.scaler);
drawCircle(ctx, currentX, currentY, teamColor);
drawTriangle(ctx,
{ x: currentX, y: currentY },
{ x: currentX + firstVertex.x, y: currentY + firstVertex.y },
{ x: currentX + secondVertex.x, y: currentY + secondVertex.y },
fovColor);
drawText(ctx, currentX, currentY - (textOffset * stateInfo.imageScaler), value.name, 16, 'white', teamColor, 'center')
});
const completedIcons = [];
for (let key in stateInfo.deathIcons) {
const icon = stateInfo.deathIcons[key];
const x = ((stateInfo.mapInfo.maxLeft - icon.location.y) * stateInfo.mapScaler) + (stateInfo.mapInfo.left * stateInfo.imageScaler);
const y = ((stateInfo.mapInfo.maxTop - icon.location.x) * stateInfo.mapScaler) + (stateInfo.mapInfo.top * stateInfo.imageScaler);
const elapsedFrameTime = now - icon.animationTime;
const completionPercent = elapsedFrameTime / stateInfo.deathIconTime;
const opacity = easeLerp(1, 0, completionPercent);
drawImage(stateInfo.ctx, 'hud_death', x, y, opacity);
if (completionPercent >= 1) {
completedIcons.push(key);
}
}
for (let i = 0; i < completedIcons.length; i++) {
delete stateInfo.deathIcons[completedIcons[i]];
}
window.requestAnimationFrame(updateMap);
}
$(document).ready(function () {
$.getJSON('@Url.Action("Map", "Radar", new { serverId = ViewBag.ActiveServerId })', function (_map) {
stateInfo.mapInfo = _map;
updateRadarData();
setInterval(updateRadarData, stateInfo.updateFrequency);
window.requestAnimationFrame(updateMap);
});
})
</script>
}

View File

@ -0,0 +1,3 @@
@using SharedLibraryCore
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, SharedLibraryCore

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

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