Compare commits

...

404 Commits

Author SHA1 Message Date
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
02622ea7de update application version
ignore stat events of bots if they are ignored
limit max number of bot profiles to 18, greater than 18 wraps
prevent anti cheat from running on bot events
create localization folder on publish so copying over doesn't fail
include quick message mapping on webfront server history chat
make gravatar on profile not repeat
2019-04-25 13:00:54 -05:00
03ae3b5822 deleted localization files as they're now generated during release
reintroduce throttling for servers that are unreachable (defaults to 60 seconds between polls)
small revert to the RektT5M parser contell -> tell
add migration to introduce gamename to server
impllement quickmessage mapping
2019-04-23 17:27:20 -05:00
0711249a46 add parser for RektT5M
update base event parser to include "short" GUIDs
updated some localization
add tooltip to anti cheat metrics on profile for more information about what they mean
2019-04-21 16:28:13 -05:00
18f4ffa9ff fix extra prompt for server port
change vpn detection failure to warning instead of error
2019-04-18 14:19:50 -05:00
934fead5c2 fix issue with login 2019-04-17 17:50:53 -05:00
02cad10d77 prevent starting if no servers can be connected to
fix nextmap issue on t6
fix bug with kicking client for profane name
2019-04-16 11:32:42 -05:00
b134cd4728 fix gravatars not showing
fix web config not saving Uri
fix issue with token login
2019-04-14 10:55:05 -05:00
b9c4a1b5f6 finish initial implementing application configuration editing through webfront
todo: server configuration
2019-04-12 22:25:18 -05:00
f0fd4c66e9 add configuration option to force local translations
fix silly bug with no being able to claim ownership
continue work on configuration via webfront
2019-04-11 20:43:05 -05:00
52fe8fc847 !setgravatar uses meta service now
update certain prompts to use interpolated strings from translation
update application version
2019-04-09 15:02:49 -05:00
9f8c35dbed fix bug with chat context timestamps not parsing is different machine locales
add disallowed client names to default config
fix ping not working for targets
2019-04-08 20:31:32 -05:00
9d9be7f8af few more small fixes
complete join button on webfront
update for 2.2.6.0
2019-04-08 12:29:48 -05:00
dd82a5e3fa start add of join button (still need to grab the external IP address)
finish up fixes for alias stuff
2019-04-07 20:14:59 -05:00
8667532d24 remove create proxy as it's not even used anymore
more fixes for alias stuff
hopefully fix rare bug where client activity cshtml loop goes oob
add URLProtocol format to event parsers to allow connecting through webfront
2019-04-06 21:48:49 -05:00
863ba8b096 strip drive letter on gamelog server if running on linux
strip undecodable chacters from gamelog server log file
finish re work on alias add/update ( I think)
2019-04-05 21:15:17 -05:00
8ab89e113d clean up log reader/make it output more useful message if things go wrong
add unflag as a penalty
show bans/tempbans even after they've expired on penalty list
continue making alias links great again
2019-04-05 13:34:03 -05:00
00634780d4 use "world client" when recieving fall damage/damage
fix rare bug with GetClientByName
refactor some alias/ef stuff. still more to do
2019-04-02 20:20:37 -05:00
6f80f1edbb refine webfront pages
finish refactor of penalty information/profile
optimize pull penalty query
start impl of quick message mapping
2019-03-31 19:56:31 -05:00
9393b35c39 start implementation of configuration via webfront 2019-03-30 22:04:15 -05:00
37d3f4f90d changes for latest release 2019-03-30 17:21:01 -05:00
8521df85f5 finish initial rework of profile page with meta pagination 2019-03-29 21:56:56 -05:00
7b8126d57e continue rework of profile
start moving profile info out of javascript into componentview
rework meta data to include count and offset
2019-03-27 19:40:26 -05:00
1e2e2218e3 finish initial rework of profile page 2019-03-26 21:02:11 -05:00
11e3235d5d fix issue with not loading last connection for admins
continue work on fixing profile
2019-03-25 21:12:16 -05:00
cae6d8389e fix bug with privileged users not always showing the most recent profile
temporary bans are now applied to all linked accounts instead of a per-guid basis
rework set level flow
add guid and ip address (if logged in) to public async endpoint
2019-03-24 21:34:20 -05:00
1056c7c335 fix for issue #70
update start script for windows
2019-03-18 10:36:31 -05:00
95e4ee672e Merge branch 'master' of https://github.com/RaidMax/IW4M-Admin 2019-03-17 17:38:17 -05:00
bf0a0befc6 fix top players row not fill full width
make stats web use view imports
add more games/maps to default settings (thanks to FryTechTiddys
#7622)
replace ConfigurationBuilder Config Handler implementation with Newtonsoft.Json as it wasn't deserializing "complex" classes properly
2019-03-17 17:37:50 -05:00
53c3ff6ce3 2 typos fixed
Plutoniun -> Plutonium
IW3 -> IW4
2019-03-17 08:06:15 -05:00
c8ec0eefa9 game log reader reads async now.
should have done that a long time ago
update profile page to have a bit better space usage
2019-03-09 10:28:04 -06:00
82bae772f0 most played command now ordered by play time
issue #68
2019-03-02 17:29:09 -06:00
af3aea77bc finish UI tweaks for issue #39 2019-03-02 14:29:09 -06:00
b3e5f468a1 finish implementation of per server top stats page 2019-02-27 20:13:15 -06:00
c21bf2ebf1 continue working on per servver topstats 2019-02-26 21:25:27 -06:00
d318a57830 start implementation for per server top stats
issue #39
2019-02-25 19:36:10 -06:00
61f1436faf prompt user to continue if not all servers can be connected
issue #58
2019-02-24 19:35:59 -06:00
0c527a5f65 Add lock menu icon for tempbanned players
issue #62
2019-02-24 19:07:56 -06:00
1457b843e2 one more small fix for meta service after testing live 2019-02-22 19:37:53 -06:00
de69bed792 small migration fix for MySql 2019-02-22 19:35:03 -06:00
4b1f44cc2a re-enable login to webfront with password
update cookie to last 3 months
add configuration option to limit # of rss feed items
prevent database tracking of bots if ignore bots requested
add last map and last server played to profile
2019-02-22 19:06:51 -06:00
74cdf8e885 Fix bug introduced with auto messages 2019-02-19 19:39:09 -06:00
5d41059641 accidentally copied a file to the wrong project 2019-02-18 19:48:28 -06:00
2e6889d9bb implement RSS feed in auto messages for issue #53
modified automessages to use async mesthods instead of synchronous
2019-02-18 19:30:38 -06:00
9c4d23f0b4 enhancement for issue #63 2019-02-17 18:48:40 -06:00
b4e3e8526a fix for issue #66 2019-02-17 13:16:48 -06:00
e3944fb8c2 add web project for stats to fix bug with pre compiled razor templates 2019-02-16 17:18:50 -06:00
40f1697c97 fix damage event not including log line
complete initiall implementation for "2FA"
issue #52
issue #66
2019-02-16 15:04:40 -06:00
2bbf2988da update application version 2019-02-15 22:20:45 -06:00
5e36bf4316 begin implementation of token authentication
replacing password authentication ingame
precompile views for webfront
issue #66
issue #52
2019-02-15 22:19:59 -06:00
a362caebac fix for T6 guid length including sign 2019-02-14 08:42:14 -06:00
2260d8974d update master to allow IW5 to pass validation
include version set on manual parser selection
update projects to .NET Core 2.2
add middleware to support ip whitelisting
(EnableWebfrontConnectionWhitelist and WebfrontConnectionWhitelist)
issue #59
2019-02-12 20:34:29 -06:00
5e04274da6 actually fix the encoding issue 2019-02-10 20:05:50 -06:00
6d2e6aee4f update application version 2019-02-09 21:26:39 -06:00
9e74c42246 fix small bug with log paths 2019-02-09 21:24:54 -06:00
dea5b3f954 fix reading PT6 having signed decimal GUID in log
fix  alternative encoding character converting
allow more paths for game log server
add localization for unknown ips in welcome plugin
add gamelog server uri to support game log server on games that must supply manual log path
misc fixes
2019-02-09 15:35:13 -06:00
7c6419a16a finish cod4x parser
add IW3 map names
2019-02-06 20:12:35 -06:00
0194196a33 update application version 2019-02-05 18:06:14 -06:00
044991272f update parsers to include game name
prompt to enter log path if game doesn't generate
2019-02-05 18:02:45 -06:00
f3290cf066 move IW4x parser out of code
add CoD4x parser
2019-02-05 11:14:43 -06:00
29eedea093 fix IW4x regression error with alternative encodings
add parser selection to server config setup
2019-02-04 19:38:24 -06:00
ce02f5dd68 Move T6 parser to javascript parser 2019-02-03 20:47:05 -06:00
e6bfa408f8 move IW3 parser to javascript 2019-02-02 20:19:24 -06:00
0a1dc46760 Add commenting for parsers
rename IW4*Parser to Base*Parser
2019-02-02 19:40:37 -06:00
97ba6aae2e put parser in right location :P 2019-02-02 19:13:17 -06:00
a456fab0e5 Increment version #
Add TeknoMW3 parser file
2019-02-02 19:11:34 -06:00
3e5282df87 Finish preliminary parser for TeknoMW3 2019-02-02 18:54:30 -06:00
59e0072744 Finish dynamic dvar parsing for IW4x 2019-02-01 19:49:25 -06:00
f1dd4f7c7f Fix IP parsing bug introduced with IW4Parser
Additional fix for Webfront Index OoB on _ClientActivity
2019-01-28 18:21:56 -06:00
760d3026ce Fixes for PR 2.3.4.0 2019-01-27 19:45:35 -06:00
07df6dbf79 Update version number and small plugin fix 2019-01-27 18:54:18 -06:00
ca535019c6 Finish RCON dynamic parser impl
Fix configuration generation bug
2019-01-27 18:41:54 -06:00
e6154822f6 Implement more dynamic parser stuff 2019-01-27 16:40:08 -06:00
7a6dccc26a Fix bug with webfront spamming issues when running
Remove IW5 parser
Begin implementation of dynamic parsers
2019-01-26 20:33:37 -06:00
08c883e0ff fix duplicate bot welcomes
fix prompt bool incorrect default value
rename GameEvent.Remote to GameEvent.IsRemote
include NetworkId in webfront claims
fix non descript error message appearing when something fails and localization is not initialized
2019-01-03 14:39:22 -06:00
aaf9eb09b6 more alias changes :(
fix flag penalty coming from wrong user
2019-01-02 18:32:39 -06:00
7b75c35c9b fix aliases for real (hopefully)
fix bug with flag not being applied
fix level being set based on IP instead of IP and name
2018-12-31 20:52:19 -06:00
07ec5cf52f update assembly version 2018-12-30 20:52:26 -06:00
cf5ee8765d finish alias fixes
add manual webfront bind url
2018-12-30 20:48:07 -06:00
9494a17997 update prompt utility functions for issue #65
tweaks to alias stuff
fix bug with bots not showing
2018-12-30 18:13:13 -06:00
5f4171ccf4 hopefulyl fix aliasing issue
bans are applied to an account if the accounts are linked but penallty on a different accounts
2018-12-29 12:43:40 -06:00
a10746d5ff Fix small issue with log reading 2018-12-19 19:24:31 -06:00
8dca05a442 Small fixes 2018-12-17 13:45:16 -06:00
8aa0d204f4 Delete stupid 2018-12-16 21:52:11 -06:00
12cf2e8247 Add server version to master api
Add IsEvadedOffense to EFPenalty
Fix remote log reading in not Windows
2018-12-16 21:16:56 -06:00
b77bdbe793 minor fixed 2018-12-03 19:21:13 -06:00
4522992c0e fix remote commands
user clientkick_for_reason for T6 parsers
small bug fixes
2018-12-01 12:17:53 -06:00
9d6cbee69c update stats
change server id
fIx change log server complaining when empty read
2018-11-27 18:31:48 -06:00
abf0609e2e fix only one administrator showing on admins page
fix profanity determent not applying penalties.
2018-11-25 21:11:55 -06:00
5ac8a55c72 fixes for new polling setup
update database model for alias (nullable ip)
heartbeats now send ip to master server
2018-11-25 20:00:36 -06:00
9bdd7d1b8a More work modifying client stuff 2018-11-07 20:30:11 -06:00
ed83c4c011 started work on getting the restart functionality in the gamelogserver
fix bug with unbanned players still showing as banned via lock icon
move player based stuff into client class
finally renamed Player to EFClient via partial class
don't try to run this build because it's in between stages
2018-11-05 21:01:29 -06:00
d9d548ea18 Small anti-cheat update 2018-10-28 20:47:56 -05:00
1779bf821d more work on skill based team balance.
added on player disconnect to custom callbacks
2018-10-25 08:14:39 -05:00
d50e6c8030 change penalty expiration datetime to null for perm bans
add tempban max time
allow searching for GUID
stats returns ranking as well
fix for promotion/demotion text
2018-10-15 19:51:04 -05:00
a58726d872 add gsc api controller for communicating with gsc
add ignore bots option
fix first localization message not working
2018-10-13 18:51:07 -05:00
dded60a6ef add test to print out all commands 2018-10-12 21:32:30 -05:00
305817d00c version 2.2 stable 2018-10-12 21:28:22 -05:00
65cf3566db fix publish for release
fix bug with localization issue crashing app if config is wrong
2018-10-12 18:59:17 -05:00
e91d076b41 curtail lost connection messages from RCon
verify still compatible with T6
fix potential null reference exception during configuration reading/setup
2018-10-10 19:22:08 -05:00
b5e9519f0c update shared guid kick plugin
script plugins reload without error using the correct file share mode when opening
increase socket timeout to 10 seconds
2018-10-09 20:19:06 -05:00
7caee55a7e refactored the welcome plugin to use a web api instead of a hard coded file
removed deprecated file class
don't wait for response when setting dvar/sending command in T6
potential fix for duplicate kick message in JS plugin
2018-10-08 21:15:59 -05:00
b289917319 update project to .net core 2.1.5
got rid of "threadsafe" stats service in stats plugin
2018-10-07 21:34:30 -05:00
c8366a22e5 fixed the vpn detection plugin method signature call
added some fixes for stats/ac
2018-10-06 15:31:05 -05:00
de902a58ac write individual server log files and main log file seperately
log writing is thread safe now
2018-10-06 11:47:14 -05:00
c7547f1ad5 clean up publish folder output to have a less cluttered structure.
add migration class to perform the migration on update
2018-10-05 22:10:39 -05:00
9d946d1bad more stability changes 2018-10-03 21:20:49 -05:00
f4ac815d07 hopefully finished with RCon changes.
added more tests.
fixed issues from event changes (there's most definitely still issues related to that)
2018-10-02 12:39:08 -05:00
7fa0b52543 more rcon tweaks, and starting on unit tests for commands bleh 2018-09-29 21:49:12 -05:00
d45729d7e1 clean up rcon, fix a bunch of little things 2018-09-29 14:52:22 -05:00
5d93e7ac57 a ton of stuff and fix migations 2018-09-23 19:45:54 -05:00
0f9d2e92e1 fix for issue #50 2018-09-16 17:51:11 -05:00
4a46abc46d add index to time sent in EFCLientMessage, so we can retrieve faster in context view
set the maximum height of the
add link to profile on client chat
move change history into a seperate service
move around AC penalty processing
2018-09-16 15:34:16 -05:00
7c708f06f3 adds two day ban to drop down on for issue #47 remove IW5 (Pluto) from supported client until it's rewritten 2018-09-13 20:00:41 -05:00
98adfb12d2 update map names for IW4 (issue #48)
only check shared GUID for IW4
optimized get privileged clients query
fine-tuned the version printout to include revision numbers
2018-09-13 14:34:42 -05:00
a786541484 re-implemented auto-upload on publish
fixed the max length migration for MySQL
configure the python projects to be able to be published from command line
optimize find active pentalties query
add feature for issue #38
testing fix for concurrent dict access (in stats plugin)
2018-09-12 19:53:11 -05:00
b9086fd145 fix parsing view angles in exponential form
update RestEase  and CodePages dependencies
optimized the find by name query
add index to name
2018-09-11 14:28:37 -05:00
3d8108f339 fixed profanity bug
fix the shared GUID connect
fix linux  log issue
2018-09-08 20:20:11 -05:00
39596db56e fixed rating and kill streak bug, but uncommenting something I forgot I commented out
Added SharedGUIDKick plugin to kick people with shared GUID
2018-09-08 17:29:30 -05:00
ba5b1e19a6 update readme
add vision average to client stats
other stuff
2018-09-07 22:29:42 -05:00
385879618d add game log server 2018-09-06 13:25:58 -05:00
0c90d02e44 update libraries to pre release
fix remaining issue for issue #32
adds overall ranking to profile page for issue #24
2018-09-04 21:07:34 -05:00
cfbacabb4a fix bug with player not getting updated on disconnect (related to issue #24)
jint version downgraded for better stability (also locked the engine instance as it's not thread safe)
updated readme
remove vpn detection from application configuration as it's now in a seperate plugin
defaulted webfront bind URl to all interfaces
readd the custom say name
added visibility percentage to AC
2018-09-04 12:40:29 -05:00
672d45df7c fix for issue #45 and #37 2018-09-02 22:09:25 -05:00
20d4ab27d3 add warn event
add alert to IW4ScriptCommands
2018-09-02 21:25:09 -05:00
e77ef69ee8 Added additional properties method to allow easier extension to client properties
updated VPN plugin to use WebClient
message is sent to client trying to execute commands before they are authenticated
fixed rare issue with ToAdmins failing
record bullet distance fraction for client kills (_customcallbacks)
change client level/permissions through webfront
ability to tempban through webfront
2018-09-02 16:59:27 -05:00
cc7628d058 fixed broken broadcast events
events don't get out of order when a invalid event line throws exception
handle the stats history update with no change throwing DBConcurrencyException
2018-08-31 22:35:51 -05:00
46bdc2ac33 moved event API stuff around
finally fixed threading issue (which actually had to do with IW4x log outputs being out of sync (not an issue with my code). What a lot of headache over something that wasn't my fault.
2018-08-30 20:53:00 -05:00
bbefd53db4 think I finished reworking the event system
added http log reading support for debugging remotely
started working on unit test framework
2018-08-28 16:32:59 -05:00
56cb8c50e7 reworked event management (again)
almost finished
2018-08-27 17:07:54 -05:00
0538d9f479 started update for readme
start update for version changes
hopefully fixed pesky stat bug
move vpn detection into script plugin
2018-08-26 19:20:47 -05:00
1343d4959e more support for javascript plugins 2018-08-23 16:16:30 -05:00
ac64d8d3c1 fixed unicode crap stuff in webhook
enable preview of tiered compiliation (faster startup)
ban events are sent to the API properly now
add vpn except id configuration
begin work on javascript plugin support
2018-08-22 20:25:34 -05:00
b5939bbdaf add more event types to discord webhook 2018-08-08 15:36:42 -05:00
a0fafe5797 cleaned up some namespace discrepancies
fixed the coloring for custom groups translation
add reserved slots
add webhook project to show notifications in discord
2018-08-07 13:43:09 -05:00
bbade07646 add localized level names
intellisense suggestion junk
2018-08-03 21:11:58 -05:00
3c0e101f14 appeal website is always show on kick
previously banned for recursion fixed
2018-08-03 19:06:47 -05:00
d0be08629d add page list to manager so we can inject pages into the layout view 2018-08-03 17:10:20 -05:00
9d00d5a16a removed event controller, and added status to api controller
get time passed returns weeks after 90 days
and months after 365
2018-08-02 20:52:35 -05:00
396e5c9215 confirmed working for linux
fixed the database access issue
2018-08-01 21:09:22 -05:00
4ec16d3aa2 increased max events for event api to 100
added GameInfo to EventInfo class
make sure score gets updated properly after authentication
2018-07-30 19:31:00 -05:00
f40bcce44f update to .NET Core 2.1
fix bower repo deprecation
2018-07-29 14:43:42 -05:00
6071ad8653 fix bug with AC failing to ban because of EF issue. 2018-07-05 21:04:34 -05:00
16d7ccd590 fix parsing of certain chat messages
print out the correct exception message when a server is not responding.
prevent log reader from reading before the servers have initialized
2018-07-04 21:09:42 -05:00
87541c4a5a more changes to the event management.
bots ip adjusted
2018-07-01 19:30:38 -05:00
af6361144e moved validate command into shared library.
reworked connection system to read from log file for join/quits and authenticate later with polling
2018-06-30 20:55:16 -05:00
454238a192 finer version numbers work correctly.
fix bug with level being reset.
add {{ADMINS}} to message tokens
modified offset threshold calculation
2018-06-26 20:17:24 -05:00
e7c7145da1 Show time passed since ban instead of "forever"
reworked event api to include all events (sans unknown)
2018-06-16 21:11:25 -05:00
5be6b75ccf [webfront] search by ip and name
[application] levels set properly with multiple GUIDs
[stats] require 3 hours of playtime for top stats recognition
[application] configurable rcon polling rate
2018-06-07 21:19:12 -05:00
e60f612f95 [application] added chat context to profile page
[iw4script] reworked balance to balance based on performance rating
[stats] log penalty context to database
2018-06-05 16:31:36 -05:00
ba023ceeb5 [application] added next map command and token 2018-06-02 21:21:01 -05:00
e3dba96d72 [sharedlibrary] add client meta
[application] add gravatar command
2018-06-01 23:48:10 -05:00
6d0f859a93 more updates to top stats page 2018-06-01 19:55:26 -05:00
696e2d12c9 change table design for rating history 2018-05-31 19:17:52 -05:00
bf68e5672f Add automated ban offense for anti-cheat
add EFClientStatHistory and EFClientAverageStatHistory for tracking change of stats over time
2018-05-30 20:50:20 -05:00
2204686b08 added top player stats
fix for some commands returning multiple matches found when target not required
2018-05-28 20:30:31 -05:00
897ec0d0c1 [master] make version info update live instead of requiring a restart in 2018-05-24 21:39:03 -05:00
d9a601328c stats tweaked to scale SPM based on team size
invalid client id results in 404 rather than exception page
performance based on traditional elo rating
fixed @ (broadcast commands)
added reports to penalty list and profile
2018-05-24 14:48:57 -05:00
36d493f05b update file localizations
update custom callbacks
add server count to master
add most played to token list
2018-05-21 16:09:27 -05:00
be68335f70 update change tracking and elo
master shows monitoring server count
master can provide individual localizations
2018-05-20 21:35:56 -05:00
4d585e6ab2 set default elo rating
maybe fix deadlock again :c
changed "skill" to Performance (Skill + Elo / 2)
2018-05-17 18:31:58 -05:00
4006c09045 add most played command
hopefully fixed thread lock?
started work on elo rating
2018-05-15 23:57:37 -05:00
699c19cd4b adding Cod4 support (for steam GUID is truncated to 16 characters)
exit properly whoops
add all linked accounts to drop down
consolidate linked admin accounts to the most recently seen one
limited some waits to 5s to hopefully prevent a rare thread lock
2018-05-14 12:55:10 -05:00
6e5501b32d fix T6 reading
add WaW support
fix stats threading
2018-05-10 23:52:20 -05:00
e964013700 lots of fixes :) 2018-05-10 00:34:29 -05:00
9ff7f39e8d SPM fix for negative/teamdamage
added localization as downloaded from the Master API
interupted network communication no longer treated as unknown exception
topstats prints the right message if no one qualifies
angle adjustments
move unflag to seperate command
2018-05-07 23:58:46 -05:00
a54ea3913d add translation for webfront
discord link has been genericized to social link
2018-05-05 17:52:04 -05:00
e8dff01c41 re-added the kill server command (can only be used if run as admin)
less warns when using a disposed socket
topstats added to tokens as {{TOPSTATS}}
fixed topstats reporting for only a single server
added fix to iw4 regex for negative score
tokens now support multiple lines (using Environment.NewLine to separate)
localization includes culture again
2018-05-05 15:36:26 -05:00
3092a529e9 add penalties for angle info
queue Tell/Say to prevent plugins from throwing exception when server is offlline
fixed CPU usage issue over time
sort penalties by type on webfront
2018-05-03 23:22:10 -05:00
f442f251f6 more stat SPM fixes
prevent null say event from executing when exiting
adjusted rcon and socket timeout
fixed bug with login/setpassword not working after claiming ownership
2018-05-03 00:25:49 -05:00
3a463be7f8 Profanity deterrent kick players with offensive names
status parsing with Regex in IW4 is much cleaner
fixed tempban not always kicking
made plugin event tasks parallel
2018-04-29 15:44:04 -05:00
35e7f57156 fixed up IW5 parser with new event system
changed login alias to li (duplicate)
fixed crashing bug in generic repo
fixed anonymous name in access to web console
2018-04-28 20:11:13 -05:00
8071fb37bc SPM and skill is rounded in profile now
fixed web console not waiting for reponse
fixed password not saving over time
web users level update properly now when promoted/demoted
2018-04-28 16:39:45 -05:00
bb90a807b7 moved heartbeat to timer instead of manual task/thread
GameEventHandler uses ConcurrentQueue for events
exception handlers for events and log reading
added IW4ScriptCommands plugin
fixed stats
lots of little fixes
2018-04-28 00:22:18 -05:00
2c2c442ba7 updated portuguese translation
fixed issue with locale when no config present
changed kick color on webfront
aliased owner to iamgod (for b3 familiar users)
hopefully fixed stats issue
added T5M (V2 BO2) support
made dvar grab at beginning minimal to prevent throttling on older CODS
2018-04-26 19:19:42 -05:00
b6c979beba fixed base controller Manager being null
fixed log reading duplicates with new event processing
added portuguese translation
2018-04-26 15:26:03 -05:00
99390f1f35 fixed issue with status response erroring when incorrect length
view angle vector parse fail is now a handled exception
change local host check to byte array to make it faster than comparing string
kick command now requires moderator level or higher
tempban now requires administrator level or higher
hopefully fixed negative SPM bug
pipelined the events and consolidated them to run through GameEventHandler
uniform console colors
2018-04-26 01:13:04 -05:00
ece519251a added MySQL support
fixed login bug
IW3 official support
2018-04-25 01:38:59 -05:00
0e3d280595 more localization
fixed issue with IW4 parser not reading map changes properly
2018-04-24 17:01:27 -05:00
5dfaa4ebd6 update projects to .NET Core 2.0.7
added instance and client count to api page
removed vestigial ConfigGenerator
2018-04-23 16:03:50 -05:00
02ef5a0bf8 adding IW5m parsers
reduce status polling rate
adding preliminary russian localization
small rcon tweak to attempt to send custom encoded messages
removed exception handling in ConvertLong
throttled servers will still attempt to execute events
2018-04-23 00:43:48 -05:00
515 changed files with 55594 additions and 8663 deletions

18
.gitignore vendored
View File

@ -220,7 +220,23 @@ Thumbs.db
DEPLOY
global.min.css
global.min.js
bootstrap-custom.css
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
/GameLogServer/env
launchSettings.json
/VpnDetectionPrivate.js
/Plugins/ScriptPlugins/VpnDetectionPrivate.js
**/Master/env_master
/GameLogServer/log_env
**/*.css
/Master/master/persistence
/WebfrontCore/wwwroot/fonts
/WebfrontCore/wwwroot/font

View File

@ -1,75 +0,0 @@
using System;
using System.Collections.Generic;
using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Application.API
{
class EventApi : IEventApi
{
Queue<EventInfo> Events = new Queue<EventInfo>();
DateTime LastFlagEvent;
static string[] FlaggedMessageContains =
{
" wh ",
"hax",
"cheat",
" hack ",
"aim",
"wall",
"cheto",
"hak",
"bot"
};
int FlaggedMessageCount;
public Queue<EventInfo> GetEvents() => Events;
public void OnServerEvent(object sender, GameEvent E)
{
if (E.Type == GameEvent.EventType.Say && E.Origin.Level < Player.Permission.Trusted)
{
bool flaggedMessage = false;
foreach (string msg in FlaggedMessageContains)
flaggedMessage = flaggedMessage ? flaggedMessage : E.Data.ToLower().Contains(msg);
if (flaggedMessage)
FlaggedMessageCount++;
if (FlaggedMessageCount > 3)
{
if (Events.Count > 20)
Events.Dequeue();
FlaggedMessageCount = 0;
E.Owner.Broadcast("If you suspect someone of ^5CHEATING ^7use the ^5!report ^7command").Wait();
Events.Enqueue(new EventInfo(
EventInfo.EventType.ALERT,
EventInfo.EventVersion.IW4MAdmin,
"Chat indicates there may be a cheater",
"Alert",
E.Owner.Hostname, ""));
}
if ((DateTime.UtcNow - LastFlagEvent).Minutes >= 3)
{
FlaggedMessageCount = 0;
LastFlagEvent = DateTime.Now;
}
}
if (E.Type == GameEvent.EventType.Report)
{
Events.Enqueue(new EventInfo(
EventInfo.EventType.ALERT,
EventInfo.EventVersion.IW4MAdmin,
$"**{E.Origin.Name}** has reported **{E.Target.Name}** for: {E.Data.Trim()}",
E.Target.Name, E.Origin.Name, ""));
}
}
}
}

View File

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

View File

@ -0,0 +1,19 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application.API.GameLogServer
{
public class LogInfo
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("length")]
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

@ -8,9 +8,13 @@ namespace IW4MAdmin.Application.API.Master
public class ApiServer
{
[JsonProperty("id")]
public int Id { get; set; }
public long Id { get; set; }
[JsonProperty("ip")]
public string IPAddress { get; set; }
[JsonProperty("port")]
public short Port { get; set; }
[JsonProperty("version")]
public string Version { get; set; }
[JsonProperty("gametype")]
public string Gametype { get; set; }
[JsonProperty("map")]

View File

@ -1,16 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RestEase;
using SharedLibraryCore;
namespace IW4MAdmin.Application.API.Master
{
/// <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();
@ -29,29 +34,38 @@ 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()
{
ClientNum = s.ClientNum,
Game = s.GameName.ToString(),
Version = s.Version,
Gametype = s.Gametype,
Hostname = s.Hostname,
Map = s.CurrentMap.Name,
MaxClientNum = s.MaxClients,
Id = s.GetHashCode(),
Port = (short)s.GetPort()
Id = s.EndPoint,
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
@ -36,13 +40,16 @@ namespace IW4MAdmin.Application.API.Master
public class Endpoint
{
#if !DEBUG
private static IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
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,12 +60,20 @@ 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();
[Get("localization/{languageTag}")]
Task<SharedLibraryCore.Localization.Layout> GetLocalization([Path("languageTag")] string languageTag);
}
}

View File

@ -2,15 +2,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2.1.0</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 server</Description>
<Copyright>2018</Copyright>
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated servers</Description>
<Copyright>2019</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>
@ -19,57 +19,59 @@
<AssemblyName>IW4MAdmin</AssemblyName>
<Configurations>Debug;Release;Prerelease</Configurations>
<Win32Resource />
<RootNamespace>IW4MAdmin.Application</RootNamespace>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RestEase" Version="1.4.5" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" />
<PackageReference Include="RestEase" Version="1.4.10" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" />
</ItemGroup>
<PropertyGroup>
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation>
<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>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\IW4MAdmin.en-US.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>IW4MAdmin.en-US.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\IW4MAdmin.en-US.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>IW4MAdmin.en-US.Designer.cs</LastGenOutput>
</EmbeddedResource>
<ProjectReference Include="..\WebfrontCore\WebfrontCore.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="DefaultSettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\IW4MAdmin.en-US.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="call $(ProjectDir)BuildScripts\PreBuild.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PreBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" />
</Target>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="call $(ProjectDir)BuildScripts\PostBuild.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
<Output TaskParameter="Assemblies" ItemName="CurrentAssembly" />
</GetAssemblyIdentity>
<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 $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
<Exec Command="if $(ConfigurationName) == Debug call $(ProjectDir)BuildScripts\PostPublish.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(ConfigurationName)" />
</Target>
</Project>

View File

@ -0,0 +1,799 @@
using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers;
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.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
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 readonly ConcurrentBag<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
public ILogger Logger => GetLogger(0);
public bool Running { get; private set; }
public bool IsInitialized { get; private 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 { get; }
public CancellationToken CancellationToken => _tokenSource.Token;
public string ExternalIPAddress { get; private set; }
public bool IsRestartRequested { get; private set; }
public IMiddlewareActionHandler MiddlewareActionHandler { get; }
private readonly List<IManagerCommand> _commands;
private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc;
readonly AliasService AliasSvc;
readonly PenaltyService PenaltySvc;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
GameEventHandler Handler;
readonly IPageList PageList;
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 IPluginImporter _pluginImporter;
public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IPluginImporter pluginImporter)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>();
ClientSvc = new ClientService();
AliasSvc = new AliasService();
PenaltySvc = new PenaltyService();
ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow;
PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>();
AdditionalRConParsers = new List<IRConParser>();
TokenAuthenticator = new TokenAuthentication();
_metaService = new MetaService();
_tokenSource = new CancellationTokenSource();
_loggers.Add(0, logger);
_commands = commands.ToList();
_translationLookup = translationLookup;
_commandConfiguration = commandConfiguration;
_pluginImporter = pluginImporter;
}
public async Task ExecuteEvent(GameEvent newEvent)
{
#if DEBUG == true
Logger.WriteDebug($"Entering event process for {newEvent.Id}");
#endif
// the event has failed already
if (newEvent.Failed)
{
goto skip;
}
try
{
await newEvent.Owner.ExecuteEvent(newEvent);
// save the event info to the database
var changeHistorySvc = new ChangeHistoryService();
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 = EventFailReason.Permission;
newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {ex.Message}");
}
catch (NetworkException ex)
{
newEvent.FailReason = EventFailReason.Exception;
Logger.WriteError(ex.Message);
Logger.WriteDebug(ex.GetExceptionInfo());
}
catch (ServerException ex)
{
newEvent.FailReason = EventFailReason.Exception;
Logger.WriteWarning(ex.Message);
}
catch (Exception ex)
{
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.Complete();
#if DEBUG == true
Logger.WriteDebug($"Exiting event process for {newEvent.Id}");
#endif
}
public IList<Server> GetServers()
{
return Servers;
}
public IList<IManagerCommand> GetCommands()
{
return _commands;
}
public async Task UpdateServerStates()
{
// store the server hash code and task for it
var runningUpdateTasks = new Dictionary<long, Task>();
while (!_tokenSource.IsCancellationRequested)
{
// select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks
.Where(ut => ut.Value.Status == TaskStatus.RanToCompletion ||
ut.Value.Status == TaskStatus.Canceled ||
ut.Value.Status == TaskStatus.Faulted)
.Select(ut => ut.Key)
.ToList();
// this is to prevent the log reader from starting before the initial
// query of players on the server
if (serverTasksToRemove.Count > 0)
{
IsInitialized = true;
}
// remove the update tasks as they have completd
foreach (long serverId in serverTasksToRemove)
{
runningUpdateTasks.Remove(serverId);
}
// select the servers where the tasks have completed
var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
{
runningUpdateTasks.Add(server.EndPoint, Task.Run(async () =>
{
try
{
await server.ProcessUpdatesAsync(_tokenSource.Token);
if (server.Throttled)
{
await Task.Delay((int)_throttleTimeout.TotalMilliseconds, _tokenSource.Token);
}
}
catch (Exception e)
{
Logger.WriteWarning($"Failed to update status for {server}");
Logger.WriteDebug(e.GetExceptionInfo());
}
finally
{
server.IsInitialized = true;
}
}));
}
#if DEBUG
Logger.WriteDebug($"{runningUpdateTasks.Count} servers queued for stats updates");
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
try
{
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;
}
}
}
public async Task Init()
{
Running = true;
ExternalIPAddress = await Utilities.GetExternalIP();
#region PLUGINS
foreach (var plugin in _pluginImporter.ActivePlugins)
{
try
{
if (plugin is ScriptPlugin scriptPlugin)
{
await scriptPlugin.Initialize(this);
scriptPlugin.Watcher.Changed += async (sender, e) =>
{
try
{
await scriptPlugin.Initialize(this);
}
catch (Exception ex)
{
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());
}
}
#endregion
#region CONFIG
var config = ConfigHandler.Configuration();
// copy over default config if it doesn't exist
if (config == null)
{
var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration();
ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
var newConfig = ConfigHandler.Configuration();
newConfig.AutoMessages = defaultConfig.AutoMessages;
newConfig.GlobalRules = defaultConfig.GlobalRules;
newConfig.Maps = defaultConfig.Maps;
newConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
newConfig.QuickMessages = defaultConfig.QuickMessages;
if (newConfig.Servers == null)
{
ConfigHandler.Set(newConfig);
newConfig.Servers = new ServerConfiguration[1];
do
{
var serverConfig = new ServerConfiguration();
foreach (var parser in AdditionalRConParsers)
{
serverConfig.AddRConParser(parser);
}
foreach (var parser in AdditionalEventParsers)
{
serverConfig.AddEventParser(parser);
}
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();
}
}
else
{
if (string.IsNullOrEmpty(config.Id))
{
config.Id = Guid.NewGuid().ToString();
await ConfigHandler.Save();
}
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
{
config.WebfrontBindUrl = "http://0.0.0.0:1624";
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);
if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null)
{
foreach (var parser in AdditionalRConParsers)
{
serverConfig.AddRConParser(parser);
}
foreach (var parser in AdditionalEventParsers)
{
serverConfig.AddEventParser(parser);
}
serverConfig.ModifyParsers();
}
await ConfigHandler.Save();
}
}
if (config.Servers.Length == 0)
{
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
}
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252");
#endregion
#region DATABASE
using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString,
GetApplicationSettings().Configuration()?.DatabaseProvider))
{
await new ContextSeed(db).Seed();
}
#endregion
#region COMMANDS
if (ClientSvc.GetOwners().Result.Count > 0)
{
_commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
}
List<IManagerCommand> commandsToAddToConfig = new List<IManagerCommand>();
var cmdConfig = _commandConfiguration.Configuration();
if (cmdConfig == null)
{
cmdConfig = new CommandConfiguration();
commandsToAddToConfig.AddRange(_commands);
}
else
{
var unsavedCommands = _commands.Where(_cmd => !cmdConfig.Commands.Keys.Contains(_cmd.GetType().Name));
commandsToAddToConfig.AddRange(unsavedCommands);
}
foreach (var cmd in commandsToAddToConfig)
{
cmdConfig.Commands.Add(cmd.GetType().Name,
new CommandProperties()
{
Name = cmd.Name,
Alias = cmd.Alias,
MinimumPermission = cmd.Permission
});
}
_commandConfiguration.Set(cmdConfig);
await _commandConfiguration.Save();
#endregion
#region META
async Task<List<ProfileMeta>> getProfileMeta(int clientId, int offset, int count, DateTime? startAt)
{
var metaList = new List<ProfileMeta>();
// we don't want to return anything because it means we're trying to retrieve paged meta data
if (count > 1)
{
return metaList;
}
var lastMapMeta = await _metaService.GetPersistentMeta("LastMapPlayed", new EFClient() { ClientId = clientId });
if (lastMapMeta != null)
{
metaList.Add(new ProfileMeta()
{
Id = lastMapMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_MAP"],
Value = lastMapMeta.Value,
Show = true,
Type = ProfileMeta.MetaType.Information,
});
}
var lastServerMeta = await _metaService.GetPersistentMeta("LastServerPlayed", new EFClient() { ClientId = clientId });
if (lastServerMeta != null)
{
metaList.Add(new ProfileMeta()
{
Id = lastServerMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_SERVER"],
Value = lastServerMeta.Value,
Show = true,
Type = ProfileMeta.MetaType.Information
});
}
var client = await GetClientService().Get(clientId);
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_TIME_HOURS"]} {Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PLAYER"]}",
Value = Math.Round(client.TotalConnectionTime / 3600.0, 1).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Show = true,
Column = 1,
Order = 0,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_FSEEN"],
Value = Utilities.GetTimePassed(client.FirstConnection, false),
Show = true,
Column = 1,
Order = 1,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_LSEEN"],
Value = Utilities.GetTimePassed(client.LastConnection, false),
Show = true,
Column = 1,
Order = 2,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_CONNECTIONS"],
Value = client.Connections.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Show = true,
Column = 1,
Order = 3,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_MASKED"],
Value = client.Masked ? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"] : Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"],
Sensitive = true,
Column = 1,
Order = 4,
Type = ProfileMeta.MetaType.Information
});
return metaList;
}
async Task<List<ProfileMeta>> getPenaltyMeta(int clientId, int offset, int count, DateTime? startAt)
{
if (count <= 1)
{
return new List<ProfileMeta>();
}
var penalties = await GetPenaltyService().GetClientPenaltyForMetaAsync(clientId, count, offset, startAt);
return penalties.Select(_penalty => new ProfileMeta()
{
Id = _penalty.Id,
Type = _penalty.PunisherId == clientId ? ProfileMeta.MetaType.Penalized : ProfileMeta.MetaType.ReceivedPenalty,
Value = _penalty,
When = _penalty.TimePunished,
Sensitive = _penalty.Sensitive
})
.ToList();
}
MetaService.AddRuntimeMeta(getProfileMeta);
MetaService.AddRuntimeMeta(getPenaltyMeta);
#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, _translationLookup, _pluginImporter);
await ServerInstance.Initialize();
_servers.Add(ServerInstance);
Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname));
// add the start event for this server
var e = new GameEvent()
{
Type = GameEvent.EventType.Start,
Data = $"{ServerInstance.GameName} started",
Owner = ServerInstance
};
Handler.AddEvent(e);
successServers++;
}
catch (ServerException e)
{
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"].FormatExt($"[{Conf.IPAddress}:{Conf.Port}]"));
if (e.GetType() == typeof(DvarException))
{
Logger.WriteDebug($"{e.Message} {(e.GetType() == typeof(DvarException) ? $"({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})" : "")}");
}
lastException = e;
}
}
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
if (successServers == 0)
{
throw lastException;
}
if (successServers != config.Servers.Length)
{
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
{
throw lastException;
}
}
}
private async Task SendHeartbeat()
{
bool connected = false;
while (!_tokenSource.IsCancellationRequested)
{
if (!connected)
{
try
{
await Heartbeat.Send(this, true);
connected = true;
}
catch (Exception e)
{
connected = false;
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
}
}
else
{
try
{
await Heartbeat.Send(this);
}
catch (System.Net.Http.HttpRequestException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
}
catch (AggregateException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(RestEase.ApiException));
foreach (var ex in exceptions)
{
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
}
}
catch (RestEase.ApiException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
}
catch (Exception e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
}
}
try
{
await Task.Delay(30000, _tokenSource.Token);
}
catch { break; }
}
}
public async Task Start()
{
await Task.WhenAll(new[]
{
SendHeartbeat(),
UpdateServerStates()
});
}
public void Stop()
{
_tokenSource.Cancel();
Running = false;
}
public void Restart()
{
IsRestartRequested = true;
Stop();
}
public ILogger GetLogger(long serverId)
{
if (_loggers.ContainsKey(serverId))
{
return _loggers[serverId];
}
else
{
var newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
_loggers.Add(serverId, newLogger);
return newLogger;
}
}
public IList<MessageToken> GetMessageTokens()
{
return MessageTokens;
}
public IList<EFClient> GetActiveClients()
{
// 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()
{
return ClientSvc;
}
public AliasService GetAliasService()
{
return AliasSvc;
}
public PenaltyService GetPenaltyService()
{
return PenaltySvc;
}
public IConfigurationHandler<ApplicationConfiguration> GetApplicationSettings()
{
return ConfigHandler;
}
public IEventHandler GetEventHandler()
{
return Handler;
}
public IList<Assembly> GetPluginAssemblies()
{
return _pluginImporter.PluginAssemblies.Union(_pluginImporter.Assemblies).ToList();
}
public IPageList GetPageList()
{
return PageList;
}
public IRConParser GenerateDynamicRConParser(string name)
{
return new DynamicRConParser()
{
Name = name
};
}
public IEventParser GenerateDynamicEventParser(string name)
{
return new DynamicEventParser()
{
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);
}
}
}

View File

@ -2,6 +2,9 @@ set SolutionDir=%1
set ProjectDir=%2
set TargetDir=%3
set OutDir=%4
set Version=%5
echo %Version% > "%SolutionDir%DEPLOY\version.txt"
echo Copying dependency configs
copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%"
@ -15,4 +18,10 @@ if not exist "%TargetDir%Plugins" (
xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
echo Copying plugins for publish
del %SolutionDir%BUILD\Plugins\Tests.dll
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\Windows\Plugins\"
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
echo Copying script plugins for publish
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\Windows\Plugins\"
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"

View File

@ -1,29 +1,41 @@
set SolutionDir=%1
set ProjectDir=%2
set TargetDir=%3
set PublishDir=%1
set SourceDir=%2
SET COPYCMD=/Y
echo Deleting extra language files
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 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'
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'
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\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'
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"
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'
echo making start scripts
@(echo @echo off && echo @title IW4MAdmin && echo set DOTNET_CLI_TELEMETRY_OPTOUT=1 && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%PublishDir%\StartIW4MAdmin.cmd"
@(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%PublishDir%\StartIW4MAdmin.sh"
echo Deleting misc files
if exist "%SolutionDir%Publish\Windows\web.config" del "%SolutionDir%Publish\Windows\web.config"
del "%SolutionDir%Publish\Windows\*pdb"
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

@ -1,62 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace IW4MAdmin.Application
{
class ConfigurationGenerator
{
public static List<ServerConfiguration> GenerateServerConfig(List<ServerConfiguration> configList)
{
var loc = Utilities.CurrentLocalization.LocalizationSet;
var newConfig = new ServerConfiguration();
while (string.IsNullOrEmpty(newConfig.IPAddress))
{
try
{
string input = Utilities.PromptString(loc["SETUP_SERVER_IP"]);
IPAddress.Parse(input);
newConfig.IPAddress = input;
}
catch (Exception)
{
continue;
}
}
while (newConfig.Port == 0)
{
try
{
newConfig.Port = Int16.Parse(Utilities.PromptString(loc["SETUP_SERVER_PORT"]));
}
catch (Exception)
{
continue;
}
}
newConfig.Password = Utilities.PromptString(loc["SETUP_SERVER_RCON"]);
newConfig.AutoMessages = new List<string>();
newConfig.Rules = new List<string>();
newConfig.UseT6MParser = Utilities.PromptBool(loc["SETUP_SERVER_USET6M"]);
configList.Add(newConfig);
if (Utilities.PromptBool(loc["SETUP_SERVER_SAVE"]))
GenerateServerConfig(configList);
return configList;
}
}
}

View File

@ -3,6 +3,7 @@
"AutoMessages": [
"This server uses ^5IW4M Admin v{{VERSION}} ^7get it at ^5raidmax.org/IW4MAdmin",
"^5IW4M Admin ^7sees ^5YOU!",
"{{TOPSTATS}}",
"This server has seen a total of ^5{{TOTALPLAYERS}} ^7players!",
"Cheaters are ^1unwelcome ^7 on this server",
"Did you know 8/10 people agree with unverified statistics?"
@ -15,7 +16,231 @@
"Keep grenade launcher use to a minimum",
"Balance teams at ALL times"
],
"DisallowedClientNames": [ "Unknown Soldier", "VickNet", "UnknownSoldier", "CHEATER", "Play77" ],
"QuickMessages": [
{
"Game": "IW4",
"Messages": {
"QUICKMESSAGE_AREA_SECURE": "Area secure!",
"QUICKMESSAGE_ARE_YOU_CRAZY": "Are you crazy?",
"QUICKMESSAGE_ATTACK_LEFT_FLANK": "Attack left flank!",
"QUICKMESSAGE_ATTACK_RIGHT_FLANK": "Attack right flank!",
"QUICKMESSAGE_COME_ON": "Come on.",
"QUICKMESSAGE_ENEMIES_SPOTTED": "Multiple contacts!",
"QUICKMESSAGE_ENEMY_DOWN": "Enemy down!",
"QUICKMESSAGE_ENEMY_GRENADE": "Enemy grenade!",
"QUICKMESSAGE_ENEMY_SPOTTED": "Contact!",
"QUICKMESSAGE_FALL_BACK": "Fall back!",
"QUICKMESSAGE_FOLLOW_ME": "On me!",
"QUICKMESSAGE_GREAT_SHOT": "Nice shot!",
"QUICKMESSAGE_GRENADE": "Grenade!",
"QUICKMESSAGE_HOLD_THIS_POSITION": "Hold this position!",
"QUICKMESSAGE_HOLD_YOUR_FIRE": "Hold your fire!",
"QUICKMESSAGE_IM_IN_POSITION": "In position.",
"QUICKMESSAGE_IM_ON_MY_WAY": "Moving.",
"QUICKMESSAGE_MOVE_IN": "Move in!",
"QUICKMESSAGE_NEED_REINFORCEMENTS": "Need reinforcements!",
"QUICKMESSAGE_NO_SIR": "Negative.",
"QUICKMESSAGE_ON_MY_WAY": "On my way.",
"QUICKMESSAGE_REGROUP": "Regroup!",
"QUICKMESSAGE_SNIPER": "Sniper!",
"QUICKMESSAGE_SORRY": "Sorry.",
"QUICKMESSAGE_SQUAD_ATTACK_LEFT_FLANK": "Squad, attack left flank!",
"QUICKMESSAGE_SQUAD_ATTACK_RIGHT_FLANK": "Squad, attack right flank!",
"QUICKMESSAGE_SQUAD_HOLD_THIS_POSITION": "Squad, hold this position!",
"QUICKMESSAGE_SQUAD_REGROUP": "Squad, regroup!",
"QUICKMESSAGE_SQUAD_STICK_TOGETHER": "Squad, stick together!",
"QUICKMESSAGE_STICK_TOGETHER": "Stick together!",
"QUICKMESSAGE_SUPPRESSING_FIRE": "Base of fire!",
"QUICKMESSAGE_TOOK_LONG_ENOUGH": "Took long enough!",
"QUICKMESSAGE_TOOK_YOU_LONG_ENOUGH": "Took you long enough!",
"QUICKMESSAGE_WATCH_SIX": "Watch your six!",
"QUICKMESSAGE_YES_SIR": "Roger.",
"QUICKMESSAGE_YOURE_CRAZY": "You're crazy!",
"QUICKMESSAGE_YOURE_NUTS": "You're nuts!",
"QUICKMESSAGE_YOU_OUTTA_YOUR_MIND": "You outta your mind?"
}
}
],
"Maps": [
{
"Game": "IW3",
"Maps": [
{
"Alias": "Ambush",
"Name": "mp_convoy"
},
{
"Alias": "Backlot",
"Name": "mp_backlot"
},
{
"Alias": "Bloc",
"Name": "mp_bloc"
},
{
"Alias": "Bog",
"Name": "mp_bog"
},
{
"Alias": "Countdown",
"Name": "mp_countdown"
},
{
"Alias": "Crash",
"Name": "mp_crash"
},
{
"Alias": "Crossfire",
"Name": "mp_crossfire"
},
{
"Alias": "District",
"Name": "mp_citystreets"
},
{
"Alias": "Downpour",
"Name": "mp_farm"
},
{
"Alias": "Overgrown",
"Name": "mp_overgrown"
},
{
"Alias": "Pipeline",
"Name": "mp_pipeline"
},
{
"Alias": "Shipment",
"Name": "mp_shipment"
},
{
"Alias": "Showdown",
"Name": "mp_showdown"
},
{
"Alias": "Strike",
"Name": "mp_strike"
},
{
"Alias": "Vacant",
"Name": "mp_vacant"
},
{
"Alias": "Wet Work",
"Name": "mp_cargoship"
},
{
"Alias": "Winter Crash",
"Name": "mp_crash_snow"
},
{
"Alias": "Broadcast",
"Name": "mp_broadcast"
},
{
"Alias": "Creek",
"Name": "mp_creek"
},
{
"Alias": "Chinatown",
"Name": "mp_carentan"
},
{
"Alias": "Killhouse",
"Name": "mp_killhouse"
}
]
},
{
"Game": "T4",
"Maps": [
{
"Alias": "Airfield",
"Name": "mp_airfield"
},
{
"Alias": "Asylum",
"Name": "mp_asylum"
},
{
"Alias": "Castle",
"Name": "mp_castle"
},
{
"Alias": "Cliffside",
"Name": "mp_shrine"
},
{
"Alias": "Courtyard",
"Name": "mp_courtyard"
},
{
"Alias": "Dome",
"Name": "mp_dome"
},
{
"Alias": "Downfall",
"Name": "mp_downfall"
},
{
"Alias": "Hanger",
"Name": "mp_hangar"
},
{
"Alias": "Makin",
"Name": "mp_makin"
},
{
"Alias": "Outskirts",
"Name": "mp_outskirts"
},
{
"Alias": "Roundhouse",
"Name": "mp_roundhouse"
},
{
"Alias": "Upheaval",
"Name": "mp_suburban"
},
{
"Alias": "Knee Deep",
"Name": "mp_kneedeep"
},
{
"Alias": "Nightfire",
"Name": "mp_nachtfeuer"
},
{
"Alias": "Station",
"Name": "mp_subway"
},
{
"Alias": "Banzai",
"Name": "mp_kwai"
},
{
"Alias": "Corrosion",
"Name": "mp_stalingrad"
},
{
"Alias": "Sub Pens",
"Name": "mp_docks"
},
{
"Alias": "Battery",
"Name": "mp_drum"
},
{
"Alias": "Breach",
"Name": "mp_bgate"
},
{
"Alias": "Revolution",
"Name": "mp_vodka"
}
]
},
{
"Game": "IW4",
"Maps": [
@ -24,11 +249,6 @@
"Name": "mp_rust"
},
{
"Alias": "Highrise",
"Name": "mp_highrise"
},
{
"Alias": "Terminal",
"Name": "mp_terminal"
@ -39,16 +259,6 @@
"Name": "mp_crash"
},
{
"Alias": "Skidrow",
"Name": "mp_nightshift"
},
{
"Alias": "Quarry",
"Name": "mp_quarry"
},
{
"Alias": "Afghan",
"Name": "mp_afghan"
@ -170,7 +380,7 @@
},
{
"Alias": "IW4 Credits",
"Alias": "Test map",
"Name": "iw4_credits"
},
@ -189,6 +399,11 @@
"Name": "mp_cargoship_sh"
},
{
"Alias": "Cargoship",
"Name": "mp_cargoship"
},
{
"Alias": "Shipment",
"Name": "mp_shipment"
@ -215,28 +430,306 @@
},
{
"Alias": "Favela - Tropical",
"Alias": "Tropical Favela",
"Name": "mp_fav_tropical"
},
{
"Alias": "Estate - Tropical",
"Alias": "Tropical Estate",
"Name": "mp_estate_tropical"
},
{
"Alias": "Crash - Tropical",
"Alias": "Tropical Crash",
"Name": "mp_crash_tropical"
},
{
"Alias": "Forgotten City",
"Name": "mp_bloc_sh"
},
{
"Alias": "Crossfire",
"Name": "mp_cross_fire"
},
{
"Alias": "Bloc",
"Name": "mp_bloc"
},
{
"Alias": "Oilrig",
"Name": "oilrig"
},
{
"Name": "Village",
"Alias": "co_hunted"
}
]
},
{
"Game": "T6M",
"Game": "T5",
"Maps": [
{
"Alias": "Array",
"Name": "mp_array"
},
{
"Alias": "Berlin Wall",
"Name": "mp_berlinwall2"
},
{
"Alias": "Convoy",
"Name": "mp_gridlock"
},
{
"Alias": "Cracked",
"Name": "mp_cracked"
},
{
"Alias": "Crisis",
"Name": "mp_crisis"
},
{
"Alias": "Discovery",
"Name": "mp_discovery"
},
{
"Alias": "Drive-In",
"Name": "mp_drivein"
},
{
"Alias": "Firing Range",
"Name": "mp_firingrange"
},
{
"Alias": "Grid",
"Name": "mp_duga"
},
{
"Alias": "Hangar 18",
"Name": "mp_area51"
},
{
"Alias": "Hanoi",
"Name": "mp_hanoi"
},
{
"Alias": "Havana",
"Name": "mp_cairo"
},
{
"Alias": "Hazard",
"Name": "mp_golfcourse"
},
{
"Alias": "Hotel",
"Name": "mp_hotel"
},
{
"Alias": "Jungle",
"Name": "mp_havoc"
},
{
"Alias": "Kowloon",
"Name": "mp_kowloon"
},
{
"Alias": "Launch",
"Name": "mp_cosmodrome"
},
{
"Alias": "Nuketown",
"Name": "mp_nuked"
},
{
"Alias": "Radiation",
"Name": "mp_radiation"
},
{
"Alias": "Silo",
"Name": "mp_silo"
},
{
"Alias": "Stadium",
"Name": "mp_stadium"
},
{
"Alias": "Stockpile",
"Name": "mp_outskirts"
},
{
"Alias": "Summit",
"Name": "mp_mountain"
},
{
"Alias": "Villa",
"Name": "mp_villa"
},
{
"Alias": "WMD",
"Name": "mp_russianbase"
},
{
"Alias": "Zoo",
"Name": "mp_zoo"
}
]
},
{
"Game": "IW5",
"Maps": [
{
"Alias": "Seatown",
"Name": "mp_seatown"
},
{
"Alias": "Lockdown",
"Name": "mp_alpha"
},
{
"Alias": "Mission",
"Name": "mp_bravo"
},
{
"Alias": "Carbon",
"Name": "mp_carbon"
},
{
"Alias": "Dome",
"Name": "mp_dome"
},
{
"Alias": "Arkaden",
"Name": "mp_plaza2"
},
{
"Alias": "Downturn",
"Name": "mp_exchange"
},
{
"Alias": "Bootleg",
"Name": "mp_bootleg"
},
{
"Alias": "Hardhat",
"Name": "mp_hardhat"
},
{
"Alias": "Interchange",
"Name": "mp_interchange"
},
{
"Alias": "Fallen",
"Name": "mp_lambeth"
},
{
"Alias": "Outpost",
"Name": "mp_radar"
},
{
"Alias": "Bakaara",
"Name": "mp_mogadishu"
},
{
"Alias": "Resistance",
"Name": "mp_paris"
},
{
"Alias": "Underground",
"Name": "mp_underground"
},
{
"Alias": "Village",
"Name": "mp_village"
},
{
"Alias": "Aground",
"Name": "mp_aground_ss"
},
{
"Alias": "Boardwalk",
"Name": "mp_boardwalk"
},
{
"Alias": "U-turn",
"Name": "mp_burn_ss"
},
{
"Alias": "Foundation",
"Name": "mp_cement"
},
{
"Alias": "Erosion",
"Name": "mp_courtyard_ss"
},
{
"Alias": "Intersection",
"Name": "mp_crosswalk_ss"
},
{
"Alias": "Getaway",
"Name": "mp_hillside_ss"
},
{
"Alias": "Piazza",
"Name": "mp_italy"
},
{
"Alias": "Sanctuary",
"Name": "mp_meteora"
},
{
"Alias": "Gulch",
"Name": "mp_moab"
},
{
"Alias": "Black Box",
"Name": "mp_morningwood"
},
{
"Alias": "Parish",
"Name": "mp_nola"
},
{
"Alias": "Overwatch",
"Name": "mp_overwatch"
},
{
"Alias": "Liberation",
"Name": "mp_park"
},
{
"Alias": "Oasis",
"Name": "mp_qadeem"
},
{
"Alias": "Lookout",
"Name": "mp_restrepo_ss"
},
{
"Alias": "Off Shore",
"Name": "mp_roughneck"
},
{
"Alias": "Decommission",
"Name": "mp_shipbreaker"
},
{
"Alias": "Vortex",
"Name": "mp_six_ss"
},
{
"Alias": "Terminal",
"Name": "mp_terminal_cls"
}
]
},
{
"Game": "T6",
"Maps": [
{
"Alias": "Aftermath",

View File

@ -0,0 +1,300 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System;
using System.Text.RegularExpressions;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.EventParsers
{
class BaseEventParser : IEventParser
{
public BaseEventParser()
{
Configuration = new DynamicEventParserConfiguration()
{
GameDirectory = "main",
};
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32});([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.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.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.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetTeam, 4);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetName, 5);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginNetworkId, 6);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginClientNumber, 7);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginTeam, 8);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginName, 9);
Configuration.Damage.AddMapping(ParserRegex.GroupType.Weapon, 10);
Configuration.Damage.AddMapping(ParserRegex.GroupType.Damage, 11);
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.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetTeam, 4);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetName, 5);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginNetworkId, 6);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginClientNumber, 7);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginTeam, 8);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginName, 9);
Configuration.Kill.AddMapping(ParserRegex.GroupType.Weapon, 10);
Configuration.Kill.AddMapping(ParserRegex.GroupType.Damage, 11);
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
}
public IEventParserConfiguration Configuration { get; set; }
public string Version { get; set; } = "CoD";
public Game GameName { get; set; } = Game.COD;
public string URLProtocolFormat { get; set; } = "CoD://{{ip}}:{{port}}";
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();
string[] lineSplit = logLine.Split(';');
string eventType = lineSplit[0];
if (eventType == "say" || eventType == "sayteam")
{
var matchResult = Regex.Match(logLine, Configuration.Say.Pattern);
if (matchResult.Success)
{
string message = matchResult
.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.ToString()
.Replace("\x15", "")
.Trim();
if (message.Length > 0)
{
long originId = matchResult.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle);
if (message[0] == '!' || message[0] == '@')
{
return new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = new EFClient() { NetworkId = originId },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = new EFClient() { NetworkId = originId },
Message = message,
Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin
};
}
}
}
if (eventType == "K")
{
var match = Regex.Match(logLine, Configuration.Kill.Pattern);
if (match.Success)
{
long originId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].Value.ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
long targetId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].Value.ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
return new GameEvent()
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
}
if (eventType == "D")
{
var regexMatch = Regex.Match(logLine, Configuration.Damage.Pattern);
if (regexMatch.Success)
{
long originId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
long targetId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
}
if (eventType == "J")
{
var regexMatch = Regex.Match(logLine, Configuration.Join.Pattern);
if (regexMatch.Success)
{
return new GameEvent()
{
Type = GameEvent.EventType.PreConnect,
Data = logLine,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(),
},
NetworkId = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle),
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting,
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true
};
}
}
if (eventType == "Q")
{
var regexMatch = Regex.Match(logLine, Configuration.Quit.Pattern);
if (regexMatch.Success)
{
return new GameEvent()
{
Type = GameEvent.EventType.PreDisconnect,
Data = logLine,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString()
},
NetworkId = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(Configuration.GuidNumberStyle),
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Disconnecting
},
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true
};
}
}
if (eventType.Contains("ExitLevel"))
{
return new GameEvent()
{
Type = GameEvent.EventType.MapEnd,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None
};
}
if (eventType.Contains("InitGame"))
{
string dump = eventType.Replace("InitGame: ", "");
return new GameEvent()
{
Type = GameEvent.EventType.MapChange,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
Extra = dump.DictionaryFromKeyValue(),
RequiredEntity = GameEvent.EventRequiredEntity.None
};
}
// this is a custom event printed out by _customcallbacks.gsc (used for team balance)
if (eventType == "JoinTeam")
{
return new GameEvent()
{
Type = GameEvent.EventType.JoinTeam,
Data = logLine,
Origin = new EFClient() { NetworkId = lineSplit[1].ConvertGuidToLong(Configuration.GuidNumberStyle) },
RequiredEntity = GameEvent.EventRequiredEntity.Target
};
}
// this is a custom event printed out by _customcallbacks.gsc (used for anticheat)
if (eventType == "ScriptKill")
{
long originId = lineSplit[1].ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
long targetId = lineSplit[2].ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
return new GameEvent()
{
Type = GameEvent.EventType.ScriptKill,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
// this is a custom event printed out by _customcallbacks.gsc (used for anticheat)
if (eventType == "ScriptDamage")
{
long originId = lineSplit[1].ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
long targetId = lineSplit[2].ConvertGuidToLong(Configuration.GuidNumberStyle, 1);
return new GameEvent()
{
Type = GameEvent.EventType.ScriptDamage,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Data = logLine,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None
};
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.EventParsers
{
/// <summary>
/// empty generic implementation of the IEventParserConfiguration
/// allows script plugins to generate dynamic event parsers
/// </summary>
sealed internal class DynamicEventParser : BaseEventParser
{
}
}

View File

@ -0,0 +1,21 @@
using SharedLibraryCore.Interfaces;
using System.Globalization;
namespace IW4MAdmin.Application.EventParsers
{
/// <summary>
/// generic implementation of the IEventParserConfiguration
/// allows script plugins to generate dynamic configurations
/// </summary>
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 NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
}
}

View File

@ -1,110 +0,0 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace Application.EventParsers
{
class IW4EventParser : IEventParser
{
public GameEvent GetEvent(Server server, string logLine)
{
string[] lineSplit = logLine.Split(';');
string cleanedEventLine = Regex.Replace(lineSplit[0], @"[0-9]+:[0-9]+\ ", "").Trim();
if (cleanedEventLine[0] == 'K')
{
if (!server.CustomCallback)
{
return new GameEvent()
{
Type = GameEvent.EventType.Script,
Data = logLine,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server
};
}
}
if (cleanedEventLine == "say" || cleanedEventLine == "sayteam")
{
return new GameEvent()
{
Type = GameEvent.EventType.Say,
Data = lineSplit[4].Replace("\x15", ""),
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server,
Message = lineSplit[4].Replace("\x15", "")
};
}
if (cleanedEventLine.Contains("ScriptKill"))
{
return new GameEvent()
{
Type = GameEvent.EventType.Script,
Data = logLine,
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong()),
Owner = server
};
}
if (cleanedEventLine.Contains("ExitLevel"))
{
return new GameEvent()
{
Type = GameEvent.EventType.MapEnd,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
if (cleanedEventLine.Contains("InitGame"))
{
return new GameEvent()
{
Type = GameEvent.EventType.MapChange,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
// other parsers can derive from this parser so we make it virtual
public virtual string GetGameDir() => "userraw";
}
}

View File

@ -1,12 +0,0 @@
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace Application.EventParsers
{
class IW5EventParser : IW4EventParser
{
public override string GetGameDir() => "rzodemo";
}
}

View File

@ -1,112 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace Application.EventParsers
{
class T6MEventParser : IEventParser
{
public GameEvent GetEvent(Server server, string logLine)
{
string cleanedLogLine = Regex.Replace(logLine, @"^ *[0-9]+:[0-9]+ *", "");
string[] lineSplit = cleanedLogLine.Split(';');
if (lineSplit[0][0] == 'K')
{
return new GameEvent()
{
Type = GameEvent.EventType.Script,
Data = cleanedLogLine,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server
};
}
if (lineSplit[0][0] == 'D')
{
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = cleanedLogLine,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server
};
}
if (lineSplit[0] == "say" || lineSplit[0] == "sayteam")
{
return new GameEvent()
{
Type = GameEvent.EventType.Say,
Data = lineSplit[4],
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server,
Message = lineSplit[4]
};
}
if (lineSplit[0].Contains("ExitLevel"))
{
return new GameEvent()
{
Type = GameEvent.EventType.MapEnd,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
/*if (lineSplit[0].Contains("ShutdownGame"))
{
}*/
if (lineSplit[0].Contains("InitGame"))
{
return new GameEvent()
{
Type = GameEvent.EventType.MapChange,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
public string GetGameDir() => $"t6r{Path.DirectorySeparatorChar}data";
}
}

View File

@ -0,0 +1,66 @@
using IW4MAdmin.Application.Misc;
using SharedLibraryCore;
using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using System.Threading;
namespace IW4MAdmin.Application
{
class GameEventHandler : IEventHandler
{
readonly ApplicationManager Manager;
private readonly EventProfiler _profiler;
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 GameEventHandler(IManager mgr)
{
Manager = (ApplicationManager)mgr;
_profiler = new EventProfiler(mgr.GetLogger(0));
GameEventAdded += GameEventHandler_GameEventAdded;
}
private async void GameEventHandler_GameEventAdded(object sender, GameEventArgs args)
{
var start = DateTime.Now;
await Manager.ExecuteEvent(args.Event);
EventApi.OnGameEvent(sender, args);
#if DEBUG
_profiler.Profile(start, DateTime.Now, args.Event);
#endif
}
public void AddEvent(GameEvent gameEvent)
{
#if DEBUG
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
gameEvent.Owner.Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
if (Manager.Running || overrideEvents.Contains(gameEvent.Type))
{
#if DEBUG
gameEvent.Owner.Logger.WriteDebug($"Adding event with id {gameEvent.Id}");
#endif
GameEventAdded?.Invoke(this, new GameEventArgs(null, false, gameEvent));
}
#if DEBUG
else
{
gameEvent.Owner.Logger.WriteDebug($"Skipping event as we're shutting down {gameEvent.Id}");
}
#endif
}
}
}

View File

@ -0,0 +1,121 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO
{
class GameLogEventDetection
{
private long previousFileSize;
private readonly Server _server;
private readonly IGameLogReader _reader;
private readonly string _gameLogFile;
private readonly bool _ignoreBots;
class EventState
{
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;
_ignoreBots = server.Manager.GetApplicationSettings().Configuration().IgnoreBots;
}
public async Task PollForChanges()
{
while (!_server.Manager.CancellationToken.IsCancellationRequested)
{
if (_server.IsInitialized)
{
try
{
await UpdateLogEvents();
}
catch (Exception e)
{
_server.Logger.WriteWarning($"Failed to update log event for {_server.EndPoint}");
_server.Logger.WriteDebug(e.GetExceptionInfo());
}
}
await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken);
}
_server.Logger.WriteDebug("Stopped polling for changes");
}
private async Task UpdateLogEvents()
{
long fileSize = _reader.Length;
if (previousFileSize == 0)
{
previousFileSize = fileSize;
}
long fileDiff = fileSize - previousFileSize;
// this makes the http log get pulled
if (fileDiff < 1 && fileSize != -1)
return;
var events = await _reader.ReadEventsFromLog(_server, 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 != 1)
{
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.GetEventHandler().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

@ -0,0 +1,83 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO
{
class GameLogReader : IGameLogReader
{
private readonly IEventParser _parser;
private readonly string _logFile;
public long Length => new FileInfo(_logFile).Length;
public int UpdateInterval => 300;
public GameLogReader(string logFile, IEventParser parser)
{
_logFile = logFile;
_parser = parser;
}
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, 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 (FileStream fs = new FileStream(_logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
byte[] buff = new byte[fileSizeDiff];
fs.Seek(startPosition, SeekOrigin.Begin);
await fs.ReadAsync(buff, 0, (int)fileSizeDiff, server.Manager.CancellationToken);
var stringBuilder = new StringBuilder();
char[] charBuff = Utilities.EncodingType.GetChars(buff);
foreach (char c in charBuff)
{
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.Where(_line => _line.Length > 0))
{
try
{
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);
}
}
return events;
}
}
}

View File

@ -0,0 +1,76 @@
using IW4MAdmin.Application.API.GameLogServer;
using RestEase;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using static SharedLibraryCore.Utilities;
namespace IW4MAdmin.Application.IO
{
/// <summary>
/// provides capibility of reading log files over HTTP
/// </summary>
class GameLogReaderHttp : IGameLogReader
{
readonly IEventParser Parser;
readonly IGameLogServer Api;
readonly string logPath;
private string lastKey = "next";
public GameLogReaderHttp(Uri gameLogServerUri, string logPath, IEventParser parser)
{
this.logPath = logPath.ToBase64UrlSafeString(); ;
Parser = parser;
Api = RestClient.For<IGameLogServer>(gameLogServerUri);
}
public long Length => -1;
public int UpdateInterval => 500;
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
{
var events = new List<GameEvent>();
string b64Path = logPath;
var response = await Api.Log(b64Path, lastKey);
lastKey = response.NextKey;
if (!response.Success && string.IsNullOrEmpty(lastKey))
{
server.Logger.WriteError($"Could not get log server info of {logPath}/{b64Path} ({server.LogPath})");
return events;
}
else if (!string.IsNullOrWhiteSpace(response.Data))
{
// parse each line
foreach (string eventLine in response.Data
.Split(Environment.NewLine)
.Where(_line => _line.Length > 0))
{
try
{
var gameEvent = Parser.GenerateGameEvent(eventLine);
events.Add(gameEvent);
#if DEBUG == true
server.Logger.WriteDebug($"Parsed event with id {gameEvent.Id} from http");
#endif
}
catch (Exception e)
{
server.Logger.WriteError("Could not properly parse event line from http");
server.Logger.WriteDebug(e.Message);
server.Logger.WriteDebug(eventLine);
}
}
}
return events;
}
}
}

1197
Application/IW4MServer.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
using SharedLibraryCore;
using IW4MAdmin.Application.API.Master;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Globalization;
@ -9,25 +11,69 @@ namespace IW4MAdmin.Application.Localization
{
public class Configure
{
public static void Initialize()
public static ITranslationLookup Initialize(bool useLocalTranslation, string customLocale = null)
{
string currentLocal = CultureInfo.CurrentCulture.Name;
string localizationFile = $"Localization{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocal}.json";
string localizationContents;
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
if (File.Exists(localizationFile))
if (!useLocalTranslation)
{
localizationContents = File.ReadAllText(localizationFile);
try
{
var api = Endpoint.Get();
var localization = api.GetLocalization(currentLocale).Result;
Utilities.CurrentLocalization = localization;
return localization.LocalizationIndex;
}
catch (Exception)
{
// the online localization failed so will default to local files
}
}
else
// culture doesn't exist so we just want language
if (localizationFiles.Length == 0)
{
localizationFile = $"Localization{Path.DirectorySeparatorChar}IW4MAdmin.en-US.json";
localizationContents = File.ReadAllText(localizationFile);
localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale.Substring(0, 2)}*.json");
}
Utilities.CurrentLocalization = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents);
// language doesn't exist either so defaulting to english
if (localizationFiles.Length == 0)
{
localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), "*.en-US.json");
}
// this should never happen unless the localization folder is empty
if (localizationFiles.Length == 0)
{
throw new Exception("No localization files were found");
}
var localizationDict = new Dictionary<string, string>();
foreach (string filePath in localizationFiles)
{
var localizationContents = File.ReadAllText(filePath, Encoding.UTF8);
var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents);
foreach (var item in eachLocalizationFile.LocalizationIndex.Set)
{
if (!localizationDict.TryAdd(item.Key, item.Value))
{
Program.ServerManager.GetLogger(0).WriteError($"Could not add locale string {item.Key} to localization");
}
}
}
string localizationFile = $"{Path.Join(Utilities.OperatingDirectory, "Localization")}{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocale}-{currentLocale.ToUpper()}.json";
Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict)
{
LocalizationName = currentLocale,
};
return Utilities.CurrentLocalization.LocalizationIndex;
}
}
}

View File

@ -1,109 +0,0 @@
{
"LocalizationName": "en-US",
"LocalizationSet": {
"MANAGER_VERSION_FAIL": "Could not get latest IW4MAdmin version",
"MANAGER_VERSION_UPDATE": "has an update. Latest version is",
"MANAGER_VERSION_CURRENT": "Your version is",
"MANAGER_VERSION_SUCCESS": "IW4MAdmin is up to date",
"MANAGER_INIT_FAIL": "Fatal error during initialization",
"MANAGER_EXIT": "Press any key to exit...",
"SETUP_ENABLE_WEBFRONT": "Enable webfront",
"SETUP_ENABLE_MULTIOWN": "Enable multiple owners",
"SETUP_ENABLE_STEPPEDPRIV": "Enable stepped privilege hierarchy",
"SETUP_ENABLE_CUSTOMSAY": "Enable custom say name",
"SETUP_SAY_NAME": "Enter custom say name",
"SETUP_USE_CUSTOMENCODING": "Use custom encoding parser",
"SETUP_ENCODING_STRING": "Enter encoding string",
"SETUP_ENABLE_VPNS": "Enable client VPNs",
"SETUP_IPHUB_KEY": "Enter iphub.info api key",
"SETUP_DISPLAY_DISCORD": "Display discord link on webfront",
"SETUP_DISCORD_INVITE": "Enter discord invite link",
"SETUP_SERVER_USET6M": "Use T6M parser",
"SETUP_SERVER_IP": "Enter server IP Address",
"SETUP_SERVER_PORT": "Enter server port",
"SETUP_SERVER_RCON": "Enter server RCon password",
"SETUP_SERVER_SAVE": "Configuration saved, add another",
"SERVER_KICK_VPNS_NOTALLOWED": "VPNs are not allowed on this server",
"SERVER_KICK_TEXT": "You were kicked",
"SERVER_KICK_MINNAME": "Your name must contain at least 3 characters",
"SERVER_KICK_NAME_INUSE": "Your name is being used by someone else",
"SERVER_KICK_GENERICNAME": "Please change your name using /name",
"SERVER_KICK_CONTROLCHARS": "Your name cannot contain control characters",
"SERVER_TB_TEXT": "You're temporarily banned",
"SERVER_TB_REMAIN": "You are temporarily banned",
"SERVER_BAN_TEXT": "You're banned",
"SERVER_BAN_PREV": "Previously banned for",
"SERVER_BAN_APPEAL": "appeal at",
"SERVER_REPORT_COUNT": "There are ^5{0} ^7recent reports",
"SERVER_WARNLIMT_REACHED": "Too many warnings",
"SERVER_WARNING": "Warning",
"SERVER_WEBSITE_GENERIC": "this server's website",
"BROADCAST_ONLINE": "^5IW4MADMIN ^7is now ^2ONLINE",
"BROADCAST_OFFLINE": "IW4MAdmin is going offline",
"COMMAND_HELP_SYNTAX": "syntax:",
"COMMAND_HELP_OPTIONAL": "optional",
"COMMAND_UNKNOWN": "You entered an unknown command",
"COMMAND_NOACCESS": "You do not have access to that command",
"COMMAND_NOTAUTHORIZED": "You are not authorized to execute that command",
"COMMAND_MISSINGARGS": "Not enough arguments supplied",
"COMMAND_TARGET_MULTI": "Multiple players match that name",
"COMMAND_TARGET_NOTFOUND": "Unable to find specified player",
"PLUGIN_IMPORTER_NOTFOUND": "No plugins found to load",
"PLUGIN_IMPORTER_REGISTERCMD": "Registered command",
"COMMANDS_OWNER_SUCCESS": "Congratulations, you have claimed ownership of this server!",
"COMMANDS_OWNER_FAIL": "This server already has an owner",
"COMMANDS_WARN_FAIL": "You do not have the required privileges to warn",
"COMMANDS_WARNCLEAR_SUCCESS": "All warning cleared for",
"COMMANDS_KICK_SUCCESS": "has been kicked",
"COMMANDS_KICK_FAIL": "You do not have the required privileges to kick",
"COMMANDS_TEMPBAN_SUCCESS": "has been temporarily banned for",
"COMMANDS_TEMPBAN_FAIL": "You cannot temporarily ban",
"COMMANDS_BAN_SUCCESS": "has been permanently banned",
"COMMANDS_BAN_FAIL": "You cannot ban",
"COMMANDS_UNBAN_SUCCESS": "Successfully unbanned",
"COMMANDS_UNBAN_FAIL": "is not banned",
"COMMANDS_HELP_NOTFOUND": "Could not find that command",
"COMMANDS_HELP_MOREINFO": "Type !help <command name> to get command usage syntax",
"COMMANDS_FASTRESTART_UNMASKED": "fast restarted the map",
"COMMANDS_FASTRESTART_MASKED": "The map has been fast restarted",
"COMMANDS_MAPROTATE": "Map rotating in ^55 ^7seconds",
"COMMANDS_SETLEVEL_SELF": "You cannot change your own level",
"COMMANDS_SETLEVEL_OWNER": "There can only be 1 owner. Modify your settings if multiple owners are required",
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "This server does not allow you to promote",
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "You can only promote ^5{0} ^7to ^5{1} ^7or lower privilege",
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "Congratulations! You have been promoted to",
"COMMANDS_SETLEVEL_SUCCESS": "was successfully promoted",
"COMMANDS_SETLEVEL_FAIL": "Invalid group specified",
"COMMANDS_ADMINS_NONE": "No visible administrators online",
"COMMANDS_MAP_SUCCESS": "Changing to map",
"COMMANDS_MAP_UKN": "Attempting to change to unknown map",
"COMMANDS_FIND_MIN": "Please enter at least 3 characters",
"COMMANDS_FIND_EMPTY": "No players found",
"COMMANDS_RULES_NONE": "The server owner has not set any rules",
"COMMANDS_FLAG_SUCCESS": "You have flagged",
"COMMANDS_FLAG_UNFLAG": "You have unflagged",
"COMMANDS_FLAG_FAIL": "You cannot flag",
"COMMANDS_REPORT_FAIL_CAMP": "You cannot report an player for camping",
"COMMANDS_REPORT_FAIL_DUPLICATE": "You have already reported this player",
"COMMANDS_REPORT_FAIL_SELF": "You cannot report yourself",
"COMMANDS_REPORT_FAIL": "You cannot report",
"COMMANDS_REPORT_SUCCESS": "Thank you for your report, an administrator has been notified",
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Reports successfully cleared",
"COMMANDS_REPORTS_NONE": "No players reported yet",
"COMMANDS_MASK_ON": "You are now masked",
"COMMANDS_MASK_OFF": "You are now unmasked",
"COMMANDS_BANINFO_NONE": "No active ban was found for that player",
"COMMANDS_BANINO_SUCCESS": "was banned by ^5{0} ^7for:",
"COMMANDS_ALIAS_ALIASES": "Aliases",
"COMMANDS_ALIAS_IPS": "IPs",
"COMMANDS_RCON_SUCCESS": "Successfully sent RCon command",
"COMMANDS_PLUGINS_LOADED": "Loaded Plugins",
"COMMANDS_IP_SUCCESS": "Your external IP is",
"COMMANDS_PRUNE_FAIL": "Invalid number of inactive days",
"COMMANDS_PRUNE_SUCCESS": "inactive privileged users were pruned",
"COMMANDS_PASSWORD_FAIL": "Your password must be at least 5 characters long",
"COMMANDS_PASSWORD_SUCCESS": "Your password has been set successfully",
"COMMANDS_PING_TARGET": "ping is",
"COMMANDS_PING_SELF": "Your ping is"
}
}

View File

@ -1,79 +0,0 @@
using System;
using System.IO;
namespace IW4MAdmin.Application
{
class Logger : SharedLibraryCore.Interfaces.ILogger
{
enum LogType
{
Verbose,
Info,
Debug,
Warning,
Error,
Assert
}
string FileName;
object ThreadLock;
public Logger(string fn)
{
FileName = fn;
ThreadLock = new object();
if (File.Exists(fn))
File.Delete(fn);
}
void Write(string msg, LogType type)
{
string LogLine = $"[{DateTime.Now.ToString("HH:mm:ss")}] - {type}: {msg}";
lock (ThreadLock)
{
#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);
#else
if (type == LogType.Error || type == LogType.Verbose)
Console.WriteLine(LogLine);
//if (type != LogType.Debug)
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
#endif
}
}
public void WriteVerbose(string msg)
{
Write(msg, LogType.Verbose);
}
public void WriteDebug(string msg)
{
Write(msg, LogType.Debug);
}
public void WriteError(string msg)
{
Write(msg, LogType.Error);
}
public void WriteInfo(string msg)
{
Write(msg, LogType.Info);
}
public void WriteWarning(string msg)
{
Write(msg, LogType.Warning);
}
public void WriteAssert(bool condition, string msg)
{
if (!condition)
Write(msg, LogType.Assert);
}
}
}

View File

@ -1,154 +1,298 @@
using System;
using System.Threading.Tasks;
using System.IO;
using System.Reflection;
using IW4MAdmin.Application.Helpers;
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Database;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace IW4MAdmin.Application
{
public class Program
{
static public double Version { get; private set; }
static public ApplicationManager ServerManager = ApplicationManager.GetInstance();
public static string OperatingDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
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()
{
AppDomain.CurrentDomain.SetData("DataDirectory", OperatingDirectory);
System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.BelowNormal;
Localization.Configure.Initialize();
var loc = Utilities.CurrentLocalization.LocalizationSet;
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
Version = Assembly.GetExecutingAssembly().GetName().Version.Major + Assembly.GetExecutingAssembly().GetName().Version.Minor / 10.0f;
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray;
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
Console.WriteLine("=====================================================");
Console.WriteLine(" IW4M ADMIN");
Console.WriteLine(" IW4MAdmin");
Console.WriteLine(" by RaidMax ");
Console.WriteLine($" Version {Version.ToString("0.0")}");
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("=====================================================");
await LaunchAsync();
}
/// <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()
{
restart:
ITranslationLookup translationLookup = null;
try
{
using (var db = new DatabaseContext())
new ContextSeed(db).Seed().Wait();
var services = ConfigureServices();
serviceProvider = services.BuildServiceProvider();
ServerManager = (ApplicationManager)serviceProvider.GetRequiredService<IManager>();
translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>();
CheckDirectories();
// do any needed housekeeping file/folder migrations
ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories();
ServerManager = ApplicationManager.GetInstance();
var api = API.Master.Endpoint.Get();
var version = new API.Master.VersionInfo()
{
CurrentVersionStable = 99.99f
};
try
{
version = api.GetVersion().Result;
}
catch (Exception e)
{
ServerManager.Logger.WriteWarning(loc["MANAGER_VERSION_FAIL"]);
while (e.InnerException != null)
{
e = e.InnerException;
}
ServerManager.Logger.WriteDebug(e.Message);
}
if (version.CurrentVersionStable == 99.99f)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
Console.ForegroundColor = ConsoleColor.White;
}
#if !PRERELEASE
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"]} [v{Version.ToString("0.0")}]");
Console.ForegroundColor = ConsoleColor.White;
}
#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"]} [v{Version.ToString("0.0")}-pr]");
Console.ForegroundColor = ConsoleColor.White;
}
#endif
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
Console.ForegroundColor = ConsoleColor.White;
}
ServerManager.Init().Wait();
var consoleTask = Task.Run(() =>
{
String userInput;
Player Origin = ServerManager.GetClientService().Get(1).Result.AsPlayer();
do
{
userInput = Console.ReadLine();
if (userInput?.ToLower() == "quit")
ServerManager.Stop();
if (ServerManager.Servers.Count == 0)
{
Console.WriteLine("No servers are currently being monitored");
continue;
}
Origin.CurrentServer = ServerManager.Servers[0];
GameEvent E = new GameEvent(GameEvent.EventType.Say, userInput, Origin, null, ServerManager.Servers[0]);
ServerManager.Servers[0].ExecuteEvent(E);
Console.Write('>');
} while (ServerManager.Running);
});
if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront)
{
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
}
ServerManager.Start();
ServerManager.Logger.WriteVerbose("Shutdown complete");
ServerManager.Logger.WriteInfo(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_VERSION"].FormatExt(Version));
await CheckVersion(translationLookup);
await ServerManager.Init();
}
catch (Exception e)
{
Console.WriteLine(loc["MANAGER_INIT_FAIL"]);
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
string exitMessage = translationLookup == null ? "Press any key to exit..." : translationLookup["MANAGER_EXIT"];
Console.WriteLine(failMessage);
while (e.InnerException != null)
{
e = e.InnerException;
}
Console.WriteLine($"Exception: {e.Message}");
Console.WriteLine(loc["MANAGER_EXIT"]);
if (e is ConfigurationException configException)
{
Console.WriteLine(translationLookup[configException.Message].FormatExt(configException.ConfigurationFileName));
foreach (string error in configException.Errors)
{
Console.WriteLine(error);
}
}
else
{
Console.WriteLine(e.Message);
}
Console.WriteLine(exitMessage);
Console.ReadKey();
return;
}
try
{
ApplicationTask = RunApplicationTasksAsync();
await ApplicationTask;
}
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 = _fallbackVersion
};
try
{
version = await api.GetVersion(1);
}
catch (Exception e)
{
ServerManager.Logger.WriteWarning(loc["MANAGER_VERSION_FAIL"]);
while (e.InnerException != null)
{
e = e.InnerException;
}
ServerManager.Logger.WriteDebug(e.Message);
}
if (version.CurrentVersionStable == _fallbackVersion)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
#if !PRERELEASE
else if (version.CurrentVersionStable > Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
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()}-pr]");
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString()}-pr]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#endif
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
}
static void CheckDirectories()
/// <summary>
/// reads input from the console and executes entered commands on the default server
/// </summary>
/// <returns></returns>
private static async Task ReadConsoleInput()
{
string curDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
string lastCommand;
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
if (!Directory.Exists($"{curDirectory}Plugins"))
Directory.CreateDirectory($"{curDirectory}Plugins");
try
{
while (!ServerManager.CancellationToken.IsCancellationRequested)
{
lastCommand = Console.ReadLine();
if (lastCommand?.Length > 0)
{
if (lastCommand?.Length > 0)
{
GameEvent E = new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = lastCommand,
Origin = Origin,
Owner = ServerManager.Servers[0]
};
ServerManager.GetEventHandler().AddEvent(E);
await E.WaitAsync(Utilities.DefaultCommandTimeout, ServerManager.CancellationToken);
Console.Write('>');
}
}
}
}
catch (OperationCanceledException)
{ }
}
/// <summary>
/// Configures the dependency injection services
/// </summary>
private static IServiceCollection ConfigureServices()
{
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())
.AddSingleton<ILogger>(_serviceProvider => new Logger("IW4MAdmin-Manager"))
.AddSingleton<IPluginImporter, PluginImporter>()
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
.AddTransient(_serviceProvider =>
{
var importer = _serviceProvider.GetRequiredService<IPluginImporter>();
var config = _serviceProvider.GetRequiredService<CommandConfiguration>();
var layout = _serviceProvider.GetRequiredService<ITranslationLookup>();
// todo: this is disgusting, but I need it until I can figure out a way to dynamically load the plugins without creating an instance.
return importer.CommandTypes.
Union(typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
.Where(_command => _command.BaseType == typeof(Command)))
.Select(_cmdType => Activator.CreateInstance(_cmdType, config, layout) as IManagerCommand);
})
.AddSingleton(_serviceProvider =>
{
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>();
return serviceCollection;
}
}
}

View File

@ -1,415 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.IO;
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Services;
using IW4MAdmin.Application.API;
using Microsoft.Extensions.Configuration;
using WebfrontCore;
using SharedLibraryCore.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Text;
namespace IW4MAdmin.Application
{
public class ApplicationManager : IManager
{
private List<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
public Dictionary<int, Player> PrivilegedClients { get; set; }
public ILogger Logger { get; private set; }
public bool Running { get; private set; }
public EventHandler<GameEvent> ServerEventOccurred { get; private set; }
public DateTime StartTime { get; private set; }
static ApplicationManager Instance;
List<AsyncStatus> TaskStatuses;
List<Command> Commands;
List<MessageToken> MessageTokens;
ClientService ClientSvc;
AliasService AliasSvc;
PenaltyService PenaltySvc;
BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
EventApi Api;
#if FTP_LOG
const int UPDATE_FREQUENCY = 700;
#else
const int UPDATE_FREQUENCY = 450;
#endif
private ApplicationManager()
{
Logger = new Logger($@"{Utilities.OperatingDirectory}IW4MAdmin.log");
_servers = new List<Server>();
Commands = new List<Command>();
TaskStatuses = new List<AsyncStatus>();
MessageTokens = new List<MessageToken>();
ClientSvc = new ClientService();
AliasSvc = new AliasService();
PenaltySvc = new PenaltyService();
PrivilegedClients = new Dictionary<int, Player>();
Api = new EventApi();
ServerEventOccurred += Api.OnServerEvent;
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
StartTime = DateTime.UtcNow;
}
private void OnCancelKey(object sender, ConsoleCancelEventArgs args)
{
Stop();
}
public IList<Server> GetServers()
{
return Servers;
}
public IList<Command> GetCommands()
{
return Commands;
}
public static ApplicationManager GetInstance()
{
return Instance ?? (Instance = new ApplicationManager());
}
public async Task Init()
{
#region DATABASE
var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted))
.Select(c => new
{
c.Password,
c.PasswordSalt,
c.ClientId,
c.Level,
c.Name
});
foreach (var a in ipList)
{
try
{
PrivilegedClients.Add(a.ClientId, new Player()
{
Name = a.Name,
ClientId = a.ClientId,
Level = a.Level,
PasswordSalt = a.PasswordSalt,
Password = a.Password
});
}
catch (ArgumentException)
{
continue;
}
}
#endregion
#region CONFIG
var config = ConfigHandler.Configuration();
// copy over default config if it doesn't exist
if (config == null)
{
var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration();
ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
var newConfig = ConfigHandler.Configuration();
newConfig.AutoMessagePeriod = defaultConfig.AutoMessagePeriod;
newConfig.AutoMessages = defaultConfig.AutoMessages;
newConfig.GlobalRules = defaultConfig.GlobalRules;
newConfig.Maps = defaultConfig.Maps;
if (newConfig.Servers == null)
{
ConfigHandler.Set(newConfig);
newConfig.Servers = ConfigurationGenerator.GenerateServerConfig(new List<ServerConfiguration>());
config = newConfig;
await ConfigHandler.Save();
}
}
else if (config != null)
{
if (string.IsNullOrEmpty(config.Id))
{
config.Id = Guid.NewGuid().ToString();
await ConfigHandler.Save();
}
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
{
config.WebfrontBindUrl = "http://127.0.0.1:1624";
await ConfigHandler.Save();
}
}
else if (config.Servers.Count == 0)
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252");
#endregion
#region PLUGINS
SharedLibraryCore.Plugins.PluginImporter.Load(this);
foreach (var Plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
{
try
{
await Plugin.OnLoadAsync(this);
}
catch (Exception e)
{
Logger.WriteError($"An error occured loading plugin {Plugin.Name}");
Logger.WriteDebug($"Exception: {e.Message}");
Logger.WriteDebug($"Stack Trace: {e.StackTrace}");
}
}
#endregion
#region COMMANDS
if (ClientSvc.GetOwners().Result.Count == 0)
Commands.Add(new COwner());
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 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());
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
Commands.Add(C);
#endregion
#region INIT
async Task Init(ServerConfiguration Conf)
{
try
{
var ServerInstance = new IW4MServer(this, Conf);
await ServerInstance.Initialize();
lock (_servers)
{
_servers.Add(ServerInstance);
}
Logger.WriteVerbose($"Now monitoring {ServerInstance.Hostname}");
// this way we can keep track of execution time and see if problems arise.
var Status = new AsyncStatus(ServerInstance, UPDATE_FREQUENCY);
lock (TaskStatuses)
{
TaskStatuses.Add(Status);
}
}
catch (ServerException e)
{
Logger.WriteError($"Not monitoring server {Conf.IPAddress}:{Conf.Port} due to uncorrectable errors");
if (e.GetType() == typeof(DvarException))
Logger.WriteDebug($"Could not get the dvar value for {(e as DvarException).Data["dvar_name"]} (ensure the server has a map loaded)");
else if (e.GetType() == typeof(NetworkException))
{
Logger.WriteDebug(e.Message);
}
// throw the exception to the main method to stop before instantly exiting
throw e;
}
}
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
#endregion
Running = true;
}
private void HeartBeatThread()
{
bool successfulConnection = false;
restartConnection:
while (!successfulConnection)
{
try
{
API.Master.Heartbeat.Send(this, true).Wait();
successfulConnection = true;
}
catch (Exception e)
{
successfulConnection = false;
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
}
Thread.Sleep(30000);
}
while (Running)
{
Logger.WriteDebug("Sending heartbeat...");
try
{
API.Master.Heartbeat.Send(this).Wait();
}
catch (System.Net.Http.HttpRequestException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
}
catch (AggregateException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(RestEase.ApiException));
foreach (var ex in exceptions)
{
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
successfulConnection = false;
goto restartConnection;
}
}
}
catch (RestEase.ApiException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
successfulConnection = false;
goto restartConnection;
}
}
Thread.Sleep(30000);
}
}
public void Start()
{
Task.Run(() => HeartBeatThread());
while (Running || TaskStatuses.Count > 0)
{
for (int i = 0; i < TaskStatuses.Count; i++)
{
var Status = TaskStatuses[i];
// task is read to be rerun
if (Status.RequestedTask == null || Status.RequestedTask.Status == TaskStatus.RanToCompletion)
{
// remove the task when we want to quit and last run has finished
if (!Running)
{
TaskStatuses.RemoveAt(i);
continue;
}
// normal operation
else
{
Status.Update(new Task<bool>(() => { return (Status.Dependant as Server).ProcessUpdatesAsync(Status.GetToken()).Result; }));
if (Status.RunAverage > 1000 + UPDATE_FREQUENCY && !(Status.Dependant as Server).Throttled)
Logger.WriteWarning($"Update task average execution is longer than desired for {(Status.Dependant as Server)} [{Status.RunAverage}ms]");
}
}
if (Status.RequestedTask.Status == TaskStatus.Faulted)
{
Logger.WriteWarning($"Update task for {(Status.Dependant as Server)} faulted, restarting");
Status.Abort();
}
}
Thread.Sleep(UPDATE_FREQUENCY);
}
#if !DEBUG
foreach (var S in Servers)
S.Broadcast(Utilities.CurrentLocalization.LocalizationSet["BROADCAST_OFFLINE"]).Wait();
#endif
_servers.Clear();
}
public void Stop()
{
Running = false;
}
public ILogger GetLogger()
{
return Logger;
}
public IList<MessageToken> GetMessageTokens()
{
return MessageTokens;
}
public IList<Player> GetActiveClients()
{
var ActiveClients = new List<Player>();
foreach (var server in _servers)
ActiveClients.AddRange(server.Players.Where(p => p != null));
return ActiveClients;
}
public ClientService GetClientService() => ClientSvc;
public AliasService GetAliasService() => AliasSvc;
public PenaltyService GetPenaltyService() => PenaltySvc;
public IConfigurationHandler<ApplicationConfiguration> GetApplicationSettings() => ConfigHandler;
public IDictionary<int, Player> GetPrivilegedClients() => PrivilegedClients;
public IEventApi GetEventApi() => Api;
public bool ShutdownRequested() => !Running;
}
}

View File

@ -0,0 +1,90 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace IW4MAdmin.Application.Migration
{
/// <summary>
/// helps facilitate the migration of configs from one version and or location
/// to another
/// </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"));
}
}
/// <summary>
/// moves existing configs from the root folder into a configs folder
/// </summary>
public static void MoveConfigFolder10518(ILogger log)
{
string currentDirectory = Utilities.OperatingDirectory;
// we don't want to do this for migrations or tests where the
// property isn't initialized or it's wrong
if (currentDirectory != null)
{
string configDirectory = Path.Join(currentDirectory, "Configuration");
if (!Directory.Exists(configDirectory))
{
log?.WriteDebug($"Creating directory for configs {configDirectory}");
Directory.CreateDirectory(configDirectory);
}
var configurationFiles = Directory.EnumerateFiles(currentDirectory, "*.json")
.Select(f => f.Split(Path.DirectorySeparatorChar).Last())
.Where(f => f.Count(c => c == '.') == 1);
foreach (var configFile in configurationFiles)
{
log?.WriteDebug($"Moving config file {configFile}");
string destinationPath = Path.Join("Configuration", configFile);
if (!File.Exists(destinationPath))
{
File.Move(configFile, destinationPath);
}
}
if (!File.Exists(Path.Join("Database", "Database.db")) &&
File.Exists("Database.db"))
{
log?.WriteDebug("Moving database file");
File.Move("Database.db", Path.Join("Database", "Database.db"));
}
}
}
public static void ModifyLogPath020919(SharedLibraryCore.Configuration.ServerConfiguration config)
{
if (config.ManualLogPath.IsRemoteLog())
{
config.GameLogServerUrl = new Uri(config.ManualLogPath);
config.ManualLogPath = null;
}
}
}
}

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

132
Application/Misc/Logger.cs Normal file
View File

@ -0,0 +1,132 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.IO;
using System.Threading;
namespace IW4MAdmin.Application
{
class Logger : ILogger
{
enum LogType
{
Verbose,
Info,
Debug,
Warning,
Error,
Assert
}
readonly string FileName;
readonly ReaderWriterLockSlim WritingLock;
static readonly short MAX_LOG_FILES = 10;
public Logger(string fn)
{
FileName = Path.Join(Utilities.OperatingDirectory, "Log", $"{fn}.log");
WritingLock = new ReaderWriterLockSlim();
RotateLogs();
}
~Logger()
{
WritingLock.Dispose();
}
/// <summary>
/// rotates logs when log is initialized
/// </summary>
private void RotateLogs()
{
string maxLog = FileName + MAX_LOG_FILES;
if (File.Exists(maxLog))
{
File.Delete(maxLog);
}
for (int i = MAX_LOG_FILES - 1; i >= 0; i--)
{
string logToMove = i == 0 ? FileName : FileName + i;
string movedLogName = FileName + (i + 1);
if (File.Exists(logToMove))
{
File.Move(logToMove, movedLogName);
}
}
}
void Write(string msg, LogType type)
{
WritingLock.EnterWriteLock();
string stringType = type.ToString();
msg = msg.StripColors();
try
{
stringType = Utilities.CurrentLocalization.LocalizationIndex[$"GLOBAL_{type.ToString().ToUpper()}"];
}
catch (Exception) { }
string LogLine = $"[{DateTime.Now.ToString("MM.dd.yyy HH:mm:ss.fff")}] - {stringType}: {msg}";
try
{
#if DEBUG
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
Console.WriteLine(LogLine);
//File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
//Debug.WriteLine(msg);
#else
if (type == LogType.Error || type == LogType.Verbose)
{
Console.WriteLine(LogLine);
}
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
#endif
}
catch (Exception ex)
{
Console.WriteLine("Well.. It looks like your machine can't event write to the log file. That's something else...");
Console.WriteLine(ex.GetExceptionInfo());
}
WritingLock.ExitWriteLock();
}
public void WriteVerbose(string msg)
{
Write(msg, LogType.Verbose);
}
public void WriteDebug(string msg)
{
Write(msg, LogType.Debug);
}
public void WriteError(string msg)
{
Write(msg, LogType.Error);
}
public void WriteInfo(string msg)
{
Write(msg, LogType.Info);
}
public void WriteWarning(string msg)
{
Write(msg, LogType.Warning);
}
public void WriteAssert(bool condition, string msg)
{
if (!condition)
Write(msg, LogType.Assert);
}
}
}

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,26 @@
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application
{
/// <summary>
/// implementatin of IPageList that supports basic
/// pages title and page location for webfront
/// </summary>
class PageList : IPageList
{
/// <summary>
/// Pages dictionary
/// Key = page name
/// Value = page location (url)
/// </summary>
public IDictionary<string, string> Pages { get; set; }
public PageList()
{
Pages = new Dictionary<string, string>();
}
}
}

View File

@ -0,0 +1,115 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using SharedLibraryCore.Interfaces;
using System.Linq;
using SharedLibraryCore;
namespace IW4MAdmin.Application.Helpers
{
public class PluginImporter : IPluginImporter
{
public IList<Type> CommandTypes { get; private set; } = new List<Type>();
public IList<IPlugin> ActivePlugins { get; private set; } = new List<IPlugin>();
public IList<Assembly> PluginAssemblies { get; private set; } = new List<Assembly>();
public IList<Assembly> Assemblies { get; private set; } = new List<Assembly>();
private readonly ILogger _logger;
private readonly ITranslationLookup _translationLookup;
public PluginImporter(ILogger logger, ITranslationLookup translationLookup)
{
_logger = logger;
_translationLookup = translationLookup;
Load();
}
/// <summary>
/// Loads all the assembly and javascript plugins
/// </summary>
private void Load()
{
string pluginDir = $"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}";
string[] dllFileNames = null;
string[] scriptFileNames = null;
if (Directory.Exists(pluginDir))
{
dllFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.dll");
scriptFileNames = Directory.GetFiles($"{Utilities.OperatingDirectory}Plugins{Path.DirectorySeparatorChar}", "*.js");
}
else
{
dllFileNames = new string[0];
scriptFileNames = new string[0];
}
if (dllFileNames.Length == 0 &&
scriptFileNames.Length == 0)
{
_logger.WriteDebug(_translationLookup["PLUGIN_IMPORTER_NOTFOUND"]);
return;
}
// load up the script plugins
foreach (string fileName in scriptFileNames)
{
var plugin = new ScriptPlugin(fileName);
_logger.WriteDebug($"Loaded script plugin \"{ plugin.Name }\" [{plugin.Version}]");
ActivePlugins.Add(plugin);
}
ICollection<Assembly> assemblies = new List<Assembly>(dllFileNames.Length);
foreach (string dllFile in dllFileNames)
{
assemblies.Add(Assembly.LoadFrom(dllFile));
}
int LoadedCommands = 0;
foreach (Assembly Plugin in assemblies)
{
if (Plugin != null)
{
Assemblies.Add(Plugin);
Type[] types = Plugin.GetTypes();
foreach (Type assemblyType in types)
{
if (assemblyType.IsClass && assemblyType.BaseType == typeof(Command))
{
CommandTypes.Add(assemblyType);
_logger.WriteDebug($"{_translationLookup["PLUGIN_IMPORTER_REGISTERCMD"]} \"{assemblyType.Name}\"");
LoadedCommands++;
continue;
}
try
{
if (assemblyType.GetInterface("IPlugin", false) == null)
continue;
var notifyObject = Activator.CreateInstance(assemblyType);
IPlugin newNotify = (IPlugin)notifyObject;
if (ActivePlugins.FirstOrDefault(x => x.Name == newNotify.Name) == null)
{
ActivePlugins.Add(newNotify);
PluginAssemblies.Add(Plugin);
_logger.WriteDebug($"Loaded plugin \"{newNotify.Name}\" [{newNotify.Version}]");
}
}
catch (Exception e)
{
_logger.WriteWarning(_translationLookup["PLUGIN_IMPORTER_ERROR"].FormatExt(Plugin.Location));
_logger.WriteDebug(e.GetExceptionInfo());
}
}
}
}
_logger.WriteInfo($"Loaded {ActivePlugins.Count} plugins and registered {LoadedCommands} plugin commands.");
}
}
}

View File

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

View File

@ -1,37 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Application.Misc
{
public class VPNCheck
{
public static async Task<bool> UsingVPN(string ip, string apiKey)
{
#if DEBUG
return await Task.FromResult(false);
#else
try
{
using (var RequestClient = new System.Net.Http.HttpClient())
{
RequestClient.DefaultRequestHeaders.Add("X-Key", apiKey);
string response = await RequestClient.GetStringAsync($"http://v2.api.iphub.info/ip/{ip}");
var responseJson = JsonConvert.DeserializeObject<JObject>(response);
int blockType = Convert.ToInt32(responseJson["block"]);
return blockType == 1;
}
}
catch (Exception)
{
return false;
}
#endif
}
}
}

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Release</Configuration>
<TargetFramework>netcoreapp2.0</TargetFramework>
<PublishDir>C:\Projects\IW4M-Admin\Publish\Windows</PublishDir>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,219 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.RconParsers
{
#if DEBUG
public class BaseRConParser : IRConParser
#else
class BaseRConParser : IRConParser
#endif
{
public BaseRConParser()
{
Configuration = new DynamicRConParserConfiguration()
{
CommandPrefixes = new CommandPrefix()
{
Tell = "tell {0} {1}",
Say = "say {0}",
Kick = "clientkick {0} \"{1}\"",
Ban = "clientkick {0} \"{1}\"",
TempBan = "tempbanclient {0} \"{1}\"",
RConCommand = "ÿÿÿÿrcon {0} {1}",
RConGetDvar = "ÿÿÿÿrcon {0} {1}",
RConSetDvar = "ÿÿÿÿrcon {0} set {1}",
RConGetStatus = "ÿÿÿÿgetstatus",
RConGetInfo = "ÿÿÿÿgetinfo",
RConResponse = "ÿÿÿÿprint",
},
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|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);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConNetworkId, 4);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7);
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n(?:latched: \"(.+)?\"\n)? *(.+)$";
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5);
Configuration.MapStatus.Pattern = @"map: (([a-z]|_|\d)+)";
Configuration.MapStatus.AddMapping(ParserRegex.GroupType.RConStatusMap, 1);
}
public IRConParserConfiguration Configuration { get; set; }
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)
{
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)
{
string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName);
string response = string.Join('\n', lineSplit.Skip(1));
var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (!lineSplit[0].Contains(Configuration.CommandPrefixes.RConResponse) ||
response.Contains("Unknown command") ||
!match.Success)
{
throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName));
}
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>()
{
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(Connection 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(Connection connection, string dvarName, object dvarValue)
{
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, $"{dvarName} {dvarValue}")).Length > 0;
}
private List<EFClient> ClientsFromStatus(string[] Status)
{
List<EFClient> StatusPlayers = new List<EFClient>();
if (Status.Length < 4)
{
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNEXPECTED_STATUS"]);
}
int validMatches = 0;
foreach (string statusLine in Status)
{
string responseLine = statusLine.Trim();
var regex = Regex.Match(responseLine, Configuration.Status.Pattern, RegexOptions.IgnoreCase);
if (regex.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);
int ping = 999;
// their state can be CNCT, ZMBI etc
if (regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value.Length <= 3)
{
ping = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value);
}
long networkId;
try
{
networkId = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].Value.ConvertGuidToLong(Configuration.GuidNumberStyle);
}
catch (FormatException)
{
continue;
}
string name = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].Value.Trim();
int? ip = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Value.Split(':')[0].ConvertToIP();
var client = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = name,
IPAddress = ip
},
NetworkId = networkId,
ClientNumber = clientNumber,
Ping = ping,
Score = score,
State = EFClient.ClientState.Connecting
};
#if DEBUG
if (client.NetworkId < 1000 && client.NetworkId > 0)
{
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)
{
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_ROTATING_MAP"]);
}
return StatusPlayers;
}
}
}

View File

@ -0,0 +1,10 @@
namespace IW4MAdmin.Application.RconParsers
{
/// <summary>
/// empty implementation of the IW4RConParser
/// allows script plugins to generate dynamic RCon parsers
/// </summary>
sealed internal class DynamicRConParser : BaseRConParser
{
}
}

View File

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

View File

@ -1,115 +0,0 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using SharedLibraryCore;
using SharedLibraryCore.RCon;
using SharedLibraryCore.Exceptions;
namespace Application.RconParsers
{
class IW4RConParser : IRConParser
{
private static CommandPrefix Prefixes = new CommandPrefix()
{
Tell = "tellraw {0} {1}",
Say = "sayraw {0}",
Kick = "clientkick {0} \"{1}\"",
Ban = "clientkick {0} \"{1}\"",
TempBan = "tempbanclient {0} \"{1}\""
};
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
{
return (await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command)).Skip(1).ToArray();
}
public async Task<Dvar<T>> GetDvarAsync<T>(Connection connection, string dvarName)
{
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.DVAR, dvarName);
if (LineSplit.Length < 3)
{
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
e.Data["dvar_name"] = dvarName;
throw e;
}
string[] ValueSplit = LineSplit[1].Split(new char[] { '"' }, StringSplitOptions.RemoveEmptyEntries);
if (ValueSplit.Length < 5)
{
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
e.Data["dvar_name"] = dvarName;
throw e;
}
string DvarName = Regex.Replace(ValueSplit[0], @"\^[0-9]", "");
string DvarCurrentValue = Regex.Replace(ValueSplit[2], @"\^[0-9]", "");
string DvarDefaultValue = Regex.Replace(ValueSplit[4], @"\^[0-9]", "");
return new Dvar<T>(DvarName)
{
Value = (T)Convert.ChangeType(DvarCurrentValue, typeof(T))
};
}
public async Task<List<Player>> GetStatusAsync(Connection connection)
{
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status");
return ClientsFromStatus(response);
}
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
{
return (await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"set {dvarName} {dvarValue}")).Length > 0;
}
public CommandPrefix GetCommandPrefixes() => Prefixes;
private List<Player> ClientsFromStatus(string[] Status)
{
List<Player> StatusPlayers = new List<Player>();
if (Status.Length < 4)
throw new ServerException("Unexpected status response received");
foreach (String S in Status)
{
String responseLine = S.Trim();
if (Regex.Matches(responseLine, @" *^\d+", RegexOptions.IgnoreCase).Count > 0)
{
String[] playerInfo = responseLine.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
int cID = -1;
int Ping = -1;
Int32.TryParse(playerInfo[2], out Ping);
String cName = Encoding.UTF8.GetString(Encoding.Convert(Utilities.EncodingType, Encoding.UTF8, Utilities.EncodingType.GetBytes(responseLine.Substring(46, 18).StripColors().Trim())));
long npID = Regex.Match(responseLine, @"([a-z]|[0-9]){16}", RegexOptions.IgnoreCase).Value.ConvertLong();
int.TryParse(playerInfo[0], out cID);
var regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}");
int cIP = regex.Value.Split(':')[0].ConvertToIP();
regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+");
int score = Int32.Parse(regex.Value.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)[1]);
Player P = new Player()
{
Name = cName,
NetworkId = npID,
ClientNumber = cID,
IPAddress = cIP,
Ping = Ping,
Score = score,
IsBot = npID == -1
};
StatusPlayers.Add(P);
}
}
return StatusPlayers;
}
}
}

View File

@ -1,198 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using SharedLibraryCore.RCon;
using SharedLibraryCore.Exceptions;
using System.Text;
using System.Linq;
using System.Net.Http;
namespace Application.RconParsers
{
public class T6MRConParser : IRConParser
{
class T6MResponse
{
public class SInfo
{
public short Com_maxclients { get; set; }
public string Game { get; set; }
public string Gametype { get; set; }
public string Mapname { get; set; }
public short NumBots { get; set; }
public short NumClients { get; set; }
public short Round { get; set; }
public string Sv_hostname { get; set; }
}
public class PInfo
{
public short Assists { get; set; }
public string Clan { get; set; }
public short Deaths { get; set; }
public short Downs { get; set; }
public short Headshots { get; set; }
public short Id { get; set; }
public bool IsBot { get; set; }
public short Kills { get; set; }
public string Name { get; set; }
public short Ping { get; set; }
public short Revives { get; set; }
public int Score { get; set; }
public long Xuid { get; set; }
public string Ip { get; set; }
}
public SInfo Info { get; set; }
public PInfo[] Players { get; set; }
}
private static CommandPrefix Prefixes = new CommandPrefix()
{
Tell = "tell {0} {1}",
Say = "say {0}",
Kick = "clientKick {0}",
Ban = "clientKick {0}",
TempBan = "clientKick {0}"
};
public CommandPrefix GetCommandPrefixes() => Prefixes;
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
{
await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, false);
return new string[] { "Command Executed" };
}
public async Task<Dvar<T>> GetDvarAsync<T>(Connection connection, string dvarName)
{
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"get {dvarName}");
if (LineSplit.Length < 2)
{
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
e.Data["dvar_name"] = dvarName;
throw e;
}
string[] ValueSplit = LineSplit[1].Split(new char[] { '"' });
if (ValueSplit.Length == 0)
{
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
e.Data["dvar_name"] = dvarName;
throw e;
}
string DvarName = dvarName;
string DvarCurrentValue = Regex.Replace(ValueSplit[1], @"\^[0-9]", "");
return new Dvar<T>(DvarName)
{
Value = (T)Convert.ChangeType(DvarCurrentValue, typeof(T))
};
}
public async Task<List<Player>> GetStatusAsync(Connection connection)
{
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status");
return ClientsFromStatus(response);
//return ClientsFromResponse(connection);
}
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
{
// T6M doesn't respond with anything when a value is set, so we can only hope for the best :c
await connection.SendQueryAsync(StaticHelpers.QueryType.DVAR, $"set {dvarName} {dvarValue}", false);
return true;
}
private async Task<List<Player>> ClientsFromResponse(Connection conn)
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri($"http://{conn.Endpoint.Address}:{conn.Endpoint.Port}/");
try
{
var parameters = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("rcon_password", conn.RConPassword)
});
var serverResponse = await client.PostAsync("/info", parameters);
var serverResponseObject = Newtonsoft.Json.JsonConvert.DeserializeObject<T6MResponse>(await serverResponse.Content.ReadAsStringAsync());
return serverResponseObject.Players.Select(p => new Player()
{
Name = p.Name,
NetworkId = p.Xuid,
ClientNumber = p.Id,
IPAddress = p.Ip.Split(':')[0].ConvertToIP(),
Ping = p.Ping,
Score = p.Score,
IsBot = p.IsBot,
}).ToList();
}
catch (HttpRequestException e)
{
throw new NetworkException(e.Message);
}
}
}
private List<Player> ClientsFromStatus(string[] status)
{
List<Player> StatusPlayers = new List<Player>();
foreach (string statusLine in status)
{
String responseLine = statusLine;
if (Regex.Matches(responseLine, @"^ *\d+", RegexOptions.IgnoreCase).Count > 0) // its a client line!
{
String[] playerInfo = responseLine.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
int clientId = -1;
int Ping = -1;
Int32.TryParse(playerInfo[3], out Ping);
var regex = Regex.Match(responseLine, @"\^7.*\ +0 ");
string name = Encoding.UTF8.GetString(Encoding.Convert(Utilities.EncodingType, Encoding.UTF8, Utilities.EncodingType.GetBytes(regex.Value.Substring(0, regex.Value.Length - 2).StripColors().Trim())));
long networkId = playerInfo[4].ConvertLong();
int.TryParse(playerInfo[0], out clientId);
regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}");
#if DEBUG
Ping = 1;
#endif
int ipAddress = regex.Value.Split(':')[0].ConvertToIP();
regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+");
int score = 0;
// todo: fix this when T6M score is valid ;)
//int score = Int32.Parse(playerInfo[1]);
StatusPlayers.Add(new Player()
{
Name = name,
NetworkId = networkId,
ClientNumber = clientId,
IPAddress = ipAddress,
Ping = Ping,
Score = score,
IsBot = networkId < 1
});
}
}
return StatusPlayers;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,214 @@
import requests
import time
import json
import collections
import os
# the following classes model the discord webhook api parameters
class WebhookAuthor():
def __init__(self, name=None, url=None, icon_url=None):
if name:
self.name = name
if url:
self.url = url
if icon_url:
self.icon_url = icon_url
class WebhookField():
def __init__(self, name=None, value=None, inline=False):
if name:
self.name = name
if value:
self.value = value
if inline:
self.inline = inline
class WebhookEmbed():
def __init__(self):
self.author = ''
self.title = ''
self.url = ''
self.description = ''
self.color = 0
self.fields = []
self.thumbnail = {}
class WebhookParams():
def __init__(self, username=None, avatar_url=None, content=None):
self.username = ''
self.avatar_url = ''
self.content = ''
self.embeds = []
# quick way to convert all the objects to a nice json object
def to_json(self):
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
# gets the relative link to a user's profile
def get_client_profile(profile_id):
return u'{}/Client/ProfileAsync/{}'.format(base_url, profile_id)
def get_client_profile_markdown(client_name, profile_id):
return u'[{}]({})'.format(client_name, get_client_profile(profile_id))
#todo: exception handling for opening the file
if os.getenv("DEBUG"):
config_file_name = 'config.dev.json'
else:
config_file_name = 'config.json'
with open(config_file_name) as json_config_file:
json_config = json.load(json_config_file)
# this should be an URL to an IP or FQN to an IW4MAdmin instance
# ie http://127.0.0.1 or http://IW4MAdmin.com
base_url = json_config['IW4MAdminUrl']
end_point = '/api/event'
request_url = base_url + end_point
# this should be the full discord webhook url
# ie https://discordapp.com/api/webhooks/<id>/<token>
discord_webhook_notification_url = json_config['DiscordWebhookNotificationUrl']
discord_webhook_information_url = json_config['DiscordWebhookInformationUrl']
# this should be the numerical id of the discord group
# 12345678912345678
notify_role_ids = json_config['NotifyRoleIds']
def get_new_events():
events = []
response = requests.get(request_url)
data = response.json()
should_notify = False
for event in data:
# commonly used event info items
event_type = event['eventType']['name']
server_name = event['ownerEntity']['name']
if event['originEntity']:
origin_client_name = event['originEntity']['name']
origin_client_id = int(event['originEntity']['id'])
if event['targetEntity']:
target_client_name = event['targetEntity']['name'] or ''
target_client_id = int(event['targetEntity']['id']) or 0
webhook_item = WebhookParams()
webhook_item_embed = WebhookEmbed()
#todo: the following don't need to be generated every time, as it says the same
webhook_item.username = 'IW4MAdmin'
webhook_item.avatar_url = 'https://raidmax.org/IW4MAdmin/img/iw4adminicon-3.png'
webhook_item_embed.color = 31436
webhook_item_embed.url = base_url
webhook_item_embed.thumbnail = { 'url' : 'https://raidmax.org/IW4MAdmin/img/iw4adminicon-3.png' }
webhook_item.embeds.append(webhook_item_embed)
# the server should be visible on all event types
server_field = WebhookField('Server', server_name)
webhook_item_embed.fields.append(server_field)
role_ids_string = ''
for id in notify_role_ids:
role_ids_string += '\r\n<@&{}>\r\n'.format(id)
if event_type == 'Report':
report_reason = event['extraInfo']
report_reason_field = WebhookField('Reason', report_reason)
reported_by_field = WebhookField('By', get_client_profile_markdown(origin_client_name, origin_client_id))
reported_field = WebhookField('Reported Player',get_client_profile_markdown(target_client_name, target_client_id))
# add each fields to the embed
webhook_item_embed.title = 'Player Reported'
webhook_item_embed.fields.append(reported_field)
webhook_item_embed.fields.append(reported_by_field)
webhook_item_embed.fields.append(report_reason_field)
should_notify = True
elif event_type == 'Ban':
ban_reason = event['extraInfo']
ban_reason_field = WebhookField('Reason', ban_reason)
banned_by_field = WebhookField('By', get_client_profile_markdown(origin_client_name, origin_client_id))
banned_field = WebhookField('Banned Player', get_client_profile_markdown(target_client_name, target_client_id))
# add each fields to the embed
webhook_item_embed.title = 'Player Banned'
webhook_item_embed.fields.append(banned_field)
webhook_item_embed.fields.append(banned_by_field)
webhook_item_embed.fields.append(ban_reason_field)
should_notify = True
elif event_type == 'Connect':
connected_field = WebhookField('Connected Player', get_client_profile_markdown(origin_client_name, origin_client_id))
webhook_item_embed.title = 'Player Connected'
webhook_item_embed.fields.append(connected_field)
elif event_type == 'Disconnect':
disconnected_field = WebhookField('Disconnected Player', get_client_profile_markdown(origin_client_name, origin_client_id))
webhook_item_embed.title = 'Player Disconnected'
webhook_item_embed.fields.append(disconnected_field)
elif event_type == 'Say':
say_client_field = WebhookField('Player', get_client_profile_markdown(origin_client_name, origin_client_id))
message_field = WebhookField('Message', event['extraInfo'])
webhook_item_embed.title = 'Message From Player'
webhook_item_embed.fields.append(say_client_field)
webhook_item_embed.fields.append(message_field)
#if event_type == 'ScriptKill' or event_type == 'Kill':
# kill_str = '{} killed {}'.format(get_client_profile_markdown(origin_client_name, origin_client_id),
# get_client_profile_markdown(target_client_name, target_client_id))
# killed_field = WebhookField('Kill Information', kill_str)
# webhook_item_embed.title = 'Player Killed'
# webhook_item_embed.fields.append(killed_field)
#todo: handle other events
else:
continue
#make sure there's at least one group to notify
if len(notify_role_ids) > 0:
# unfortunately only the content can be used to to notify members in groups
#embed content shows the role but doesn't notify
webhook_item.content = role_ids_string
events.append({'item' : webhook_item, 'notify' : should_notify})
return events
# sends the data to the webhook location
def execute_webhook(data):
for event in data:
event_json = event['item'].to_json()
url = None
if event['notify']:
url = discord_webhook_notification_url
else:
if len(discord_webhook_information_url) > 0:
url = discord_webhook_information_url
if url :
response = requests.post(url,
data=event_json,
headers={'Content-type' : 'application/json'})
# grabs new events and executes the webhook fo each valid event
def run():
failed_count = 1
print('starting polling for events')
while True:
try:
new_events = get_new_events()
execute_webhook(new_events)
except Exception as e:
print('failed to get new events ({})'.format(failed_count))
print(e)
failed_count += 1
time.sleep(5)
if __name__ == "__main__":
run()

View File

@ -0,0 +1,99 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>15a81d6e-7502-46ce-8530-0647a380b5f4</ProjectGuid>
<ProjectHome>.</ProjectHome>
<StartupFile>DiscordWebhook.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<OutputPath>.</OutputPath>
<Name>DiscordWebhook</Name>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<RootNamespace>DiscordWebhook</RootNamespace>
<InterpreterId>MSBuild|env|$(MSBuildProjectFullPath)</InterpreterId>
<IsWindowsApplication>False</IsWindowsApplication>
<LaunchProvider>Standard Python launcher</LaunchProvider>
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
<Environment>DEBUG=True</Environment>
<PublishUrl>C:\Projects\IW4M-Admin\Publish\WindowsPrerelease\DiscordWebhook</PublishUrl>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Prerelease' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
<OutputPath>bin\Prerelease\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="DiscordWebhook.py" />
</ItemGroup>
<ItemGroup>
<Interpreter Include="env\">
<Id>env</Id>
<Version>3.6</Version>
<Description>env (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
</ItemGroup>
<ItemGroup>
<Content Include="config.json">
<Publish>True</Publish>
</Content>
<Content Include="requirements.txt">
<Publish>True</Publish>
</Content>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
<!-- Uncomment the CoreCompile target to enable the Build command in
Visual Studio and specify your pre- and post-build commands in
the BeforeBuild and AfterBuild targets below. -->
<!--<Target Name="CoreCompile" />-->
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<AutoAssignPort>True</AutoAssignPort>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@ -0,0 +1,6 @@
{
"IW4MAdminUrl": "",
"DiscordWebhookNotificationUrl": "",
"DiscordWebhookInformationUrl": "",
"NotifyRoleIds": []
}

View File

@ -0,0 +1,7 @@
certifi>=2018.4.16
chardet>=3.0.4
idna>=2.7
pip>=18.0
requests>=2.19.1
setuptools>=39.0.1
urllib3>=1.23

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 ( level.teamBased && isDefined( attacker ) && ( self != attacker ) && isDefined( attacker.team ) && ( self.pers[ "team" ] == attacker.team ) )
{
return;
}
if ( self.health - iDamage > 0 )
{
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

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>42efda12-10d3-4c40-a210-9483520116bc</ProjectGuid>
<ProjectHome>.</ProjectHome>
<ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
<StartupFile>runserver.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<LaunchProvider>Standard Python launcher</LaunchProvider>
<WebBrowserUrl>http://localhost</WebBrowserUrl>
<OutputPath>.</OutputPath>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<Name>GameLogServer</Name>
<RootNamespace>GameLogServer</RootNamespace>
<InterpreterId>MSBuild|log_env|$(MSBuildProjectFullPath)</InterpreterId>
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
<Environment>DEBUG=True</Environment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Prerelease' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
<OutputPath>bin\Prerelease\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="GameLogServer\log_reader.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="GameLogServer\restart_resource.py" />
<Compile Include="GameLogServer\server.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="runserver.py" />
<Compile Include="GameLogServer\__init__.py" />
<Compile Include="GameLogServer\log_resource.py" />
</ItemGroup>
<ItemGroup>
<Folder Include="GameLogServer\" />
</ItemGroup>
<ItemGroup>
<None Include="Stable.pubxml" />
</ItemGroup>
<ItemGroup>
<Interpreter Include="env\">
<Id>env</Id>
<Version>3.6</Version>
<Description>env (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<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>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
</ItemGroup>
<ItemGroup>
<Content Include="requirements.txt" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
<!-- Specify pre- and post-build commands in the BeforeBuild and
AfterBuild targets below. -->
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<AutoAssignPort>True</AutoAssignPort>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@ -0,0 +1,9 @@
"""
The flask application package.
"""
from flask import Flask
from flask_restful import Api
app = Flask(__name__)
api = Api(app)

View File

@ -0,0 +1,118 @@
import re
import os
import time
import random
import string
class LogReader(object):
def __init__(self):
self.log_file_sizes = {}
# (if the time between checks is greater, ignore ) - in seconds
self.max_file_time_change = 30
def read_file(self, path, retrieval_key):
# this removes old entries that are no longer valid
try:
self._clear_old_logs()
except Exception as e:
print('could not clear old logs')
print(e)
if os.name != 'nt':
path = re.sub(r'^[A-Z]\:', '', path)
path = re.sub(r'\\+', '/', path)
# prevent traversing directories
if re.search('r^.+\.\.\\.+$', path):
return self._generate_bad_response()
# must be a valid log path and log file
if not re.search(r'^.+[\\|\/](.+)[\\|\/].+.log$', path):
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 self._generate_bad_response()
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
}
else:
last_log_info = self.log_file_sizes[retrieval_key]
print('next key is %s' % next_retrieval_key)
expired_key = last_log_info['previous_key']
print('expired key is %s' % expired_key)
# 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
}
if expired_key in self.log_file_sizes:
print('deleting expired key %s' % expired_key)
del self.log_file_sizes[expired_key]
#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(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_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 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:
return os.stat(path).st_size
except Exception as e:
print('could not get the size of the log file at {0}'.format(path))
print(e)
return -1
reader = LogReader()

View File

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

View File

@ -0,0 +1,29 @@
#from flask_restful import Resource
#from flask import request
#import requests
#import os
#import subprocess
#import re
#def get_pid_of_server_windows(port):
# process = subprocess.Popen('netstat -aon', shell=True, stdout=subprocess.PIPE)
# output = process.communicate()[0]
# matches = re.search(' *(UDP) +([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}):'+ str(port) + ' +[^\w]*([0-9]+)', output.decode('utf-8'))
# if matches is not None:
# return matches.group(3)
# else:
# return 0
#class RestartResource(Resource):
# def get(self):
# try:
# response = requests.get('http://' + request.remote_addr + ':1624/api/restartapproved')
# if response.status_code == 200:
# pid = get_pid_of_server_windows(response.json()['port'])
# subprocess.check_output("Taskkill /PID %s /F" % pid)
# else:
# return {}, 400
# except Exception as e:
# print(e)
# return {}, 500
# return {}, 200

View File

@ -0,0 +1,14 @@
from flask import Flask
from flask_restful import Api
from .log_resource import LogResource
#from .restart_resource import RestartResource
import logging
app = Flask(__name__)
def init():
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
api = Api(app)
api.add_resource(LogResource, '/log/<string:path>/<string:retrieval_key>')
#api.add_resource(RestartResource, '/restart')

View File

@ -0,0 +1,12 @@
aniso8601==6.0.0
Click==7.0
Flask==1.0.2
Flask-RESTful==0.3.7
itsdangerous==1.1.0
Jinja2==2.10
MarkupSafe==1.1.1
pip==10.0.1
pytz==2018.9
setuptools==39.0.1
six==1.12.0
Werkzeug==0.16.0

View File

@ -0,0 +1,15 @@
"""
This script runs the GameLogServer application using a development server.
"""
from os import environ
from GameLogServer.server import app, init
if __name__ == '__main__':
HOST = environ.get('SERVER_HOST', '0.0.0.0')
try:
PORT = int(environ.get('SERVER_PORT', '1625'))
except ValueError:
PORT = 5555
init()
app.run('0.0.0.0', PORT, debug=False)

View File

@ -1,14 +1,18 @@

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
_customcallbacks.gsc = _customcallbacks.gsc
GameFiles\IW4x\userraw\_commands.gsc = GameFiles\IW4x\userraw\_commands.gsc
GameFiles\IW4x\userraw\_customcallbacks.gsc = GameFiles\IW4x\userraw\_customcallbacks.gsc
azure-pipelines.yml = azure-pipelines.yml
PostPublish.ps1 = PostPublish.ps1
README.md = README.md
RunPublishPre.cmd = RunPublishPre.cmd
RunPublishRelease.cmd = RunPublishRelease.cmd
version.txt = version.txt
EndProjectSection
EndProject
@ -28,7 +32,30 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Login", "Plugins\Login\Logi
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Master", "Master\Master.pyproj", "{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
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\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js
Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js
Plugins\ScriptPlugins\ParserTeknoMW3.js = Plugins\ScriptPlugins\ParserTeknoMW3.js
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
EndProjectSection
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "GameLogServer", "GameLogServer\GameLogServer.pyproj", "{42EFDA12-10D3-4C40-A210-9483520116BC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{A848FCF1-8527-4AA8-A1AA-50D29695C678}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatsWeb", "Plugins\Web\StatsWeb\StatsWeb.csproj", "{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}"
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -260,6 +287,124 @@ Global
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x64.Build.0 = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.ActiveCfg = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x64.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x64.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x86.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x86.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x64.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x86.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Any CPU.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.ActiveCfg = Release|Any CPU
{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
{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
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.Build.0 = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x64.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x64.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x86.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x86.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x64.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x86.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Any CPU.Build.0 = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x64.ActiveCfg = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x64.Build.0 = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x86.ActiveCfg = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x86.Build.0 = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x64.ActiveCfg = Debug|Any CPU
{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 = 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
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x64.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x86.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Any CPU.Build.0 = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x64.ActiveCfg = Release|Any CPU
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -270,6 +415,12 @@ Global
{958FF7EC-0226-4E85-A85B-B84EC768197D} = {26E8B310-269E-46D4-A612-24601F16065F}
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F}
{B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F}
{6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F}
{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {26E8B310-269E-46D4-A612-24601F16065F}
{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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

View File

@ -7,17 +7,25 @@
<ProjectGuid>f5051a32-6bd0-4128-abba-c202ee15fc5c</ProjectGuid>
<ProjectHome>.</ProjectHome>
<ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
<StartupFile>runserver.py</StartupFile>
<StartupFile>master\runserver.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<LaunchProvider>Web launcher</LaunchProvider>
<LaunchProvider>Standard Python launcher</LaunchProvider>
<WebBrowserUrl>http://localhost</WebBrowserUrl>
<OutputPath>.</OutputPath>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<Name>Master</Name>
<RootNamespace>Master</RootNamespace>
<InterpreterId>MSBuild|dev_env|$(MSBuildProjectFullPath)</InterpreterId>
<InterpreterId>MSBuild|env_master|$(MSBuildProjectFullPath)</InterpreterId>
<IsWindowsApplication>False</IsWindowsApplication>
<PythonRunWebServerCommand>
</PythonRunWebServerCommand>
<PythonDebugWebServerCommand>
</PythonDebugWebServerCommand>
<PythonRunWebServerCommandType>script</PythonRunWebServerCommandType>
<PythonDebugWebServerCommandType>script</PythonDebugWebServerCommandType>
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
@ -51,27 +59,36 @@
<Compile Include="master\models\__init__.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="Master\resources\authenticate.py">
<Compile Include="master\resources\authenticate.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\resources\history_graph.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="Master\resources\instance.py">
<Compile Include="master\resources\instance.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="Master\resources\null.py">
<Compile Include="master\resources\localization.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="Master\resources\version.py">
<Compile Include="master\resources\null.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="Master\resources\__init__.py">
<Compile Include="Master\resources\server.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\resources\version.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\resources\__init__.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\routes.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\runserver.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\schema\instanceschema.py">
<SubType>Code</SubType>
</Compile>
@ -81,33 +98,32 @@
<Compile Include="master\schema\__init__.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="runserver.py" />
<Compile Include="master\__init__.py" />
<Compile Include="master\views.py" />
</ItemGroup>
<ItemGroup>
<Folder Include="C:\Projects\IW4M-Admin\Master\master\" />
<Folder Include="master\" />
<Folder Include="master\context\" />
<Folder Include="master\models\" />
<Folder Include="master\config\" />
<Folder Include="master\schema\" />
<Folder Include="Master\resources\" />
<Folder Include="Master\static\" />
<Folder Include="Master\templates\" />
<Folder Include="master\static\" />
<Folder Include="master\templates\" />
</ItemGroup>
<ItemGroup>
<None Include="FolderProfile.pubxml" />
<Content Include="master\config\master.json" />
<Content Include="master\templates\serverlist.html" />
<None Include="Release.pubxml" />
<Content Include="requirements.txt" />
<Content Include="Master\templates\index.html" />
<Content Include="Master\templates\layout.html" />
<Content Include="master\templates\index.html" />
<Content Include="master\templates\layout.html" />
</ItemGroup>
<ItemGroup>
<Interpreter Include="dev_env\">
<Id>dev_env</Id>
<Interpreter Include="env_master\">
<Id>env_master</Id>
<Version>3.6</Version>
<Description>dev_env (Python 3.6 (64-bit))</Description>
<Description>env_master (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>

View File

@ -6,7 +6,6 @@ from flask import Flask
from flask_restful import Resource, Api
from flask_jwt_extended import JWTManager
from master.context.base import Base
import json
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'my key!'
@ -14,7 +13,7 @@ app.config['PROPAGATE_EXCEPTIONS'] = True
jwt = JWTManager(app)
api = Api(app)
ctx = Base()
config = json.load(open('./master/config/master.json'))
#config = json.load(open('./master/config/master.json'))
import master.routes
import master.views

View File

@ -1,4 +1,4 @@
{
"current-version-stable": 2.0,
"current-version-prerelease": 2.0
"current-version-stable": 2.1,
"current-version-prerelease": 2.1
}

View File

@ -15,9 +15,9 @@ class Base():
self.scheduler.start()
self.scheduler.add_job(
func=self._remove_staleinstances,
trigger=IntervalTrigger(seconds=120),
trigger=IntervalTrigger(seconds=60),
id='stale_instance_remover',
name='Remove stale instances if no heartbeat in 120 seconds',
name='Remove stale instances if no heartbeat in 60 seconds',
replace_existing=True
)
self.scheduler.add_job(
@ -37,10 +37,11 @@ class Base():
client_num += server.clientnum
self.history.add_client_history(client_num)
self.history.add_instance_history(len(self.instance_list))
self.history.add_server_history(len(servers))
def _remove_staleinstances(self):
for key, value in list(self.instance_list.items()):
if int(time.time()) - value.last_heartbeat > 120:
if int(time.time()) - value.last_heartbeat > 60:
print('[_remove_staleinstances] removing stale instance {id}'.format(id=key))
del self.instance_list[key]
del self.token_list[key]

View File

@ -5,17 +5,26 @@ class History():
def __init__(self):
self.client_history = list()
self.instance_history = list()
self.server_history = list()
def add_client_history(self, client_num):
if len(self.client_history) > 1440:
if len(self.client_history) > 20160:
self.client_history = self.client_history[1:]
self.client_history.append({
'count' : client_num,
'time' : int(time.time())
})
def add_server_history(self, server_num):
if len(self.server_history) > 20160:
self.server_history = self.server_history[1:]
self.server_history.append({
'count' : server_num,
'time' : int(time.time())
})
def add_instance_history(self, instance_num):
if len(self.instance_history) > 1440:
if len(self.instance_history) > 20160:
self.instance_history = self.instance_history[1:]
self.instance_history.append({
'count' : instance_num,

View File

@ -1,14 +1,16 @@
class ServerModel(object):
def __init__(self, id, port, game, hostname, clientnum, maxclientnum, map, gametype):
def __init__(self, id, port, game, hostname, clientnum, maxclientnum, map, gametype, ip, version):
self.id = id
self.port = port
self.version = version
self.game = game
self.hostname = hostname
self.clientnum = clientnum
self.maxclientnum = maxclientnum
self.map = map
self.gametype = gametype
self.ip = ip
def __repr__(self):
return '<ServerModel(id={id})>'.format(id=self.id)

View File

@ -8,10 +8,11 @@ import datetime
class Authenticate(Resource):
def post(self):
instance_id = request.json['id']
if ctx.get_token(instance_id) is not False:
return { 'message' : 'that id already has a token'}, 401
else:
expires = datetime.timedelta(days=1)
token = create_access_token(instance_id, expires_delta=expires)
ctx.add_token(instance_id, token)
return { 'access_token' : token }, 200
#todo: see why this is failing
#if ctx.get_token(instance_id) is not False:
# return { 'message' : 'that id already has a token'}, 401
#else:
expires = datetime.timedelta(days=30)
token = create_access_token(instance_id, expires_delta=expires)
ctx.add_token(instance_id, token)
return { 'access_token' : token }, 200

View File

@ -11,22 +11,18 @@ class HistoryGraph(Resource):
custom_style = Style(
background='transparent',
plot_background='transparent',
foreground='rgba(109, 118, 126, 0.3)',
foreground_strong='rgba(109, 118, 126, 0.3)',
foreground_subtle='rgba(109, 118, 126, 0.3)',
foreground='#6c757d',
foreground_strong='#6c757d',
foreground_subtle='#6c757d',
opacity='0.1',
opacity_hover='0.2',
transition='100ms ease-in',
colors=('#007acc', '#749363')
transition='0ms',
colors=('#749363','#007acc'),
)
graph = pygal.StackedLine(
interpolate='cubic',
interpolation_precision=3,
#x_labels_major_every=100,
#x_labels_major_count=500,
graph = pygal.Line(
stroke_style={'width': 0.4},
show_dots=False,
#show_dots=False,
show_legend=False,
fill=True,
style=custom_style,
@ -37,10 +33,17 @@ class HistoryGraph(Resource):
if len(instance_count) > 0:
graph.x_labels = [ timeago.format(instance_count[0])]
graph.add('Instance Count', [history['count'] for history in ctx.history.instance_history][-history_count:])
graph.add('Client Count', [history['count'] for history in ctx.history.client_history][-history_count:])
return { 'message' : graph.render(),
'data_points' : len(instance_count)
instance_counts = [history['count'] for history in ctx.history.instance_history][-history_count:]
client_counts = [history['count'] for history in ctx.history.client_history][-history_count:]
server_counts = [history['count'] for history in ctx.history.server_history][-history_count:]
graph.add('Client Count', client_counts)
graph.add('Instance Count', instance_counts)
return { 'message' : graph.render().replace("<title>Pygal</title>", ""),
'data_points' : len(instance_count),
'instance_count' : 0 if len(instance_counts) is 0 else instance_counts[-1],
'client_count' : 0 if len(client_counts) is 0 else client_counts[-1],
'server_count' : 0 if len(server_counts) is 0 else server_counts[-1]
}, 200
except Exception as e:
return { 'message' : str(e) }, 500

View File

@ -4,6 +4,8 @@ from flask_jwt_extended import jwt_required
from marshmallow import ValidationError
from master.schema.instanceschema import InstanceSchema
from master import ctx
import json
from netaddr import IPAddress
class Instance(Resource):
def get(self, id=None):
@ -21,6 +23,11 @@ class Instance(Resource):
@jwt_required
def put(self, id):
try:
for server in request.json['servers']:
if 'ip' not in server or IPAddress(server['ip']).is_private() or IPAddress(server['ip']).is_loopback():
server['ip'] = request.remote_addr
if 'version' not in server:
server['version'] = 'Unknown'
instance = InstanceSchema().load(request.json)
except ValidationError as err:
return {'message' : err.messages }, 400
@ -30,6 +37,11 @@ class Instance(Resource):
@jwt_required
def post(self):
try:
for server in request.json['servers']:
if 'ip' not in server or server['ip'] == 'localhost':
server['ip'] = request.remote_addr
if 'version' not in server:
server['version'] = 'Unknown'
instance = InstanceSchema().load(request.json)
except ValidationError as err:
return {'message' : err.messages }, 400

View File

@ -0,0 +1,59 @@
from flask_restful import Resource
from flask import request, jsonify
from flask_jwt_extended import create_access_token
from master import app, ctx
import datetime
import urllib.request
import csv
from io import StringIO
class Localization(Resource):
def list(self):
response = urllib.request.urlopen('https://docs.google.com/spreadsheets/d/e/2PACX-1vRQjCqPvd0Xqcn86WqpFqp_lx4KKpel9O4OV13NycmV8rmqycorgJQm-8qXMfw37QJHun3pqVZFUKG-/pub?gid=0&single=true&output=csv')
data = response.read().decode('utf-8')
localization = []
csv_data = csv.DictReader(StringIO(data))
for language in csv_data.fieldnames[1:]:
localization.append({
'LocalizationName' : language,
'LocalizationIndex' : {
'Set' : {}
}
})
for row in csv_data:
localization_string = row['STRING']
count = 0
for language in csv_data.fieldnames[1:]:
localization[count]['LocalizationIndex']['Set'][localization_string] = row[language]
count += 1
return localization, 200
def get(self, language_tag=None):
response = urllib.request.urlopen('https://docs.google.com/spreadsheets/d/e/2PACX-1vRQjCqPvd0Xqcn86WqpFqp_lx4KKpel9O4OV13NycmV8rmqycorgJQm-8qXMfw37QJHun3pqVZFUKG-/pub?gid=0&single=true&output=csv')
data = response.read().decode('utf-8')
csv_data = csv.DictReader(StringIO(data))
if language_tag != None:
valid_language_tag = next((l for l in csv_data.fieldnames[1:] if l == language_tag), None)
if valid_language_tag is None:
valid_language_tag = next((l for l in csv_data.fieldnames[1:] if l.startswith(language_tag[:2])), None)
if valid_language_tag is None:
valid_language_tag = 'en-US'
localization = {
'LocalizationName' : valid_language_tag,
'LocalizationIndex' : {
'Set' : {}
}
}
for row in csv_data:
localization_string = row['STRING']
localization['LocalizationIndex']['Set'][localization_string] = row[valid_language_tag]
return localization, 200
else:
return self.list()[0][0], 200

View File

@ -0,0 +1,6 @@
from flask_restful import Resource
class Server(Resource):
"""description of class"""

View File

@ -1,8 +1,9 @@
from flask_restful import Resource
from master import config
import json
class Version(Resource):
def get(self):
config = json.load(open('./master/config/master.json'))
return {
'current-version-stable' : config['current-version-stable'],
'current-version-prerelease' : config['current-version-prerelease']

View File

@ -5,9 +5,13 @@ from master.resources.instance import Instance
from master.resources.authenticate import Authenticate
from master.resources.version import Version
from master.resources.history_graph import HistoryGraph
from master.resources.localization import Localization
from master.resources.server import Server
api.add_resource(Null, '/null')
api.add_resource(Instance, '/instance/', '/instance/<string:id>')
api.add_resource(Version, '/version')
api.add_resource(Authenticate, '/authenticate')
api.add_resource(HistoryGraph, '/history/', '/history/<int:history_count>')
api.add_resource(Localization, '/localization/', '/localization/<string:language_tag>')
api.add_resource(Server, '/server')

View File

@ -4,19 +4,26 @@ from master.models.servermodel import ServerModel
class ServerSchema(Schema):
id = fields.Int(
required=True,
validate=validate.Range(1, 2147483647, 'invalid id')
validate=validate.Range(0, 25525525525565535, 'invalid id')
)
ip = fields.Str(
required=True
)
port = fields.Int(
required=True,
validate=validate.Range(1, 665535, 'invalid port')
validate=validate.Range(1, 65535, 'invalid port')
)
version = fields.String(
required=False,
validate=validate.Length(0, 128, 'invalid server version')
)
game = fields.String(
required=True,
validate=validate.Length(1, 8, 'invalid game name')
validate=validate.Length(1, 5, 'invalid game name')
)
hostname = fields.String(
required=True,
validate=validate.Length(1, 48, 'invalid hostname')
validate=validate.Length(1, 128, 'invalid hostname')
)
clientnum = fields.Int(
required=True,
@ -28,7 +35,7 @@ class ServerSchema(Schema):
)
map = fields.String(
required=True,
validate=validate.Length(1, 32, 'invalid map name')
validate=validate.Length(0, 64, 'invalid map name')
)
gametype = fields.String(
required=True,

View File

@ -2,46 +2,59 @@
{% 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 pr-3 mr-4">
<figcaption class="float-right">
<span id="history_graph_zoom_out" class="h4 oi oi-zoom-out text-muted" style="cursor:pointer;"></span>
<span id="history_graph_zoom_in" class="h4 oi oi-zoom-in text-muted" style="cursor:pointer;"></span>
</figcaption>
<figcaption class="float-left">
<span class="h4 text-muted">{{instance_count}} instances</span>
<span class="h4 text-muted">&mdash; {{client_count}} clients</span>
<span class="h4 text-muted">&mdash; {{server_count}} servers</span>
</figcaption>
</figure>
</div>
</div>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="http://kozea.github.com/pygal.js/latest/pygal-tooltips.min.js"></script>
<script>
let dataPoints = {{data_points}};
let zoomLevel = Math.ceil(dataPoints / 2);
//console.log(dataPoints);
<script type="text/javascript" src="http://kozea.github.com/pygal.js/latest/pygal-tooltips.min.js"></script>
<script>
let dataPoints = {{data_points}};
let maxPoints = 2880;
maxPoints = Math.min(maxPoints, dataPoints);
let zoomLevel = Math.floor(maxPoints);
let performingZoom = false;
function updateHistoryGraph() {
$.get('/history/' + zoomLevel)
.done(function (content) {
$('#history_graph').html(content.message);
dataPoints = content.data_points
});
function updateHistoryGraph() {
perfomingZoom = true;
$.get('/history/' + zoomLevel)
.done(function (content) {
$('#history_graph').html(content.message);
//maxPoints = Math.min(maxPoints, dataPoints);
perfomingZoom = false;
});
}
//setInterval(updateHistoryGraph, 30000);
$('#history_graph_zoom_out').click(function () {
if (performingZoom === true) {
return false;
}
setInterval(updateHistoryGraph, 30000);
zoomLevel = Math.floor(zoomLevel * 2) <= maxPoints ? Math.floor(zoomLevel * 2) : maxPoints;
updateHistoryGraph();
});
$('#history_graph_zoom_out').click(function () {
// console.log(zoomLevel);
zoomLevel = zoomLevel * 2 <= 1440 ? Math.ceil(zoomLevel * 2) : dataPoints;
updateHistoryGraph();
});
$('#history_graph_zoom_in').click(function () {
if (performingZoom === true) {
return false;
}
zoomLevel = zoomLevel / 2 > 2 ? Math.ceil(zoomLevel / 2) : 2;
updateHistoryGraph();
});
$('#history_graph_zoom_in').click(function () {
// console.log(zoomLevel);
zoomLevel = zoomLevel / 2 > 2 ? Math.ceil(zoomLevel / 2) : 2;
updateHistoryGraph();
});
</script>
</script>
{% endblock %}

View File

@ -14,6 +14,18 @@
.oi:hover {
color: #fff !important;
}
.dot {
opacity: 0;
padding: 5px;
}
.dot:hover {
opacity: 1;
}
.tooltip-box {
fill: #343a40 !important;
}
</style>
</head>
@ -27,6 +39,7 @@
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,113 @@
{% extends "layout.html" %}
{% block content %}
<!-- todo: move this! -->
<style>
.server-row {
cursor: pointer;
}
.modal-content, .nav-item {
background-color: #212529;
color: #fff;
}
.modal-header, .modal-footer {
border-color: #32383e !important;
}
.modal-dark button.close, a.nav-link {
color: #fff;
}
</style>
<div class="modal modal-dark" id="serverModalCenter" tabindex="-1" role="dialog" aria-labelledby="serverModalCenterTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="serverModalTitle">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="h5" id="server_socket"></div>
</div>
<div class="modal-footer">
<button id="connect_button" type="button" class="btn btn-dark">Connect</button>
<button type="button" class="btn btn-dark" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<nav>
<div class="nav nav-tabs" id="server_game_tabs" role="tablist">
{% for game in games %}
<a class="nav-item nav-link {{'active' if loop.first else ''}}" id="{{game}}_servers_tab" data-toggle="tab" href="#{{game}}_servers" role="tab" aria-controls="{{game}}_servers" aria-selected="{{'true' if loop.first else 'false' }}">{{game}}</a>
{% endfor %}
</div>
</nav>
<div class="tab-content" id="server_game_tabs_content">
{% for game, servers in games.items() %}
<div class="tab-pane {{'show active' if loop.first else ''}}" id="{{game}}_servers" role="tabpanel" aria-labelledby="{{game}}_servers_tab">
<table class="table table-dark table-striped table-hover table-responsive-lg">
<thead>
<tr>
<th>Server Name</th>
<th>Map Name</th>
<th>Players</th>
<th>Mode</th>
<th class="text-center">Connect</th>
</tr>
</thead>
<tbody>
{% for server in servers %}
<tr class="server-row" data-toggle="modal" data-target="#serverModalCenter"
data-ip="{{server.ip}}" data-port="{{server.port}}">
<td data-hostname="{{server.hostname}}" class="server-hostname">{{server.hostname}}</td>
<td data-map="{{server.map}} " class="server-map">{{server.map}}</td>
<td data-clientnum="{{server.clientnum}}" data-maxclientnum="{{server.maxclientnum}}"
class="server-clientnum">
{{server.clientnum}}/{{server.maxclientnum}}
</td>
<td data-gametype="{{server.gametype}}" class="server-gametype">{{server.gametype}}</td>
<td class="text-center"><span class="oi oi-play-circle"></span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
</div>
<div class="w-100 small text-right text-muted">
<span>Developed by RaidMax</span><br />
<span>PRERELEASE</span>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(() => {
$('.server-row').off('click');
$('.server-row').on('click', function (e) {
$('#serverModalTitle').text($(this).find('.server-hostname').text());
$('#server_socket').text(`/connect ${$(this).data('ip')}:${$(this).data('port')}`);
});
$('#connect_button').off('click');
$('#connect_button').on('click', (e) => alert('soon...'));
});
</script>
{% endblock %}

View File

@ -4,15 +4,32 @@ Routes and views for the flask application.
from datetime import datetime
from flask import render_template
from master import app
from master import app, ctx
from master.resources.history_graph import HistoryGraph
from collections import defaultdict
@app.route('/')
def home():
_history_graph = HistoryGraph().get(500)
_history_graph = HistoryGraph().get(2880)
return render_template(
'index.html',
title='API Overview',
history_graph = _history_graph[0]['message'],
data_points = _history_graph[0]['data_points']
data_points = _history_graph[0]['data_points'],
instance_count = _history_graph[0]['instance_count'],
client_count = _history_graph[0]['client_count'],
server_count = _history_graph[0]['server_count']
)
@app.route('/servers')
def servers():
servers = defaultdict(list)
if len(ctx.instance_list.values()) > 0:
ungrouped_servers = [server for instance in ctx.instance_list.values() for server in instance.servers]
for server in ungrouped_servers:
servers[server.game].append(server)
return render_template(
'serverlist.html',
title = 'Server List',
games = servers
)

View File

@ -1,16 +1,26 @@
aniso8601==3.0.0
aniso8601==3.0.2
APScheduler==3.5.3
certifi==2018.10.15
chardet==3.0.4
click==6.7
Flask==0.12.2
Flask==1.0.2
Flask-JWT==0.3.2
Flask-JWT-Extended==3.8.1
Flask-RESTful==0.3.6
idna==2.7
itsdangerous==0.24
Jinja2==2.10
MarkupSafe==1.0
marshmallow==3.0.0b8
pip==9.0.1
pip==9.0.3
psutil==5.4.8
pygal==2.4.0
PyJWT==1.4.2
pytz==2018.4
setuptools==39.0.1
pytz==2018.7
requests==2.20.0
setuptools==40.5.0
six==1.11.0
Werkzeug==0.14.1
timeago==1.0.8
tzlocal==1.5.1
urllib3==1.24
Werkzeug==0.15.3

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<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" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.6" PrivateAssets="All" />
</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,29 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
namespace AutomessageFeed
{
class Configuration : IBaseConfiguration
{
public bool EnableFeed { get; set; }
public string FeedUrl { get; set; }
public int MaxFeedItems { get; set; }
public IBaseConfiguration Generate()
{
EnableFeed = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_ENABLE"]);
if (EnableFeed)
{
FeedUrl = Utilities.PromptString(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_URL"]);
MaxFeedItems = Utilities.PromptInt(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_MAXITEMS"],
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_MAXITEMS_DESC"],
0, int.MaxValue, 0);
}
return this;
}
public string Name() => "AutomessageFeedConfiguration";
}
}

View File

@ -0,0 +1,85 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Threading.Tasks;
using Microsoft.SyndicationFeed.Rss;
using SharedLibraryCore.Configuration;
using System.Xml;
using Microsoft.SyndicationFeed;
using System.Collections.Generic;
using SharedLibraryCore.Helpers;
using System.Text.RegularExpressions;
namespace AutomessageFeed
{
public class Plugin : IPlugin
{
public string Name => "Automessage Feed";
public float Version => (float)Utilities.GetVersionAsDouble();
public string Author => "RaidMax";
private Configuration _configuration;
private int _currentFeedItem;
private async Task<string> GetNextFeedItem(Server server)
{
var items = new List<string>();
using (var reader = XmlReader.Create(_configuration.FeedUrl, new XmlReaderSettings() { Async = true }))
{
var feedReader = new RssFeedReader(reader);
while (await feedReader.Read())
{
switch (feedReader.ElementType)
{
case SyndicationElementType.Item:
var item = await feedReader.ReadItem();
items.Add(Regex.Replace(item.Title, @"\<.+\>.*\</.+\>", ""));
break;
}
}
}
if (_currentFeedItem < items.Count && (_configuration.MaxFeedItems == 0 || _currentFeedItem < _configuration.MaxFeedItems))
{
_currentFeedItem++;
return items[_currentFeedItem - 1];
}
_currentFeedItem = 0;
return Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_NO_ITEMS"];
}
public Task OnEventAsync(GameEvent E, Server S)
{
return Task.CompletedTask;
}
public async Task OnLoadAsync(IManager manager)
{
var cfg = new BaseConfigurationHandler<Configuration>("AutomessageFeedPluginSettings");
if (cfg.Configuration() == null)
{
cfg.Set((Configuration)new Configuration().Generate());
await cfg.Save();
}
_configuration = cfg.Configuration();
manager.GetMessageTokens().Add(new MessageToken("FEED", GetNextFeedItem));
}
public Task OnTickAsync(Server S)
{
throw new NotImplementedException();
}
public Task OnUnloadAsync()
{
return Task.CompletedTask;
}
}
}

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

@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Text;
namespace WebfrontCore.Controllers.API
{
[Route("api/gsc/[action]")]
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)
{
var clientInfo = Manager.GetActiveClients()
.FirstOrDefault(c => c.NetworkId == networkId.ConvertGuidToLong(System.Globalization.NumberStyles.HexNumber));
if (clientInfo != null)
{
var sb = new StringBuilder();
sb.AppendLine($"admin={clientInfo.IsPrivileged()}");
sb.AppendLine($"level={(int)clientInfo.Level}");
sb.AppendLine($"levelstring={clientInfo.Level.ToLocalizedLevelName()}");
sb.AppendLine($"connections={clientInfo.Connections}");
sb.AppendLine($"authenticated={clientInfo.GetAdditionalProperty<bool>("IsLoggedIn") == true}");
return Content(sb.ToString());
}
return Content("");
}
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<ApplicationIcon />
<StartupObject />
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.6" PrivateAssets="All" />
</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,46 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System.Threading.Tasks;
namespace IW4ScriptCommands
{
public class Plugin : IPlugin
{
public string Name => "IW4 Script Commands";
public float Version => 1.0f;
public string Author => "RaidMax";
public async Task OnEventAsync(GameEvent E, Server S)
{
if (E.Type == GameEvent.EventType.Start)
{
await S.SetDvarAsync("sv_iw4madmin_serverid", S.EndPoint);
}
if (E.Type == GameEvent.EventType.Warn)
{
var cmd = new ScriptCommand()
{
ClientNumber = E.Target.ClientNumber,
CommandName = "alert",
CommandArguments = new[]
{
"Warning",
"ui_mp_nukebomb_timer",
E.Data
}
};
// notifies the player ingame of the warning
await cmd.Execute(S);
}
}
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;
public Task OnTickAsync(Server S) => Task.CompletedTask;
public Task OnUnloadAsync() => 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,86 @@
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;
public RadarController(IManager manager) : base(manager)
{
_manager = manager;
}
[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 = Plugin.Config.Configuration().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.2.6" 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,73 @@
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";
internal static BaseConfigurationHandler<LiveRadarConfiguration> Config;
public Task OnEventAsync(GameEvent E, Server S)
{
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)
{
// load custom configuration
Config = new BaseConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration");
if (Config.Configuration() == null)
{
Config.Set((LiveRadarConfiguration)new LiveRadarConfiguration().Generate());
await Config.Save();
}
manager.GetPageList().Pages.Add(Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_RADAR_TITLE"], "/Radar/All");
}
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

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