Compare commits

..

139 Commits

Author SHA1 Message Date
e56f574af4 fix introduced bug :) 2020-10-01 19:06:12 -05:00
513f495304 anticheat tweaks
- reset recoil state on map change
- refactor config
- remove m21 from chest detection
- allow ignored client ids
2020-10-01 19:05:52 -05:00
910faf427b enhance script plugin features
(support service resolver with generic args)
(support requiresTarget for command)
2020-10-01 19:05:38 -05:00
9117440566 merge to pre (#172)
* update GenerateGuidFromString to resolve to a stable hash code.
fix bots not showing up on live radar

* add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x

* implement pm admins command for issue #170

* implement service resolver for script plugins
2020-09-26 18:15:56 -05:00
ad6b6a6465 merge to pre (#169)
* update GenerateGuidFromString to resolve to a stable hash code.
fix bots not showing up on live radar

* add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x
2020-09-21 15:37:31 -05:00
eb8145a168 add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x 2020-09-04 12:58:54 -05:00
a560e05df8 update pipeline file for seperate builds 2020-08-31 12:33:39 -05:00
ac06b41a0b update shared library version 2020-08-31 12:31:40 -05:00
cce6482541 allow tracking of "zombie" clients to support stat tracking in zm 2020-08-31 12:13:20 -05:00
bc7dc3a71a Add XuidString and GuidString to EFClient to allow easier interfacing with mods 2020-08-31 12:03:06 -05:00
2be719d8f9 add website override mapping to tekno parser (_website -> sv_clanWebsite) 2020-08-31 11:58:56 -05:00
8a8dec8bbd remove hard coded paths to make it easier for building in debug mode
auto copy script plugins/localization for local builds
2020-08-26 09:54:56 -05:00
2b3e21d4ba fix most played formatting issue
prevent reverse proxy to 127.0.0.1 from counting as IW4MAdmin client
copy humanizer support lib to output dir
2020-08-21 18:12:00 -05:00
4590d94d7d update bundle minifier package to use .net core one 2020-08-20 13:10:43 -05:00
5842073f91 include "all" meta button on profile
include full humanizer package to library bug in russian translations
2020-08-20 11:08:21 -05:00
c783a04a52 hide chat for password protected servers for issue #162 2020-08-20 10:38:11 -05:00
4735864113 remove some left over warnings from deprecated packages 2020-08-19 14:50:49 -05:00
d70d8fd0ae merge 2020-08-18 20:15:46 -05:00
0dc4e12d61 another attempt to fix display of long client names/temporary t6 getinfo workaround 2020-08-18 20:11:41 -05:00
778e339a61 QOL updates for profile meta
implement filterable meta for issue #158
update translations and use humanizer lib with datetime/timespan for issue #80
2020-08-18 16:35:21 -05:00
1ef2ba5344 fix misaligned kick button with long names on webfront 2020-08-18 16:35:21 -05:00
126f2fcc47 Merge branch '2.4-pr' of https://github.com/RaidMax/IW4M-Admin into 2.4-pr 2020-08-18 16:33:45 -05:00
d5789dac81 Create FUNDING.yml (#161) 2020-08-12 20:48:07 -05:00
25e2438e7f fix misaligned kick button with long names on webfront 2020-08-12 13:46:14 -05:00
19107f9e85 Update README.md 2020-08-11 20:48:13 -05:00
0e44fa10f7 Consolidate README (#156)
* Consolidate README.
We use the wiki now for most information, this just reduces "duplicate" data.
2020-08-06 13:20:35 -05:00
ebb54ebfd7 Add ManualWebFrontURL to readme. (#150)
* Update README.md

* Update project links
2020-08-06 08:49:20 -05:00
03a27d113e Merge pull request #105 from xerxes-at/2.4-pr
Added support for the AC to PlutoT6
2020-08-06 08:48:53 -05:00
22f9e581ed fix dependency injection of comands in webfront preventing ui actions from working 2020-08-06 08:48:14 -05:00
b59504a882 grab gametype from status for T7 2020-08-05 09:43:31 -05:00
ed2b01f229 update action controller to dynamically generate command names in case of overridden names (issue #152) 2020-08-04 17:26:16 -05:00
f040dd5159 fix mislabled dragunov name in live radar 2020-08-01 18:14:29 -05:00
6c00cceb7a update stats plugin to properly use the new configurable broadcast prefix. 2020-08-01 09:58:23 -05:00
04a95aa58a add configurable command and broadcast command prefix for issue #149 2020-07-31 20:40:03 -05:00
6155493181 prevent action on report from activating on privileged clients 2020-07-27 16:22:07 -05:00
297e2c283f Merge branch '2.4-pr' of https://github.com/RaidMax/IW4M-Admin into 2.4-pr 2020-07-27 11:26:37 -05:00
d8626bf70c Merge pull request #148 from RaidMax/feature/issue-144-report-action
implement action on report plugin for issue #144
2020-07-25 21:17:12 -05:00
c288184171 implement action on report plugin for issue #144 2020-07-25 21:15:46 -05:00
021c0244b4 remove old test project 2020-07-15 10:11:37 -05:00
214d15384d remove discord deprecated discord webhook file, remove game log file as it's being moved to new repo 2020-07-15 10:09:58 -05:00
36949bbf33 tweak color of kick icon 2020-07-14 15:48:38 -05:00
88b1f08149 add kick client functionality to webfront home for issue #142 2020-07-14 14:13:40 -05:00
4c583e1c53 remove master project 2020-06-30 16:42:30 -05:00
6e95a7b015 support custom master url
refactor api instatation to allow custom master url in config
2020-06-30 16:39:32 -05:00
a013a1faf0 prevent ability to kick users of same rank 2020-06-17 15:20:07 -05:00
bb4e51d9c8 adjustments for T6 and tekno (implement mapped dvars and default values) 2020-06-16 17:16:12 -05:00
ba77e0149c disable standard console in if it has been redirected 2020-06-03 19:45:06 -05:00
b8d5495055 include client name in stats info result 2020-05-30 14:14:42 -05:00
fa79f4af73 fix issue with registering multiple script commands in command configuration 2020-05-30 14:06:04 -05:00
cad2952c46 [issue #140]
fix bug with friendly fire being disabled with custom callbacks on IW4x
2020-05-30 13:39:09 -05:00
43ac1218cc fix shared library linking issue 2020-05-25 14:09:41 -05:00
aef1ac6aae Merge pull request #141 from RaidMax/feature/issue-139-stats-api
[issue #139] client lookup and stats api
2020-05-25 13:06:44 -05:00
30f2f7bf09 [issue #139] client lookup and stats api 2020-05-25 13:04:44 -05:00
4457ee5461 Merge pull request #138 from RaidMax/feature/issue-137-custom-hostname
[issue 137] custom display hostnames for webfront
2020-05-23 13:26:07 -05:00
e91c60a753 [issue 137] custom display hostnames for webfront 2020-05-23 13:25:09 -05:00
1241ac459e re-enable claims permission add/remove 2020-05-22 21:38:38 -05:00
4afd1f3cdc Merge pull request #136 from RaidMax/feature/issue-135-enhanced-search
[issue 135] enhanced search
2020-05-22 20:35:42 -05:00
5042ea6c91 [issue 135] enhanced search
implement enhanced search for chat messages
2020-05-22 20:29:41 -05:00
bef5ffbd35 update IW5 parser 2020-05-19 11:01:08 -05:00
19f5f557bd update readme / upgrade game log server packages to work with latest python release 2020-05-18 21:03:40 -05:00
6aa6af526a fix issue with counting plugin tasks causing them to be executed. why ms? 2020-05-17 17:01:13 -05:00
0cabf6f8a3 only fix double forward slash characters (instead of single) when sending messages
retry kicks on banned players if they're banned from webfront, but don't actually get kicked because the game doesn't process the command (looking at you T6)
allow capturing chat messages for names spoofed to an empty string
make sure mostkills uses days not month for cutoff
2020-05-16 20:55:18 -05:00
d3d1f31ee0 bugfixes/enhancements
prevent users from trying to set the console's level to owner
fix issue with setting multiple owners
update/improve unit tests
2020-05-16 11:54:01 -05:00
420e0d5ab5 Merge pull request #133 from RaidMax/feature/issue-132-script-command-registration
implement script plugin command registration - issue #132
2020-05-11 16:21:33 -05:00
2bd895e99d implement script plugin command registration - issue #132 2020-05-11 16:20:25 -05:00
44cacc1741 Merge pull request #130 from RaidMax/feature/issue-126-most-kills-command
[issue #129]
2020-05-05 18:50:35 -05:00
aff19b9577 [issue #129]
Add most kills command/macro
sneaky fix for tekno parser
2020-05-05 18:49:30 -05:00
267e0b8cbe [tweaks and fixes]
reenable tekno support
address vagrant thread issue
refactor game log reader creation to follow better practices
fix bot issues/address how guids are generated for bots/none provided
2020-05-04 16:50:02 -05:00
b49592d666 fix latent issue with password login due to not retreiving password/salt
set semaphore count properly for event execution throttling
2020-04-29 17:05:36 -05:00
c82139b88c small tweak to hopefully prevent too many events executing simultaneously 2020-04-29 16:27:24 -05:00
33712f3d7d update shared library nuget version 2020-04-28 18:19:46 -05:00
9dfdf5a82b remove debug output for ef 2020-04-28 17:54:06 -05:00
f5b0167f81 Merge pull request #128 from RaidMax/feature/issue-126-create-run-as-command
Feature/issue 126 create run as command
2020-04-28 16:49:40 -05:00
7715113b56 implement audit log view in webfront 2020-04-28 16:48:06 -05:00
58bfd189d0 [issue #126]
implement basic run-as functionality
2020-04-26 21:12:49 -05:00
3645cf53ff update default profanity filters to have something a little more usable 2020-04-26 15:57:51 -05:00
8a98ed7c50 small tweak for preconnect events 2020-04-26 12:32:41 -05:00
5529858edd [misc bug fixes]
properly hide broadcast failure messages if ignore connection lost is turned on
fix concurent issue for update stats history that happened with new event processing
make get/set additional property thread safe
add ellipse to truncated chat messages on home
2020-04-25 19:01:26 -05:00
ff011be8a6 unmeme the build 2020-04-22 21:08:25 -05:00
b41c4c6245 include some of the changes meant for previous build 2020-04-22 20:51:04 -05:00
92a26600af actually fix the session score concurrency issue
fix rare bug with shared guid kicker plugin
allow hiding of the connection lost notification
2020-04-22 18:46:41 -05:00
9e74dac5ed fix stat issue with concurrent threads
fix potential lost penalty if server does not response to kick request
make sure that broadcast only shows one custom say name
add unit tests
2020-04-21 17:34:00 -05:00
3ae2e42718 properly implement sv_sayName for custom say name
prevent trying to register live radar page for every server (oops)
optimize event processing to prevent slow plugins from affecting command processing
enable database connection resilency
trim extra characters from T7 reassembled response
2020-04-20 10:45:58 -05:00
0b643b2099 unmeme a dvar check 2020-04-18 17:48:49 -05:00
ee087f1c85 fix T7 extra null bytes in status response
fix regression bug with info response on T6
2020-04-18 10:46:55 -05:00
8c29027b3f partial T7 (BO3) support. includes rcon communication improvements and a small fix for displaying live radar tab 2020-04-17 15:05:16 -05:00
5bc1ad5926 fix regression issue with wine drive name mangling 2020-04-14 15:46:14 -05:00
c376266090 Merge pull request #123 from RaidMax/feature/issue-77-allow-server-type-categorization
add server categorization feature (issue #77)
2020-04-13 20:27:14 -05:00
8539223a15 add server categorization feature (issue #77) 2020-04-13 20:26:13 -05:00
b188e36786 update for new pluto iw5 rcon response 2020-04-13 19:43:24 -05:00
fca47cbce0 fix regression issue with log paths oops 2020-04-13 18:15:46 -05:00
be8041b868 refactor and test log path generation to support pluto IW5 better 2020-04-13 16:16:31 -05:00
b63d2995ed allow auto log filepath generation for pluto iw5 2020-04-12 20:48:03 -05:00
8fb2394130 support for Plutonium IW5 and only show live radar tab if monitoring at least one IW4 serves 2020-04-11 18:05:18 -05:00
36af673fc7 add ability to register custom event generators for event parsers / truncate long client names fix 2020-04-04 12:40:23 -05:00
9fdf4bad9c fix for runaway regular expression on linux
explicitly set string dvars in quotes to allow setting empty dvars
allow piping in input from command line (#114)
update the distribution for top stats elo
prevent game log file rotation from stopping event parsing
2020-04-01 14:11:56 -05:00
02a784ad09 allow prompt string to have an empty/default value
upgrade some project dependencies
don't try to run events on parsers
update top players rank distribution
2020-02-17 10:05:31 -06:00
2e5ffe91fc fix a small bug with new line truncation missing 2020-02-12 15:11:43 -06:00
68490bde57 Merge pull request #113 from RaidMax/enhancement/issue-112-toggle-automated-penalties-webfront
allow toggle of automated penalties display on the webfront
2020-02-12 13:16:11 -06:00
f430dab3a7 allow toggle of automated penalties display on the webfront
issue #112
fix small issue with script plugin loading
2020-02-12 13:13:59 -06:00
c3c21a7749 refactor a good bit of stuff for better dependency injection
fix regular expression for T6 log parsing
2020-02-11 16:44:06 -06:00
ec053eb854 Merge pull request #111 from RaidMax/enhancement/issue-108-address-t6-specific-behaviors
re-kick working as expected now
2020-02-07 11:16:27 -06:00
33494197e3 re-kick working as expected now 2020-02-07 11:15:21 -06:00
239ca30fd1 Merge pull request #110 from RaidMax/enhancement/issue-108-address-t6-specific-behaviors
fix disconnect event being cancelled
2020-02-06 21:06:38 -06:00
1dd88cdacb fix disconnect event being cancelled 2020-02-06 21:05:50 -06:00
f0f9a6beda Merge pull request #109 from RaidMax/enhancement/issue-108-address-t6-specific-behaviors
Use game time from log to ignore potential false disconnect lines - F…
2020-02-06 18:36:16 -06:00
fe380ca331 Use game time from log to ignore potential false disconnect lines - Fix for latent linking issues with multiple ips - Anticheat fix for T6 - retry kick on update if they're not allowed to connect 2020-02-06 18:35:30 -06:00
15e2170100 just a small fix that I forgot to include in the last build. 2020-02-03 08:21:42 -06:00
2872d02c37 fix plugin error spam with multi-servers 2020-02-02 16:21:34 -06:00
60ff33834e make sure we have an empty command config during initial startup, oops. 2020-02-01 13:28:05 -06:00
06cdaef8a4 allow Kekno to run with sv_running not returning anything :upside_down:
make sure script plugins output correct errors instead of being swallowed
prevent webfront error when webfront tab is left open on a server no longer being modified
2020-02-01 12:27:14 -06:00
c6d6bebeab Merge pull request #107 from RaidMax/feature/issue-104-allow-per-command-permission-config
Feature/issue 104 allow per command permission config
2020-01-31 20:23:37 -06:00
31c259f966 merge dev changes 2020-01-31 20:22:59 -06:00
318a23ae5b Finish implementation of configuable command permissions 2020-01-31 20:15:07 -06:00
11ae91281f start work to allow customizing command properties via configuration 2020-01-26 18:06:50 -06:00
1fd31beb05 fix another meme 2020-01-26 15:40:00 -06:00
116c909c2d Merge pull request #106 from RaidMax/bugfix/issue-0-misc-small-fixes
fix nuget package version for scriptcommands
2020-01-26 14:09:40 -06:00
451072276d fix nuget package version for scriptcommands
fix only one server being added during setup
2020-01-26 14:08:53 -06:00
39fb3b9966 Added support for the AC to PlutoT6
PlutoT6 requires pre-compiled GSC files.
Thats why I include the source and a compiled version. Since we can not create new GSC files but only can replace existing ones I did use this stock GSC to add our code to it.
2020-01-25 19:12:05 +01: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
362 changed files with 16734 additions and 6724 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
ko_fi: raidmax

6
.gitignore vendored
View File

@ -238,3 +238,9 @@ launchSettings.json
/GameLogServer/log_env /GameLogServer/log_env
**/*.css **/*.css
/Master/master/persistence /Master/master/persistence
/WebfrontCore/wwwroot/fonts
/WebfrontCore/wwwroot/font
/Plugins/Tests/TestSourceFiles
/Tests/ApplicationTests/Files/GameEvents.json
/Tests/ApplicationTests/Files/replay.json
/GameLogServer/game_log_server_env

View File

@ -1,72 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using RestEase;
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();
if (firstHeartbeat)
{
var token = await api.Authenticate(new AuthenticationId()
{
Id = mgr.GetApplicationSettings().Configuration().Id
});
api.AuthorizationToken = $"Bearer {token.AccessToken}";
}
var instance = new ApiInstance()
{
Id = mgr.GetApplicationSettings().Configuration().Id,
Uptime = (int)(DateTime.UtcNow - mgr.StartTime).TotalSeconds,
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.EndPoint,
Port = (short)s.Port,
IPAddress = s.IP
}).ToList()
};
Response<ResultMessage> response = null;
if (firstHeartbeat)
{
response = await api.AddInstance(instance);
}
else
{
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

@ -1,6 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using RestEase; using RestEase;
@ -37,16 +35,6 @@ namespace IW4MAdmin.Application.API.Master
public string Message { get; set; } public string Message { get; set; }
} }
public class Endpoint
{
#if !DEBUG
private static readonly IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
#else
private static readonly IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
#endif
public static IMasterApi Get() => api;
}
/// <summary> /// <summary>
/// Defines the capabilities of the master API /// Defines the capabilities of the master API
/// </summary> /// </summary>

View File

@ -5,12 +5,12 @@
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish> <MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId> <PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2.3.1.0</Version> <Version>2.3.2.0</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Product>IW4MAdmin</Product> <Product>IW4MAdmin</Product>
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated servers</Description> <Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated servers</Description>
<Copyright>2019</Copyright> <Copyright>2020</Copyright>
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl> <PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl>
<RepositoryUrl>https://github.com/RaidMax/IW4M-Admin</RepositoryUrl> <RepositoryUrl>https://github.com/RaidMax/IW4M-Admin</RepositoryUrl>
@ -24,22 +24,21 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0"> <PackageReference Include="Jint" Version="3.0.0-beta-1632" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" />
<PackageReference Include="RestEase" Version="1.4.10" /> <PackageReference Include="RestEase" Version="1.5.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<ServerGarbageCollection>false</ServerGarbageCollection> <ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection> <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation> <TieredCompilation>true</TieredCompilation>
<AssemblyVersion>2.3.1.0</AssemblyVersion> <LangVersion>Latest</LangVersion>
<FileVersion>2.3.1.0</FileVersion>
<LangVersion>7.1</LangVersion>
<StartupObject></StartupObject> <StartupObject></StartupObject>
</PropertyGroup> </PropertyGroup>

View File

@ -1,17 +1,19 @@
using IW4MAdmin.Application.API.Master; using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RconParsers; using IW4MAdmin.Application.RconParsers;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Commands; using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Configuration.Validation;
using SharedLibraryCore.Database; using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using SharedLibraryCore.Events;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Services; using SharedLibraryCore.Services;
using System; using System;
using System.Collections; using System.Collections;
@ -31,7 +33,7 @@ namespace IW4MAdmin.Application
private readonly ConcurrentBag<Server> _servers; private readonly ConcurrentBag<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList(); public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
public ILogger Logger => GetLogger(0); public ILogger Logger => GetLogger(0);
public bool Running { get; private set; } public bool IsRunning { get; private set; }
public bool IsInitialized { get; private set; } public bool IsInitialized { get; private set; }
public DateTime StartTime { get; private set; } public DateTime StartTime { get; private set; }
public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString(); public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString();
@ -42,40 +44,69 @@ namespace IW4MAdmin.Application
public CancellationToken CancellationToken => _tokenSource.Token; public CancellationToken CancellationToken => _tokenSource.Token;
public string ExternalIPAddress { get; private set; } public string ExternalIPAddress { get; private set; }
public bool IsRestartRequested { get; private set; } public bool IsRestartRequested { get; private set; }
public IMiddlewareActionHandler MiddlewareActionHandler { get; private set; } = new MiddlewareActionHandler(); public IMiddlewareActionHandler MiddlewareActionHandler { get; }
static ApplicationManager Instance; public event EventHandler<GameEvent> OnGameEventExecuted;
private readonly List<Command> Commands; private readonly List<IManagerCommand> _commands;
private readonly ILogger _logger;
private readonly List<MessageToken> MessageTokens; private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc; private readonly ClientService ClientSvc;
readonly AliasService AliasSvc; readonly AliasService AliasSvc;
readonly PenaltyService PenaltySvc; readonly PenaltyService PenaltySvc;
public BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
GameEventHandler Handler;
readonly IPageList PageList; readonly IPageList PageList;
readonly Dictionary<long, ILogger> Loggers = new Dictionary<long, ILogger>(); private readonly Dictionary<long, ILogger> _loggers = new Dictionary<long, ILogger>();
private readonly MetaService _metaService; private readonly IMetaService _metaService;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource; private readonly CancellationTokenSource _tokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>(); private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
private readonly IGameServerInstanceFactory _serverInstanceFactory;
private readonly IParserRegexFactory _parserRegexFactory;
private readonly IEnumerable<IRegisterEvent> _customParserEvents;
private readonly IEventHandler _eventHandler;
private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IMetaRegistration _metaRegistration;
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
private ApplicationManager() public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver)
{ {
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
Commands = new List<Command>();
MessageTokens = new List<MessageToken>(); MessageTokens = new List<MessageToken>();
ClientSvc = new ClientService(); ClientSvc = new ClientService(contextFactory);
AliasSvc = new AliasService(); AliasSvc = new AliasService();
PenaltySvc = new PenaltyService(); PenaltySvc = new PenaltyService();
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings"); ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow; StartTime = DateTime.UtcNow;
PageList = new PageList(); PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>(); AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, appConfigHandler.Configuration()) };
AdditionalRConParsers = new List<IRConParser>(); AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication(); TokenAuthenticator = new TokenAuthentication();
_metaService = new MetaService(); _logger = logger;
_metaService = metaService;
_tokenSource = new CancellationTokenSource(); _tokenSource = new CancellationTokenSource();
_loggers.Add(0, logger);
_commands = commands.ToList();
_translationLookup = translationLookup;
_commandConfiguration = commandConfiguration;
_serverInstanceFactory = serverInstanceFactory;
_parserRegexFactory = parserRegexFactory;
_customParserEvents = customParserEvents;
_eventHandler = eventHandler;
_scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver;
Plugins = plugins;
} }
public IEnumerable<IPlugin> Plugins { get; }
public async Task ExecuteEvent(GameEvent newEvent) public async Task ExecuteEvent(GameEvent newEvent)
{ {
#if DEBUG == true #if DEBUG == true
@ -141,6 +172,8 @@ namespace IW4MAdmin.Application
skip: skip:
// tell anyone waiting for the output that we're done // tell anyone waiting for the output that we're done
newEvent.Complete(); newEvent.Complete();
OnGameEventExecuted?.Invoke(this, newEvent);
#if DEBUG == true #if DEBUG == true
Logger.WriteDebug($"Exiting event process for {newEvent.Id}"); Logger.WriteDebug($"Exiting event process for {newEvent.Id}");
#endif #endif
@ -151,14 +184,9 @@ namespace IW4MAdmin.Application
return Servers; return Servers;
} }
public IList<Command> GetCommands() public IList<IManagerCommand> GetCommands()
{ {
return Commands; return _commands;
}
public static ApplicationManager GetInstance()
{
return Instance ?? (Instance = new ApplicationManager());
} }
public async Task UpdateServerStates() public async Task UpdateServerStates()
@ -241,22 +269,41 @@ namespace IW4MAdmin.Application
public async Task Init() public async Task Init()
{ {
Running = true; IsRunning = true;
ExternalIPAddress = await Utilities.GetExternalIP(); ExternalIPAddress = await Utilities.GetExternalIP();
#region PLUGINS #region PLUGINS
SharedLibraryCore.Plugins.PluginImporter.Load(this); foreach (var plugin in Plugins)
foreach (var Plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
{ {
try try
{ {
await Plugin.OnLoadAsync(this); if (plugin is ScriptPlugin scriptPlugin)
{
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
scriptPlugin.Watcher.Changed += async (sender, e) =>
{
try
{
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
}
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) catch (Exception ex)
{ {
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_PLUGIN"]} {Plugin.Name}"); Logger.WriteError($"{_translationLookup["SERVER_ERROR_PLUGIN"]} {plugin.Name}");
Logger.WriteDebug(ex.GetExceptionInfo()); Logger.WriteDebug(ex.GetExceptionInfo());
} }
} }
@ -281,7 +328,7 @@ namespace IW4MAdmin.Application
if (newConfig.Servers == null) if (newConfig.Servers == null)
{ {
ConfigHandler.Set(newConfig); ConfigHandler.Set(newConfig);
newConfig.Servers = new List<ServerConfiguration>(); newConfig.Servers = new ServerConfiguration[1];
do do
{ {
@ -296,8 +343,8 @@ namespace IW4MAdmin.Application
serverConfig.AddEventParser(parser); serverConfig.AddEventParser(parser);
} }
newConfig.Servers.Add((ServerConfiguration)serverConfig.Generate()); newConfig.Servers = newConfig.Servers.Where(_servers => _servers != null).Append((ServerConfiguration)serverConfig.Generate()).ToArray();
} while (Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["SETUP_SERVER_SAVE"])); } while (Utilities.PromptBool(_translationLookup["SETUP_SERVER_SAVE"]));
config = newConfig; config = newConfig;
await ConfigHandler.Save(); await ConfigHandler.Save();
@ -318,6 +365,18 @@ namespace IW4MAdmin.Application
await ConfigHandler.Save(); 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) foreach (var serverConfig in config.Servers)
{ {
Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig); Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
@ -340,7 +399,7 @@ namespace IW4MAdmin.Application
} }
} }
if (config.Servers.Count == 0) if (config.Servers.Length == 0)
{ {
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid"); throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
} }
@ -359,178 +418,58 @@ namespace IW4MAdmin.Application
#endregion #endregion
#region COMMANDS #region COMMANDS
if (ClientSvc.GetOwners().Result.Count == 0) if (ClientSvc.GetOwners().Result.Count > 0)
{ {
Commands.Add(new COwner()); _commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand));
} }
Commands.Add(new CQuit()); List<IManagerCommand> commandsToAddToConfig = new List<IManagerCommand>();
Commands.Add(new CRestart()); var cmdConfig = _commandConfiguration.Configuration();
Commands.Add(new CKick());
Commands.Add(new CSay());
Commands.Add(new CTempBan());
Commands.Add(new CBan());
Commands.Add(new CWhoAmI());
Commands.Add(new CList());
Commands.Add(new CHelp());
Commands.Add(new CFastRestart());
Commands.Add(new CMapRotate());
Commands.Add(new CSetLevel());
Commands.Add(new CUsage());
Commands.Add(new CUptime());
Commands.Add(new CWarn());
Commands.Add(new CWarnClear());
Commands.Add(new CUnban());
Commands.Add(new CListAdmins());
Commands.Add(new CLoadMap());
Commands.Add(new CFindPlayer());
Commands.Add(new CListRules());
Commands.Add(new CPrivateMessage());
Commands.Add(new CFlag());
Commands.Add(new CUnflag());
Commands.Add(new CReport());
Commands.Add(new CListReports());
Commands.Add(new CListBanInfo());
Commands.Add(new CListAlias());
Commands.Add(new CExecuteRCON());
Commands.Add(new CPlugins());
Commands.Add(new CIP());
Commands.Add(new CMask());
Commands.Add(new CPruneAdmins());
//Commands.Add(new CKillServer());
Commands.Add(new CSetPassword());
Commands.Add(new CPing());
Commands.Add(new CSetGravatar());
Commands.Add(new CNextMap());
Commands.Add(new RequestTokenCommand());
Commands.Add(new UnlinkClientCommand());
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands) if (cmdConfig == null)
{ {
Commands.Add(C); cmdConfig = new CommandConfiguration();
commandsToAddToConfig.AddRange(_commands);
} }
else
{
var unsavedCommands = _commands.Where(_cmd => !cmdConfig.Commands.Keys.Contains(_cmd.CommandConfigNameForType()));
commandsToAddToConfig.AddRange(unsavedCommands);
}
// this is because I want to store the command prefix in IW4MAdminSettings, but can't easily
// inject it to all the places that need it
cmdConfig.CommandPrefix = config.CommandPrefix;
cmdConfig.BroadcastCommandPrefix = config.BroadcastCommandPrefix;
foreach (var cmd in commandsToAddToConfig)
{
cmdConfig.Commands.Add(cmd.CommandConfigNameForType(),
new CommandProperties()
{
Name = cmd.Name,
Alias = cmd.Alias,
MinimumPermission = cmd.Permission,
AllowImpersonation = cmd.AllowImpersonation,
SupportedGames = cmd.SupportedGames
});
}
_commandConfiguration.Set(cmdConfig);
await _commandConfiguration.Save();
#endregion #endregion
#region META _metaRegistration.Register();
async Task<List<ProfileMeta>> getProfileMeta(int clientId, int offset, int count, DateTime? startAt)
#region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
{ {
var metaList = new List<ProfileMeta>(); foreach (var parser in AdditionalEventParsers)
// we don't want to return anything because it means we're trying to retrieve paged meta data
if (count > 1)
{ {
return metaList; parser.RegisterCustomEvent(customEvent.Item1, customEvent.Item2, customEvent.Item3);
} }
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 #endregion
await InitializeServers(); await InitializeServers();
@ -544,12 +483,10 @@ namespace IW4MAdmin.Application
async Task Init(ServerConfiguration Conf) async Task Init(ServerConfiguration Conf)
{ {
// setup the event handler after the class is initialized
Handler = new GameEventHandler(this);
try try
{ {
var ServerInstance = new IW4MServer(this, Conf); // todo: this might not always be an IW4MServer
var ServerInstance = _serverInstanceFactory.CreateServer(Conf, this) as IW4MServer;
await ServerInstance.Initialize(); await ServerInstance.Initialize();
_servers.Add(ServerInstance); _servers.Add(ServerInstance);
@ -564,7 +501,7 @@ namespace IW4MAdmin.Application
Owner = ServerInstance Owner = ServerInstance
}; };
Handler.AddEvent(e); AddEvent(e);
successServers++; successServers++;
} }
@ -588,7 +525,7 @@ namespace IW4MAdmin.Application
throw lastException; throw lastException;
} }
if (successServers != config.Servers.Count) if (successServers != config.Servers.Length)
{ {
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"])) if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
{ {
@ -597,91 +534,12 @@ namespace IW4MAdmin.Application
} }
} }
private async Task SendHeartbeat() public async Task Start() => await UpdateServerStates();
{
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() public void Stop()
{ {
_tokenSource.Cancel(); _tokenSource.Cancel();
Running = false; IsRunning = false;
Instance = null;
} }
public void Restart() public void Restart()
@ -692,25 +550,16 @@ namespace IW4MAdmin.Application
public ILogger GetLogger(long serverId) public ILogger GetLogger(long serverId)
{ {
if (Loggers.ContainsKey(serverId)) if (_loggers.ContainsKey(serverId))
{ {
return Loggers[serverId]; return _loggers[serverId];
} }
else else
{ {
Logger newLogger; var newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
if (serverId == 0) _loggers.Add(serverId, newLogger);
{
newLogger = new Logger("IW4MAdmin-Manager");
}
else
{
newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
}
Loggers.Add(serverId, newLogger);
return newLogger; return newLogger;
} }
} }
@ -746,14 +595,9 @@ namespace IW4MAdmin.Application
return ConfigHandler; return ConfigHandler;
} }
public IEventHandler GetEventHandler() public void AddEvent(GameEvent gameEvent)
{ {
return Handler; _eventHandler.HandleEvent(this, gameEvent);
}
public IList<Assembly> GetPluginAssemblies()
{
return SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies.Union(SharedLibraryCore.Plugins.PluginImporter.Assemblies).ToList();
} }
public IPageList GetPageList() public IPageList GetPageList()
@ -761,14 +605,20 @@ namespace IW4MAdmin.Application
return PageList; return PageList;
} }
public IRConParser GenerateDynamicRConParser() public IRConParser GenerateDynamicRConParser(string name)
{ {
return new DynamicRConParser(); return new DynamicRConParser(_parserRegexFactory)
{
Name = name
};
} }
public IEventParser GenerateDynamicEventParser() public IEventParser GenerateDynamicEventParser(string name)
{ {
return new DynamicEventParser(); return new DynamicEventParser(_parserRegexFactory, _logger, ConfigHandler.Configuration())
{
Name = name
};
} }
public async Task<IList<T>> ExecuteSharedDatabaseOperation<T>(string operationName) public async Task<IList<T>> ExecuteSharedDatabaseOperation<T>(string operationName)
@ -781,5 +631,17 @@ namespace IW4MAdmin.Application
{ {
_operationLookup.Add(operationName, operation); _operationLookup.Add(operationName, operation);
} }
public void AddAdditionalCommand(IManagerCommand command)
{
if (_commands.Any(_command => _command.Name == command.Name || _command.Alias == command.Alias))
{
throw new InvalidOperationException($"Duplicate command name or alias ({command.Name}, {command.Alias})");
}
_commands.Add(command);
}
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
} }
} }

View File

@ -0,0 +1,12 @@
param (
[string]$OutputDir = $(throw "-OutputDir is required.")
)
$localizations = @("en-US", "ru-RU", "es-EC", "pt-BR", "de-DE")
foreach($localization in $localizations)
{
$url = "http://api.raidmax.org:5000/localization/{0}" -f $localization
$filePath = "{0}Localization\IW4MAdmin.{1}.json" -f $OutputDir, $localization
$response = Invoke-WebRequest $url
Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8
}

View File

@ -26,6 +26,10 @@ if not exist "%PublishDir%\Lib\" md "%PublishDir%\Lib\"
move "%PublishDir%\*.dll" "%PublishDir%\Lib\" move "%PublishDir%\*.dll" "%PublishDir%\Lib\"
move "%PublishDir%\*.json" "%PublishDir%\Lib\" move "%PublishDir%\*.json" "%PublishDir%\Lib\"
move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes" move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes"
move "%PublishDir%\ru" "%PublishDir%\Lib\ru"
move "%PublishDir%\de" "%PublishDir%\Lib\de"
move "%PublishDir%\pt" "%PublishDir%\Lib\pt"
move "%PublishDir%\es" "%PublishDir%\Lib\es"
if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs" if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs"
echo making start scripts echo making start scripts

View File

@ -1,3 +1,6 @@
set SolutionDir=%1 set SolutionDir=%1
set ProjectDir=%2 set ProjectDir=%2
set TargetDir=%3 set TargetDir=%3
echo D | xcopy "%SolutionDir%Plugins\ScriptPlugins\*.js" "%TargetDir%Plugins" /y
powershell -File "%ProjectDir%BuildScripts\DownloadTranslations.ps1" %TargetDir%

View File

@ -1,42 +1,51 @@
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.EventParsers namespace IW4MAdmin.Application.EventParsers
{ {
class BaseEventParser : IEventParser public class BaseEventParser : IEventParser
{ {
public BaseEventParser() private readonly Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)> _customEventRegistrations;
private readonly ILogger _logger;
private readonly ApplicationConfiguration _appConfig;
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig)
{ {
Configuration = new DynamicEventParserConfiguration() _customEventRegistrations = new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
_logger = logger;
_appConfig = appConfig;
Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
{ {
GameDirectory = "main", GameDirectory = "main",
}; };
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32});([0-9]+);(.+);(.*)$"; Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);([^;]*);(.*)$";
Configuration.Say.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Say.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2); Configuration.Say.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3); Configuration.Say.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginName, 4); Configuration.Say.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Say.AddMapping(ParserRegex.GroupType.Message, 5); Configuration.Say.AddMapping(ParserRegex.GroupType.Message, 5);
Configuration.Quit.Pattern = @"^(Q);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);([0-9]+);(.*)$"; Configuration.Quit.Pattern = @"^(Q);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(.*)$";
Configuration.Quit.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Quit.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2); Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3); Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginName, 4); Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Join.Pattern = @"^(J);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);([0-9]+);(.*)$"; Configuration.Join.Pattern = @"^(J);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);([0-9]+);(.*)$";
Configuration.Join.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Join.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2); Configuration.Join.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3); Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4); Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world)?;(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world)?;(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$"; Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2); Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3); Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -51,7 +60,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12); Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13); Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world)?;(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world)?;(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$"; Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world|none)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1); Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2); Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3); Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -65,6 +74,8 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Kill.AddMapping(ParserRegex.GroupType.Damage, 11); Configuration.Kill.AddMapping(ParserRegex.GroupType.Damage, 11);
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12); Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13); Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Time.Pattern = @"^ *(([0-9]+):([0-9]+) |^[0-9]+ )";
} }
public IEventParserConfiguration Configuration { get; set; } public IEventParserConfiguration Configuration { get; set; }
@ -75,38 +86,62 @@ namespace IW4MAdmin.Application.EventParsers
public string URLProtocolFormat { get; set; } = "CoD://{{ip}}:{{port}}"; public string URLProtocolFormat { get; set; } = "CoD://{{ip}}:{{port}}";
public string Name { get; set; } = "Call of Duty";
public virtual GameEvent GenerateGameEvent(string logLine) public virtual GameEvent GenerateGameEvent(string logLine)
{ {
logLine = Regex.Replace(logLine, @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim(); var timeMatch = Configuration.Time.PatternMatcher.Match(logLine);
int gameTime = 0;
if (timeMatch.Success)
{
gameTime = timeMatch
.Values
.Skip(2)
// this converts the timestamp into seconds passed
.Select((_value, index) => int.Parse(_value.ToString()) * (index == 0 ? 60 : 1))
.Sum();
// we want to strip the time from the log line
logLine = logLine.Substring(timeMatch.Values.First().Length);
}
string[] lineSplit = logLine.Split(';'); string[] lineSplit = logLine.Split(';');
string eventType = lineSplit[0]; string eventType = lineSplit[0];
if (eventType == "say" || eventType == "sayteam") if (eventType == "say" || eventType == "sayteam")
{ {
var matchResult = Regex.Match(logLine, Configuration.Say.Pattern); var matchResult = Configuration.Say.PatternMatcher.Match(logLine);
if (matchResult.Success) if (matchResult.Success)
{ {
string message = matchResult string message = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.ToString() .ToString()
.Replace("\x15", "") .Replace("\x15", "")
.Trim(); .Trim();
if (message.Length > 0) if (message.Length > 0)
{ {
long originId = matchResult.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(); string originIdString = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
if (message[0] == '!' || message[0] == '@') long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
{ {
return new GameEvent() return new GameEvent()
{ {
Type = GameEvent.EventType.Command, Type = GameEvent.EventType.Command,
Data = message, Data = message,
Origin = new EFClient() { NetworkId = originId }, Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
Message = message, Message = message,
Extra = logLine, Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
}; };
} }
@ -114,10 +149,12 @@ namespace IW4MAdmin.Application.EventParsers
{ {
Type = GameEvent.EventType.Say, Type = GameEvent.EventType.Say,
Data = message, Data = message,
Origin = new EFClient() { NetworkId = originId }, Origin = new EFClient() { NetworkId = originId, ClientNumber = clientNumber },
Message = message, Message = message,
Extra = logLine, Extra = logLine,
RequiredEntity = GameEvent.EventRequiredEntity.Origin RequiredEntity = GameEvent.EventRequiredEntity.Origin,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
}; };
} }
} }
@ -125,50 +162,85 @@ namespace IW4MAdmin.Application.EventParsers
if (eventType == "K") if (eventType == "K")
{ {
var match = Regex.Match(logLine, Configuration.Kill.Pattern); var match = Configuration.Kill.PatternMatcher.Match(logLine);
if (match.Success) if (match.Success)
{ {
long originId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].Value.ToString().ConvertGuidToLong(1); string originIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
long targetId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].Value.ToString().ConvertGuidToLong(1); string targetIdString = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
string originName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
string targetName = match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
long targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
int originClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent() return new GameEvent()
{ {
Type = GameEvent.EventType.Kill, Type = GameEvent.EventType.Kill,
Data = logLine, Data = logLine,
Origin = new EFClient() { NetworkId = originId }, Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient() { NetworkId = targetId }, Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
}; };
} }
} }
if (eventType == "D") if (eventType == "D")
{ {
var regexMatch = Regex.Match(logLine, Configuration.Damage.Pattern); var match = Configuration.Damage.PatternMatcher.Match(logLine);
if (regexMatch.Success) if (match.Success)
{ {
long originId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(1); string originIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
long targetId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertGuidToLong(1); string targetIdString = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
string originName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
string targetName = match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetName]].ToString();
long originId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
long targetId = targetIdString.IsBotGuid() ?
targetName.GenerateGuidFromString() :
targetIdString.ConvertGuidToLong(Configuration.GuidNumberStyle, Utilities.WORLD_ID);
int originClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
int targetClientNumber = int.Parse(match.Values[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetClientNumber]]);
return new GameEvent() return new GameEvent()
{ {
Type = GameEvent.EventType.Damage, Type = GameEvent.EventType.Damage,
Data = logLine, Data = logLine,
Origin = new EFClient() { NetworkId = originId }, Origin = new EFClient() { NetworkId = originId, ClientNumber = originClientNumber },
Target = new EFClient() { NetworkId = targetId }, Target = new EFClient() { NetworkId = targetId, ClientNumber = targetClientNumber },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
}; };
} }
} }
if (eventType == "J") if (eventType == "J")
{ {
var regexMatch = Regex.Match(logLine, Configuration.Join.Pattern); var match = Configuration.Join.PatternMatcher.Match(logLine);
if (regexMatch.Success) if (match.Success)
{ {
string originIdString = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
long networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new GameEvent() return new GameEvent()
{ {
Type = GameEvent.EventType.PreConnect, Type = GameEvent.EventType.PreConnect,
@ -177,23 +249,34 @@ namespace IW4MAdmin.Application.EventParsers
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias()
{ {
Name = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString(), Name = match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine(),
}, },
NetworkId = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(), NetworkId = networkId,
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()), ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting, State = EFClient.ClientState.Connecting,
}, },
Extra = originIdString,
RequiredEntity = GameEvent.EventRequiredEntity.None, RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
}; };
} }
} }
if (eventType == "Q") if (eventType == "Q")
{ {
var regexMatch = Regex.Match(logLine, Configuration.Quit.Pattern); var match = Configuration.Quit.PatternMatcher.Match(logLine);
if (regexMatch.Success)
if (match.Success)
{ {
string originIdString = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string originName = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString();
long networkId = originIdString.IsBotGuid() ?
originName.GenerateGuidFromString() :
originIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
return new GameEvent() return new GameEvent()
{ {
Type = GameEvent.EventType.PreDisconnect, Type = GameEvent.EventType.PreDisconnect,
@ -202,14 +285,16 @@ namespace IW4MAdmin.Application.EventParsers
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias()
{ {
Name = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString() Name = match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().TrimNewLine()
}, },
NetworkId = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(), NetworkId = networkId,
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()), ClientNumber = Convert.ToInt32(match.Values[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Disconnecting State = EFClient.ClientState.Disconnecting
}, },
RequiredEntity = GameEvent.EventRequiredEntity.None, RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true IsBlocking = true,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
}; };
} }
} }
@ -222,7 +307,9 @@ namespace IW4MAdmin.Application.EventParsers
Data = logLine, Data = logLine,
Origin = Utilities.IW4MAdminClient(), Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
}; };
} }
@ -237,53 +324,32 @@ namespace IW4MAdmin.Application.EventParsers
Origin = Utilities.IW4MAdminClient(), Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(),
Extra = dump.DictionaryFromKeyValue(), Extra = dump.DictionaryFromKeyValue(),
RequiredEntity = GameEvent.EventRequiredEntity.None RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
}; };
} }
// this is a custom event printed out by _customcallbacks.gsc (used for team balance) if (_customEventRegistrations.ContainsKey(eventType))
if (eventType == "JoinTeam")
{ {
return new GameEvent() var eventModifier = _customEventRegistrations[eventType];
try
{ {
Type = GameEvent.EventType.JoinTeam, return eventModifier.Item2(logLine, Configuration, new GameEvent()
Data = logLine, {
Origin = new EFClient() { NetworkId = lineSplit[1].ConvertGuidToLong() }, Type = GameEvent.EventType.Other,
RequiredEntity = GameEvent.EventRequiredEntity.Target Data = logLine,
}; Subtype = eventModifier.Item1,
} GameTime = gameTime,
Source = GameEvent.EventSource.Log
});
}
// this is a custom event printed out by _customcallbacks.gsc (used for anticheat) catch (Exception e)
if (eventType == "ScriptKill")
{
long originId = lineSplit[1].ConvertGuidToLong(1);
long targetId = lineSplit[2].ConvertGuidToLong(1);
return new GameEvent()
{ {
Type = GameEvent.EventType.ScriptKill, _logger.WriteWarning($"Could not handle custom event generation - {e.GetExceptionInfo()}");
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(1);
long targetId = lineSplit[2].ConvertGuidToLong(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() return new GameEvent()
@ -292,8 +358,36 @@ namespace IW4MAdmin.Application.EventParsers
Data = logLine, Data = logLine,
Origin = Utilities.IW4MAdminClient(), Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(), Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None RequiredEntity = GameEvent.EventRequiredEntity.None,
GameTime = gameTime,
Source = GameEvent.EventSource.Log
}; };
} }
/// <inheritdoc/>
public void RegisterCustomEvent(string eventSubtype, string eventTriggerValue, Func<string, IEventParserConfiguration, GameEvent, GameEvent> eventModifier)
{
if (string.IsNullOrWhiteSpace(eventSubtype))
{
throw new ArgumentException("Event subtype cannot be empty");
}
if (string.IsNullOrWhiteSpace(eventTriggerValue))
{
throw new ArgumentException("Event trigger value cannot be empty");
}
if (eventModifier == null)
{
throw new ArgumentException("Event modifier must be specified");
}
if (_customEventRegistrations.ContainsKey(eventTriggerValue))
{
throw new ArgumentException($"Event trigger value '{eventTriggerValue}' is already registered");
}
_customEventRegistrations.Add(eventTriggerValue, (eventSubtype, eventModifier));
}
} }
} }

View File

@ -1,7 +1,5 @@
using System; using SharedLibraryCore.Configuration;
using System.Collections.Generic; using SharedLibraryCore.Interfaces;
using System.Text;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.EventParsers namespace IW4MAdmin.Application.EventParsers
{ {
@ -11,5 +9,8 @@ namespace IW4MAdmin.Application.EventParsers
/// </summary> /// </summary>
sealed internal class DynamicEventParser : BaseEventParser sealed internal class DynamicEventParser : BaseEventParser
{ {
public DynamicEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig) : base(parserRegexFactory, logger, appConfig)
{
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,19 @@
using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.Misc;
using Newtonsoft.Json;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Events; using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
class GameEventHandler : IEventHandler public class GameEventHandler : IEventHandler
{ {
readonly ApplicationManager Manager; private readonly EventLog _eventLog;
private readonly EventProfiler _profiler;
private delegate void GameEventAddedEventHandler(object sender, GameEventArgs args);
private event GameEventAddedEventHandler GameEventAdded;
private static readonly GameEvent.EventType[] overrideEvents = new[] private static readonly GameEvent.EventType[] overrideEvents = new[]
{ {
GameEvent.EventType.Connect, GameEvent.EventType.Connect,
@ -23,24 +22,12 @@ namespace IW4MAdmin.Application
GameEvent.EventType.Stop GameEvent.EventType.Stop
}; };
public GameEventHandler(IManager mgr) public GameEventHandler()
{ {
Manager = (ApplicationManager)mgr; _eventLog = new EventLog();
_profiler = new EventProfiler(mgr.GetLogger(0));
GameEventAdded += GameEventHandler_GameEventAdded;
} }
private async void GameEventHandler_GameEventAdded(object sender, GameEventArgs args) public void HandleEvent(IManager manager, GameEvent gameEvent)
{
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 #if DEBUG
ThreadPool.GetMaxThreads(out int workerThreads, out int n); ThreadPool.GetMaxThreads(out int workerThreads, out int n);
@ -48,12 +35,14 @@ namespace IW4MAdmin.Application
gameEvent.Owner.Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks"); gameEvent.Owner.Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif #endif
if (Manager.Running || overrideEvents.Contains(gameEvent.Type)) if (manager.IsRunning || overrideEvents.Contains(gameEvent.Type))
{ {
#if DEBUG #if DEBUG
gameEvent.Owner.Logger.WriteDebug($"Adding event with id {gameEvent.Id}"); gameEvent.Owner.Logger.WriteDebug($"Adding event with id {gameEvent.Id}");
#endif #endif
GameEventAdded?.Invoke(this, new GameEventArgs(null, false, gameEvent));
EventApi.OnGameEvent(gameEvent);
Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent));
} }
#if DEBUG #if DEBUG
else else

View File

@ -6,26 +6,18 @@ using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO namespace IW4MAdmin.Application.IO
{ {
class GameLogEventDetection public class GameLogEventDetection
{ {
private long previousFileSize; private long previousFileSize;
private readonly Server _server; private readonly Server _server;
private readonly IGameLogReader _reader; private readonly IGameLogReader _reader;
private readonly string _gameLogFile;
private readonly bool _ignoreBots; private readonly bool _ignoreBots;
class EventState public GameLogEventDetection(Server server, Uri[] gameLogUris, IGameLogReaderFactory gameLogReaderFactory)
{ {
public ILogger Log { get; set; } _reader = gameLogReaderFactory.CreateGameLogReader(gameLogUris, server.EventParser);
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; _server = server;
_ignoreBots = server.Manager.GetApplicationSettings().Configuration().IgnoreBots; _ignoreBots = server?.Manager.GetApplicationSettings().Configuration().IgnoreBots ?? false;
} }
public async Task PollForChanges() public async Task PollForChanges()
@ -52,7 +44,7 @@ namespace IW4MAdmin.Application.IO
_server.Logger.WriteDebug("Stopped polling for changes"); _server.Logger.WriteDebug("Stopped polling for changes");
} }
private async Task UpdateLogEvents() public async Task UpdateLogEvents()
{ {
long fileSize = _reader.Length; long fileSize = _reader.Length;
@ -65,9 +57,12 @@ namespace IW4MAdmin.Application.IO
// this makes the http log get pulled // this makes the http log get pulled
if (fileDiff < 1 && fileSize != -1) if (fileDiff < 1 && fileSize != -1)
{
previousFileSize = fileSize;
return; return;
}
var events = await _reader.ReadEventsFromLog(_server, fileDiff, previousFileSize); var events = await _reader.ReadEventsFromLog(fileDiff, previousFileSize);
foreach (var gameEvent in events) foreach (var gameEvent in events)
{ {
@ -81,9 +76,9 @@ namespace IW4MAdmin.Application.IO
// we don't want to add the event if ignoreBots is on and the event comes from a bot // we don't want to add the event if ignoreBots is on and the event comes from a bot
if (!_ignoreBots || (_ignoreBots && !((gameEvent.Origin?.IsBot ?? false) || (gameEvent.Target?.IsBot ?? false)))) if (!_ignoreBots || (_ignoreBots && !((gameEvent.Origin?.IsBot ?? false) || (gameEvent.Target?.IsBot ?? false))))
{ {
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != 1) if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != Utilities.WORLD_ID)
{ {
gameEvent.Origin = _server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId); gameEvent.Origin = _server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId);;
} }
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target) if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target)
@ -101,7 +96,7 @@ namespace IW4MAdmin.Application.IO
gameEvent.Target.CurrentServer = _server; gameEvent.Target.CurrentServer = _server;
} }
_server.Manager.GetEventHandler().AddEvent(gameEvent); _server.Manager.AddEvent(gameEvent);
} }
} }

View File

@ -13,18 +13,20 @@ namespace IW4MAdmin.Application.IO
{ {
private readonly IEventParser _parser; private readonly IEventParser _parser;
private readonly string _logFile; private readonly string _logFile;
private readonly ILogger _logger;
public long Length => new FileInfo(_logFile).Length; public long Length => new FileInfo(_logFile).Length;
public int UpdateInterval => 300; public int UpdateInterval => 300;
public GameLogReader(string logFile, IEventParser parser) public GameLogReader(string logFile, IEventParser parser, ILogger logger)
{ {
_logFile = logFile; _logFile = logFile;
_parser = parser; _parser = parser;
_logger = logger;
} }
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition) public async Task<IEnumerable<GameEvent>> ReadEventsFromLog(long fileSizeDiff, long startPosition)
{ {
// allocate the bytes for the new log lines // allocate the bytes for the new log lines
List<string> logLines = new List<string>(); List<string> logLines = new List<string>();
@ -34,7 +36,7 @@ namespace IW4MAdmin.Application.IO
{ {
byte[] buff = new byte[fileSizeDiff]; byte[] buff = new byte[fileSizeDiff];
fs.Seek(startPosition, SeekOrigin.Begin); fs.Seek(startPosition, SeekOrigin.Begin);
await fs.ReadAsync(buff, 0, (int)fileSizeDiff, server.Manager.CancellationToken); await fs.ReadAsync(buff, 0, (int)fileSizeDiff);
var stringBuilder = new StringBuilder(); var stringBuilder = new StringBuilder();
char[] charBuff = Utilities.EncodingType.GetChars(buff); char[] charBuff = Utilities.EncodingType.GetChars(buff);
@ -71,9 +73,9 @@ namespace IW4MAdmin.Application.IO
catch (Exception e) catch (Exception e)
{ {
server.Logger.WriteWarning("Could not properly parse event line"); _logger.WriteWarning("Could not properly parse event line");
server.Logger.WriteDebug(e.Message); _logger.WriteDebug(e.Message);
server.Logger.WriteDebug(eventLine); _logger.WriteDebug(eventLine);
} }
} }

View File

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

View File

@ -1,6 +1,5 @@
using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.IO;
using IW4MAdmin.Application.IO; using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RconParsers;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
@ -8,12 +7,12 @@ using SharedLibraryCore.Dtos;
using SharedLibraryCore.Exceptions; using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Services;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -23,14 +22,20 @@ namespace IW4MAdmin
{ {
public class IW4MServer : Server public class IW4MServer : Server
{ {
private static readonly SharedLibraryCore.Localization.Index loc = Utilities.CurrentLocalization.LocalizationIndex; private static readonly SharedLibraryCore.Localization.TranslationLookup loc = Utilities.CurrentLocalization.LocalizationIndex;
private GameLogEventDetection LogEvent; public GameLogEventDetection LogEvent;
private readonly ITranslationLookup _translationLookup;
private readonly IMetaService _metaService;
private const int REPORT_FLAG_COUNT = 4; private const int REPORT_FLAG_COUNT = 4;
private int lastGameTime = 0;
public int Id { get; private set; } public int Id { get; private set; }
public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg) public IW4MServer(IManager mgr, ServerConfiguration cfg, ITranslationLookup lookup,
IRConConnectionFactory connectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService) : base(cfg, mgr, connectionFactory, gameLogReaderFactory)
{ {
_translationLookup = lookup;
_metaService = metaService;
} }
override public async Task<EFClient> OnClientConnected(EFClient clientFromLog) override public async Task<EFClient> OnClientConnected(EFClient clientFromLog)
@ -61,6 +66,7 @@ namespace IW4MAdmin
client.Score = clientFromLog.Score; client.Score = clientFromLog.Score;
client.Ping = clientFromLog.Ping; client.Ping = clientFromLog.Ping;
client.CurrentServer = this; client.CurrentServer = this;
client.State = ClientState.Connecting;
Clients[client.ClientNumber] = client; Clients[client.ClientNumber] = client;
#if DEBUG == true #if DEBUG == true
@ -73,7 +79,7 @@ namespace IW4MAdmin
Type = GameEvent.EventType.Connect Type = GameEvent.EventType.Connect
}; };
Manager.GetEventHandler().AddEvent(e); Manager.AddEvent(e);
return client; return client;
} }
@ -100,7 +106,7 @@ namespace IW4MAdmin
Type = GameEvent.EventType.Disconnect Type = GameEvent.EventType.Disconnect
}; };
Manager.GetEventHandler().AddEvent(e); Manager.AddEvent(e);
#if DEBUG == true #if DEBUG == true
} }
#endif #endif
@ -134,12 +140,13 @@ namespace IW4MAdmin
{ {
try try
{ {
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E); C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration());
} }
catch (CommandException e) catch (CommandException e)
{ {
Logger.WriteInfo(e.Message); Logger.WriteInfo(e.Message);
E.FailReason = GameEvent.EventFailReason.Invalid;
} }
if (C != null) if (C != null)
@ -148,35 +155,66 @@ namespace IW4MAdmin
} }
} }
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins) try
{ {
try var loginPlugin = Manager.Plugins.FirstOrDefault(_plugin => _plugin.Name == "Login");
if (loginPlugin != null)
{ {
await plugin.OnEventAsync(E, this); await loginPlugin.OnEventAsync(E, this);
}
catch (AuthorizationException e)
{
E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
canExecuteCommand = false;
}
catch (Exception Except)
{
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{plugin.Name}]");
Logger.WriteDebug(Except.GetExceptionInfo());
} }
} }
catch (AuthorizationException e)
{
E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
canExecuteCommand = false;
}
// hack: this prevents commands from getting executing that 'shouldn't' be // hack: this prevents commands from getting executing that 'shouldn't' be
if (E.Type == GameEvent.EventType.Command && E.Extra is Command command && if (E.Type == GameEvent.EventType.Command && E.Extra is Command command &&
(canExecuteCommand || E.Origin?.Level == Permission.Console)) (canExecuteCommand || E.Origin?.Level == Permission.Console))
{ {
await command.ExecuteAsync(E); await command.ExecuteAsync(E);
} }
var pluginTasks = Manager.Plugins.Where(_plugin => _plugin.Name != "Login").Select(async _plugin =>
{
try
{
// we don't want to run the events on parser plugins
if (_plugin is ScriptPlugin scriptPlugin && scriptPlugin.IsParser)
{
return;
}
using (var tokenSource = new CancellationTokenSource())
{
tokenSource.CancelAfter(Utilities.DefaultCommandTimeout);
await (_plugin.OnEventAsync(E, this)).WithWaitCancellation(tokenSource.Token);
}
}
catch (Exception Except)
{
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{_plugin.Name}]");
Logger.WriteDebug(Except.GetExceptionInfo());
}
}).ToArray();
if (pluginTasks.Any())
{
await Task.WhenAny(pluginTasks);
}
} }
catch (Exception e) catch (Exception e)
{ {
lastException = e; lastException = e;
if (E.Origin != null && E.Type == GameEvent.EventType.Command)
{
E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
}
} }
finally finally
@ -188,8 +226,11 @@ namespace IW4MAdmin
if (lastException != null) if (lastException != null)
{ {
Logger.WriteDebug("Last Exception is not null"); bool notifyDisconnects = !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost;
throw lastException; if (notifyDisconnects || (!notifyDisconnects && lastException as NetworkException == null))
{
throw lastException;
}
} }
} }
} }
@ -208,10 +249,13 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.ConnectionLost) if (E.Type == GameEvent.EventType.ConnectionLost)
{ {
var exception = E.Extra as Exception; var exception = E.Extra as Exception;
Logger.WriteError(exception.Message); if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
if (exception.Data["internal_exception"] != null)
{ {
Logger.WriteDebug($"Internal Exception: {exception.Data["internal_exception"]}"); Logger.WriteError(exception.Message);
if (exception.Data["internal_exception"] != null)
{
Logger.WriteDebug($"Internal Exception: {exception.Data["internal_exception"]}");
}
} }
Logger.WriteInfo("Connection lost to server, so we are throttling the poll rate"); Logger.WriteInfo("Connection lost to server, so we are throttling the poll rate");
Throttled = true; Throttled = true;
@ -219,7 +263,7 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.ConnectionRestored) if (E.Type == GameEvent.EventType.ConnectionRestored)
{ {
if (Throttled) if (Throttled && !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{ {
Logger.WriteVerbose(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]")); Logger.WriteVerbose(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
} }
@ -261,6 +305,12 @@ namespace IW4MAdmin
return false; return false;
} }
if (E.Origin.CurrentServer == null)
{
Logger.WriteWarning($"preconnecting client {E.Origin} did not have a current server specified");
E.Origin.CurrentServer = this;
}
var existingClient = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin)); var existingClient = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
// they're already connected // they're already connected
@ -272,7 +322,9 @@ namespace IW4MAdmin
// this happens for some reason rarely where the client spots get out of order // this happens for some reason rarely where the client spots get out of order
// possible a connect/reconnect game event before we get to process it here // possible a connect/reconnect game event before we get to process it here
else if (existingClient != null && existingClient.ClientNumber != E.Origin.ClientNumber) // it appears that new games decide to switch client slots between maps (even if the clients aren't disconnecting)
// bots can have duplicate names which causes conflicting GUIDs
else if (existingClient != null && existingClient.ClientNumber != E.Origin.ClientNumber && !E.Origin.IsBot)
{ {
Logger.WriteWarning($"client {E.Origin} is trying to connect in client slot {E.Origin.ClientNumber}, but they are already registed in client slot {existingClient.ClientNumber}, swapping..."); Logger.WriteWarning($"client {E.Origin} is trying to connect in client slot {E.Origin.ClientNumber}, but they are already registed in client slot {existingClient.ClientNumber}, swapping...");
// we need to remove them so the client spots can swap // we need to remove them so the client spots can swap
@ -301,7 +353,7 @@ namespace IW4MAdmin
return false; return false;
} }
if (E.Origin.Level > EFClient.Permission.Moderator) if (E.Origin.Level > Permission.Moderator)
{ {
E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count)); E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
} }
@ -330,7 +382,7 @@ namespace IW4MAdmin
Expires = expires, Expires = expires,
Offender = E.Target, Offender = E.Target,
Offense = E.Data, Offense = E.Data,
Punisher = E.Origin, Punisher = E.ImpersonationOrigin ?? E.Origin,
When = DateTime.UtcNow, When = DateTime.UtcNow,
Link = E.Target.AliasLink Link = E.Target.AliasLink
}; };
@ -347,7 +399,7 @@ namespace IW4MAdmin
Expires = DateTime.UtcNow, Expires = DateTime.UtcNow,
Offender = E.Target, Offender = E.Target,
Offense = E.Data, Offense = E.Data,
Punisher = E.Origin, Punisher = E.ImpersonationOrigin ?? E.Origin,
When = DateTime.UtcNow, When = DateTime.UtcNow,
Link = E.Target.AliasLink Link = E.Target.AliasLink
}; };
@ -372,7 +424,7 @@ namespace IW4MAdmin
Expires = DateTime.UtcNow, Expires = DateTime.UtcNow,
Offender = E.Target, Offender = E.Target,
Offense = E.Message, Offense = E.Message,
Punisher = E.Origin, Punisher = E.ImpersonationOrigin ?? E.Origin,
Active = true, Active = true,
When = DateTime.UtcNow, When = DateTime.UtcNow,
Link = E.Target.AliasLink Link = E.Target.AliasLink
@ -391,28 +443,28 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.TempBan) else if (E.Type == GameEvent.EventType.TempBan)
{ {
await TempBan(E.Data, (TimeSpan)E.Extra, E.Target, E.Origin); ; await TempBan(E.Data, (TimeSpan)E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin); ;
} }
else if (E.Type == GameEvent.EventType.Ban) else if (E.Type == GameEvent.EventType.Ban)
{ {
bool isEvade = E.Extra != null ? (bool)E.Extra : false; bool isEvade = E.Extra != null ? (bool)E.Extra : false;
await Ban(E.Data, E.Target, E.Origin, isEvade); await Ban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin, isEvade);
} }
else if (E.Type == GameEvent.EventType.Unban) else if (E.Type == GameEvent.EventType.Unban)
{ {
await Unban(E.Data, E.Target, E.Origin); await Unban(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin);
} }
else if (E.Type == GameEvent.EventType.Kick) else if (E.Type == GameEvent.EventType.Kick)
{ {
await Kick(E.Data, E.Target, E.Origin); await Kick(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin);
} }
else if (E.Type == GameEvent.EventType.Warn) else if (E.Type == GameEvent.EventType.Warn)
{ {
await Warn(E.Data, E.Target, E.Origin); await Warn(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin);
} }
else if (E.Type == GameEvent.EventType.Disconnect) else if (E.Type == GameEvent.EventType.Disconnect)
@ -424,12 +476,20 @@ namespace IW4MAdmin
Time = DateTime.UtcNow Time = DateTime.UtcNow
}); });
await new MetaService().AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin); await _metaService.AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin);
await new MetaService().AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin); await _metaService.AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin);
} }
else if (E.Type == GameEvent.EventType.PreDisconnect) else if (E.Type == GameEvent.EventType.PreDisconnect)
{ {
bool isPotentialFalseQuit = E.GameTime.HasValue && E.GameTime.Value == lastGameTime;
if (isPotentialFalseQuit)
{
Logger.WriteInfo($"Receive predisconnect event for {E.Origin}, but it occured at game time {E.GameTime.Value}, which is the same last map change, so we're ignoring");
return false;
}
// predisconnect comes from minimal rcon polled players and minimal log players // predisconnect comes from minimal rcon polled players and minimal log players
// so we need to disconnect the "full" version of the client // so we need to disconnect the "full" version of the client
var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin)); var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
@ -440,7 +500,7 @@ namespace IW4MAdmin
return false; return false;
} }
else if (client.State == ClientState.Connected) else if (client.State != ClientState.Unknown)
{ {
#if DEBUG == true #if DEBUG == true
Logger.WriteDebug($"Begin PreDisconnect for {client}"); Logger.WriteDebug($"Begin PreDisconnect for {client}");
@ -481,14 +541,18 @@ namespace IW4MAdmin
.First(_qm => _qm.Game == GameName) .First(_qm => _qm.Game == GameName)
.Messages[E.Data.Substring(1)]; .Messages[E.Data.Substring(1)];
} }
catch { } catch
{
message = E.Data.Substring(1);
}
} }
ChatHistory.Add(new ChatInfo() ChatHistory.Add(new ChatInfo()
{ {
Name = E.Origin.Name, Name = E.Origin.Name,
Message = message, Message = message,
Time = DateTime.UtcNow Time = DateTime.UtcNow,
IsHidden = !string.IsNullOrEmpty(GamePassword)
}); });
} }
} }
@ -527,11 +591,21 @@ namespace IW4MAdmin
string mapname = dict["mapname"]; string mapname = dict["mapname"];
UpdateMap(mapname); UpdateMap(mapname);
} }
if (E.GameTime.HasValue)
{
lastGameTime = E.GameTime.Value;
}
} }
if (E.Type == GameEvent.EventType.MapEnd) if (E.Type == GameEvent.EventType.MapEnd)
{ {
Logger.WriteInfo("Game ending..."); Logger.WriteInfo("Game ending...");
if (E.GameTime.HasValue)
{
lastGameTime = E.GameTime.Value;
}
} }
if (E.Type == GameEvent.EventType.Tell) if (E.Type == GameEvent.EventType.Tell)
@ -541,13 +615,10 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.Broadcast) if (E.Type == GameEvent.EventType.Broadcast)
{ {
#if DEBUG == false if (!Utilities.IsDevelopment && E.Data != null) // hides broadcast when in development mode
// this is a little ugly but I don't want to change the abstract class
if (E.Data != null)
{ {
await E.Owner.ExecuteCommandAsync(E.Data); await E.Owner.ExecuteCommandAsync(E.Data);
} }
#endif
} }
lock (ChatHistory) lock (ChatHistory)
@ -597,6 +668,13 @@ namespace IW4MAdmin
Logger.WriteDebug(e.GetExceptionInfo()); Logger.WriteDebug(e.GetExceptionInfo());
} }
} }
else if ((client.IPAddress != null && client.State == ClientState.Disconnecting) ||
client.Level == Permission.Banned)
{
Logger.WriteWarning($"{client} state is Unknown (probably kicked), but they are still connected. trying to kick again...");
await client.CanConnect(client.IPAddress);
}
} }
/// <summary> /// <summary>
@ -627,6 +705,7 @@ namespace IW4MAdmin
var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients); var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients);
UpdateMap(statusResponse.Item2); UpdateMap(statusResponse.Item2);
UpdateGametype(statusResponse.Item3);
return new List<EFClient>[] return new List<EFClient>[]
{ {
@ -648,6 +727,14 @@ namespace IW4MAdmin
} }
} }
private void UpdateGametype(string gameType)
{
if (!string.IsNullOrEmpty(gameType))
{
Gametype = gameType;
}
}
private async Task ShutdownInternal() private async Task ShutdownInternal()
{ {
foreach (var client in GetClientsAsList()) foreach (var client in GetClientsAsList())
@ -661,12 +748,12 @@ namespace IW4MAdmin
Origin = client Origin = client
}; };
Manager.GetEventHandler().AddEvent(e); Manager.AddEvent(e);
await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token); await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token);
} }
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins) foreach (var plugin in Manager.Plugins)
{ {
await plugin.OnUnloadAsync(); await plugin.OnUnloadAsync();
} }
@ -678,6 +765,7 @@ namespace IW4MAdmin
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts) override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
{ {
bool notifyDisconnects = !Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost;
try try
{ {
if (cts.IsCancellationRequested) if (cts.IsCancellationRequested)
@ -697,21 +785,18 @@ namespace IW4MAdmin
var polledClients = await PollPlayersAsync(); var polledClients = await PollPlayersAsync();
foreach (var disconnectingClient in polledClients[1]) foreach (var disconnectingClient in polledClients[1].Where(_client => !_client.IsZombieClient /* ignores "fake" zombie clients */))
{ {
if (disconnectingClient.State == ClientState.Disconnecting) disconnectingClient.CurrentServer = this;
{
continue;
}
var e = new GameEvent() var e = new GameEvent()
{ {
Type = GameEvent.EventType.PreDisconnect, Type = GameEvent.EventType.PreDisconnect,
Origin = disconnectingClient, Origin = disconnectingClient,
Owner = this Owner = this,
Source = GameEvent.EventSource.Status
}; };
Manager.GetEventHandler().AddEvent(e); Manager.AddEvent(e);
await e.WaitAsync(Utilities.DefaultCommandTimeout, Manager.CancellationToken); await e.WaitAsync(Utilities.DefaultCommandTimeout, Manager.CancellationToken);
} }
@ -724,21 +809,25 @@ namespace IW4MAdmin
continue; continue;
} }
client.CurrentServer = this;
var e = new GameEvent() var e = new GameEvent()
{ {
Type = GameEvent.EventType.PreConnect, Type = GameEvent.EventType.PreConnect,
Origin = client, Origin = client,
Owner = this, Owner = this,
IsBlocking = true IsBlocking = true,
Extra = client.GetAdditionalProperty<string>("BotGuid"),
Source = GameEvent.EventSource.Status
}; };
Manager.GetEventHandler().AddEvent(e); Manager.AddEvent(e);
await e.WaitAsync(Utilities.DefaultCommandTimeout, Manager.CancellationToken); await e.WaitAsync(Utilities.DefaultCommandTimeout, Manager.CancellationToken);
} }
// these are the clients that have updated // these are the clients that have updated
foreach (var client in polledClients[2]) foreach (var client in polledClients[2])
{ {
client.CurrentServer = this;
var e = new GameEvent() var e = new GameEvent()
{ {
Type = GameEvent.EventType.Update, Type = GameEvent.EventType.Update,
@ -746,7 +835,7 @@ namespace IW4MAdmin
Owner = this Owner = this
}; };
Manager.GetEventHandler().AddEvent(e); Manager.AddEvent(e);
} }
if (ConnectionErrors > 0) if (ConnectionErrors > 0)
@ -759,7 +848,7 @@ namespace IW4MAdmin
Target = Utilities.IW4MAdminClient(this) Target = Utilities.IW4MAdminClient(this)
}; };
Manager.GetEventHandler().AddEvent(_event); Manager.AddEvent(_event);
} }
ConnectionErrors = 0; ConnectionErrors = 0;
@ -781,7 +870,7 @@ namespace IW4MAdmin
Data = ConnectionErrors.ToString() Data = ConnectionErrors.ToString()
}; };
Manager.GetEventHandler().AddEvent(_event); Manager.AddEvent(_event);
} }
return true; return true;
} }
@ -829,13 +918,15 @@ namespace IW4MAdmin
// this one is ok // this one is ok
catch (ServerException e) catch (ServerException e)
{ {
if (e is NetworkException && !Throttled) if (e is NetworkException && !Throttled && notifyDisconnects)
{ {
Logger.WriteError(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}")); Logger.WriteError(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
Logger.WriteDebug(e.GetExceptionInfo()); Logger.WriteDebug(e.GetExceptionInfo());
} }
else
Logger.WriteError(e.Message); {
Logger.WriteError(e.Message);
}
return false; return false;
} }
@ -855,12 +946,12 @@ namespace IW4MAdmin
EventParser = Manager.AdditionalEventParsers EventParser = Manager.AdditionalEventParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion); .FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion);
RconParser = RconParser ?? new BaseRConParser(); RconParser = RconParser ?? Manager.AdditionalRConParsers[0];
EventParser = EventParser ?? new BaseEventParser(); EventParser = EventParser ?? Manager.AdditionalEventParsers[0];
RemoteConnection.SetConfiguration(RconParser.Configuration); RemoteConnection.SetConfiguration(RconParser.Configuration);
var version = await this.GetDvarAsync<string>("version"); var version = await this.GetMappedDvarValueOrDefaultAsync<string>("version");
Version = version.Value; Version = version.Value;
GameName = Utilities.GetGame(version?.Value ?? RconParser.Version); GameName = Utilities.GetGame(version?.Value ?? RconParser.Version);
@ -877,42 +968,43 @@ namespace IW4MAdmin
Version = RconParser.Version; Version = RconParser.Version;
} }
var svRunning = await this.GetDvarAsync<int>("sv_running"); var svRunning = await this.GetMappedDvarValueOrDefaultAsync<string>("sv_running");
if (svRunning.Value == 0) if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1")
{ {
throw new ServerException(loc["SERVER_ERROR_NOT_RUNNING"]); throw new ServerException(loc["SERVER_ERROR_NOT_RUNNING"]);
} }
var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null; var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null;
// this is normally slow, but I'm only doing it because different games have different prefixes
var hostname = infoResponse == null ?
(await this.GetDvarAsync<string>("sv_hostname")).Value :
infoResponse.Where(kvp => kvp.Key.Contains("hostname")).Select(kvp => kvp.Value).First();
var mapname = infoResponse == null ?
(await this.GetDvarAsync<string>("mapname")).Value :
infoResponse["mapname"];
int maxplayers = (GameName == Game.IW4) ? // gotta love IW4 idiosyncrasies
(await this.GetDvarAsync<int>("party_maxplayers")).Value :
infoResponse == null ?
(await this.GetDvarAsync<int>("sv_maxclients")).Value :
Convert.ToInt32(infoResponse["sv_maxclients"]);
var gametype = infoResponse == null ?
(await this.GetDvarAsync<string>("g_gametype")).Value :
infoResponse.Where(kvp => kvp.Key.Contains("gametype")).Select(kvp => kvp.Value).First();
var basepath = await this.GetDvarAsync<string>("fs_basepath");
var game = infoResponse == null || !infoResponse.ContainsKey("fs_game") ?
(await this.GetDvarAsync<string>("fs_game")).Value :
infoResponse["fs_game"];
var logfile = await this.GetDvarAsync<string>("g_log");
var logsync = await this.GetDvarAsync<int>("g_logsync");
var ip = await this.GetDvarAsync<string>("net_ip");
WorkingDirectory = basepath.Value; string hostname = (await this.GetMappedDvarValueOrDefaultAsync<string>("sv_hostname", "hostname", infoResponse)).Value;
string mapname = (await this.GetMappedDvarValueOrDefaultAsync<string>("mapname", infoResponse: infoResponse)).Value;
int maxplayers = (await this.GetMappedDvarValueOrDefaultAsync<int>("sv_maxclients", infoResponse: infoResponse)).Value;
string gametype = (await this.GetMappedDvarValueOrDefaultAsync<string>("g_gametype", "gametype", infoResponse)).Value;
var basepath = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basepath"));
var basegame = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basegame"));
var game = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_game", infoResponse: infoResponse));
var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log");
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync");
var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip");
var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: "");
if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName)
{
await this.SetDvarAsync("sv_sayname", Manager.GetApplicationSettings().Configuration().CustomSayName);
}
try try
{ {
var website = await this.GetDvarAsync<string>("_website"); var website = await this.GetMappedDvarValueOrDefaultAsync<string>("_website");
// this occurs for games that don't give us anything back when
// the dvar is not set
if (string.IsNullOrWhiteSpace(website.Value))
{
throw new DvarException("value is empty");
}
Website = website.Value; Website = website.Value;
} }
@ -923,11 +1015,13 @@ namespace IW4MAdmin
InitializeMaps(); InitializeMaps();
WorkingDirectory = basepath.Value;
this.Hostname = hostname; this.Hostname = hostname;
this.MaxClients = maxplayers; this.MaxClients = maxplayers;
this.FSGame = game; this.FSGame = game.Value;
this.Gametype = gametype; this.Gametype = gametype;
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress; this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
this.GamePassword = gamePassword.Value;
UpdateMap(mapname); UpdateMap(mapname);
if (RconParser.CanGenerateLogPath) if (RconParser.CanGenerateLogPath)
@ -960,24 +1054,28 @@ namespace IW4MAdmin
CustomCallback = await ScriptLoaded(); CustomCallback = await ScriptLoaded();
// they've manually specified the log path // they've manually specified the log path
if (!string.IsNullOrEmpty(ServerConfig.ManualLogPath)) if (!string.IsNullOrEmpty(ServerConfig.ManualLogPath) || !RconParser.CanGenerateLogPath)
{ {
LogPath = ServerConfig.ManualLogPath; LogPath = ServerConfig.ManualLogPath;
if (string.IsNullOrEmpty(LogPath) && !RconParser.CanGenerateLogPath)
{
throw new ServerException(loc["SERVER_ERROR_REQUIRES_PATH"].FormatExt(GameName.ToString()));
}
} }
else else
{ {
string mainPath = EventParser.Configuration.GameDirectory; var logInfo = new LogPathGeneratorInfo()
LogPath = string.IsNullOrEmpty(game) ?
$"{basepath?.Value?.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{mainPath}{Path.DirectorySeparatorChar}{logfile?.Value}" :
$"{basepath?.Value?.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{game?.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{logfile?.Value}";
// fix wine drive name mangling
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
LogPath = Regex.Replace($"{Path.DirectorySeparatorChar}{LogPath}", @"[A-Z]:/", ""); BaseGameDirectory = basegame.Value,
} BasePathDirectory = basepath.Value,
GameDirectory = EventParser.Configuration.GameDirectory ?? "",
ModDirectory = game.Value ?? "",
LogFile = logfile.Value,
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
};
LogPath = GenerateLogPath(logInfo);
if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null) if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null)
{ {
@ -986,167 +1084,174 @@ namespace IW4MAdmin
} }
} }
LogEvent = new GameLogEventDetection(this, LogPath, ServerConfig.GameLogServerUrl); LogEvent = new GameLogEventDetection(this, GenerateUriForLog(LogPath, ServerConfig.GameLogServerUrl?.AbsoluteUri), gameLogReaderFactory);
Logger.WriteInfo($"Log file is {LogPath}"); Logger.WriteInfo($"Log file is {LogPath}");
_ = Task.Run(() => LogEvent.PollForChanges()); _ = Task.Run(() => LogEvent.PollForChanges());
#if !DEBUG
Broadcast(loc["BROADCAST_ONLINE"]); if (!Utilities.IsDevelopment)
#endif {
Broadcast(loc["BROADCAST_ONLINE"]);
}
} }
protected override async Task Warn(String Reason, EFClient Target, EFClient Origin) public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl)
{ {
// ensure player gets warned if command not performed on them in game var logUri = new Uri(logPath);
if (Target.ClientNumber < 0)
{
var ingameClient = Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == Target.ClientId);
if (ingameClient != null) if (string.IsNullOrEmpty(gameLogServerUrl))
{ {
await Warn(Reason, ingameClient, Origin); return new[] { logUri };
return;
}
} }
else else
{ {
if (Target.Warnings >= 4) return new[] { new Uri(gameLogServerUrl), logUri };
{
Target.Kick(loc["SERVER_WARNLIMT_REACHED"], Utilities.IW4MAdminClient(this));
return;
}
string message = $"^1{loc["SERVER_WARNING"]} ^7[^3{Target.Warnings}^7]: ^3{Target.Name}^7, {Reason}";
Target.CurrentServer.Broadcast(message);
} }
}
public static string GenerateLogPath(LogPathGeneratorInfo logInfo)
{
string logPath;
string workingDirectory = logInfo.BasePathDirectory;
bool baseGameIsDirectory = !string.IsNullOrWhiteSpace(logInfo.BaseGameDirectory) &&
logInfo.BaseGameDirectory.IndexOfAny(Utilities.DirectorySeparatorChars) != -1;
bool baseGameIsRelative = logInfo.BaseGameDirectory.FixDirectoryCharacters()
.Equals(logInfo.GameDirectory.FixDirectoryCharacters(), StringComparison.InvariantCultureIgnoreCase);
// we want to see if base game is provided and it 'looks' like a directory
if (baseGameIsDirectory && !baseGameIsRelative)
{
workingDirectory = logInfo.BaseGameDirectory;
}
if (string.IsNullOrWhiteSpace(logInfo.ModDirectory))
{
logPath = Path.Combine(workingDirectory, logInfo.GameDirectory, logInfo.LogFile);
}
else
{
logPath = Path.Combine(workingDirectory, logInfo.ModDirectory, logInfo.LogFile);
}
// fix wine drive name mangling
if (!logInfo.IsWindows)
{
logPath = $"{Path.DirectorySeparatorChar}{Regex.Replace(logPath, @"[A-Z]:(\/|\\)", "")}";
}
return logPath.FixDirectoryCharacters();
}
public override async Task Warn(string reason, EFClient targetClient, EFClient targetOrigin)
{
// ensure player gets warned if command not performed on them in game
targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var newPenalty = new EFPenalty() var newPenalty = new EFPenalty()
{ {
Type = EFPenalty.PenaltyType.Warning, Type = EFPenalty.PenaltyType.Warning,
Expires = DateTime.UtcNow, Expires = DateTime.UtcNow,
Offender = Target, Offender = targetClient,
Punisher = Origin, Punisher = targetOrigin,
Offense = Reason, Offense = reason,
Link = Target.AliasLink Link = targetClient.AliasLink
}; };
await Manager.GetPenaltyService().Create(newPenalty); Logger.WriteDebug($"Creating warn penalty for {targetClient}");
} await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
protected override async Task Kick(String Reason, EFClient Target, EFClient Origin) if (targetClient.IsIngame)
{
// ensure player gets kicked if command not performed on them in game
if (Target.ClientNumber < 0)
{ {
var ingameClient = Manager.GetActiveClients() if (targetClient.Warnings >= 4)
.FirstOrDefault(c => c.ClientId == Target.ClientId);
if (ingameClient != null)
{ {
await Kick(Reason, ingameClient, Origin); targetClient.Kick(loc["SERVER_WARNLIMT_REACHED"], Utilities.IW4MAdminClient(this));
return; return;
} }
}
#if !DEBUG
else
{
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, Target.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7");
await Target.CurrentServer.ExecuteCommandAsync(formattedKick);
}
#endif
#if DEBUG string message = $"^1{loc["SERVER_WARNING"]} ^7[^3{targetClient.Warnings}^7]: ^3{targetClient.Name}^7, {reason}";
// await Target.CurrentServer.OnClientDisconnected(Target); targetClient.CurrentServer.Broadcast(message);
var e = new GameEvent() }
{ }
Type = GameEvent.EventType.PreDisconnect,
Origin = Target,
Owner = this
};
Manager.GetEventHandler().AddEvent(e); public override async Task Kick(string Reason, EFClient targetClient, EFClient originClient)
#endif {
targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients()
.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
var newPenalty = new EFPenalty() var newPenalty = new EFPenalty()
{ {
Type = EFPenalty.PenaltyType.Kick, Type = EFPenalty.PenaltyType.Kick,
Expires = DateTime.UtcNow, Expires = DateTime.UtcNow,
Offender = Target, Offender = targetClient,
Offense = Reason, Offense = Reason,
Punisher = Origin, Punisher = originClient,
Link = Target.AliasLink Link = targetClient.AliasLink
}; };
await Manager.GetPenaltyService().Create(newPenalty); Logger.WriteDebug($"Creating kick penalty for {targetClient}");
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
if (targetClient.IsIngame)
{
var e = new GameEvent()
{
Type = GameEvent.EventType.PreDisconnect,
Origin = targetClient,
Owner = this
};
Manager.AddEvent(e);
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7");
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
} }
protected override async Task TempBan(String Reason, TimeSpan length, EFClient Target, EFClient Origin) public override async Task TempBan(string Reason, TimeSpan length, EFClient targetClient, EFClient originClient)
{ {
// ensure player gets banned if command not performed on them in game // ensure player gets kicked if command not performed on them in the same server
if (Target.ClientNumber < 0) targetClient = targetClient.ClientNumber < 0 ?
{ Manager.GetActiveClients()
var ingameClient = Manager.GetActiveClients() .FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
.FirstOrDefault(c => c.ClientId == Target.ClientId); targetClient;
if (ingameClient != null)
{
await TempBan(Reason, length, ingameClient, Origin);
return;
}
}
#if !DEBUG
else
{
string formattedKick = String.Format(RconParser.Configuration.CommandPrefixes.Kick, Target.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}");
await Target.CurrentServer.ExecuteCommandAsync(formattedKick);
}
#else
await Target.CurrentServer.OnClientDisconnected(Target);
#endif
var newPenalty = new EFPenalty() var newPenalty = new EFPenalty()
{ {
Type = EFPenalty.PenaltyType.TempBan, Type = EFPenalty.PenaltyType.TempBan,
Expires = DateTime.UtcNow + length, Expires = DateTime.UtcNow + length,
Offender = Target, Offender = targetClient,
Offense = Reason, Offense = Reason,
Punisher = Origin, Punisher = originClient,
Link = Target.AliasLink Link = targetClient.AliasLink
}; };
await Manager.GetPenaltyService().Create(newPenalty); Logger.WriteDebug($"Creating tempban penalty for {targetClient}");
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
if (targetClient.IsIngame)
{
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}");
Logger.WriteDebug($"Executing tempban kick command for {targetClient}");
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
} }
override protected async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false) override public async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
{ {
// ensure player gets banned if command not performed on them in game // ensure player gets kicked if command not performed on them in the same server
if (targetClient.ClientNumber < 0) targetClient = targetClient.ClientNumber < 0 ?
{ Manager.GetActiveClients()
EFClient ingameClient = null; .FirstOrDefault(c => c.ClientId == targetClient?.ClientId) ?? targetClient :
targetClient;
ingameClient = Manager.GetServers()
.Select(s => s.GetClientsAsList())
.FirstOrDefault(l => l.FirstOrDefault(c => c.ClientId == targetClient?.ClientId) != null)
?.First(c => c.ClientId == targetClient.ClientId);
if (ingameClient != null)
{
await Ban(reason, ingameClient, originClient, isEvade);
return;
}
}
else
{
#if !DEBUG
string formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7");
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString);
#else
await targetClient.CurrentServer.OnClientDisconnected(targetClient);
#endif
}
EFPenalty newPenalty = new EFPenalty() EFPenalty newPenalty = new EFPenalty()
{ {
@ -1159,8 +1264,16 @@ namespace IW4MAdmin
IsEvadedOffense = isEvade IsEvadedOffense = isEvade
}; };
Logger.WriteDebug($"Creating ban penalty for {targetClient}");
targetClient.SetLevel(Permission.Banned, originClient); targetClient.SetLevel(Permission.Banned, originClient);
await Manager.GetPenaltyService().Create(newPenalty); await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), Manager.GetLogger(0));
if (targetClient.IsIngame)
{
Logger.WriteDebug($"Attempting to kicking newly banned client {targetClient}");
string formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7");
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString);
}
} }
override public async Task Unban(string reason, EFClient Target, EFClient Origin) override public async Task Unban(string reason, EFClient Target, EFClient Origin)
@ -1186,8 +1299,8 @@ namespace IW4MAdmin
{ {
Manager.GetMessageTokens().Add(new MessageToken("TOTALPLAYERS", (Server s) => Task.Run(async () => (await Manager.GetClientService().GetTotalClientsAsync()).ToString()))); Manager.GetMessageTokens().Add(new MessageToken("TOTALPLAYERS", (Server s) => Task.Run(async () => (await Manager.GetClientService().GetTotalClientsAsync()).ToString())));
Manager.GetMessageTokens().Add(new MessageToken("VERSION", (Server s) => Task.FromResult(Application.Program.Version.ToString()))); Manager.GetMessageTokens().Add(new MessageToken("VERSION", (Server s) => Task.FromResult(Application.Program.Version.ToString())));
Manager.GetMessageTokens().Add(new MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.CNextMap.GetNextMap(s))); Manager.GetMessageTokens().Add(new MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.NextMapCommand.GetNextMap(s, _translationLookup)));
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(SharedLibraryCore.Commands.CListAdmins.OnlineAdmins(s)))); Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(SharedLibraryCore.Commands.ListAdminsCommand.OnlineAdmins(s, _translationLookup))));
} }
} }
} }

View File

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

View File

@ -1,9 +1,26 @@
using IW4MAdmin.Application.Migration; using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Factories;
using IW4MAdmin.Application.Helpers;
using IW4MAdmin.Application.Meta;
using IW4MAdmin.Application.Migration;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using RestEase;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Repositories;
using SharedLibraryCore.Services;
using Stats.Dtos;
using StatsWeb;
using System; using System;
using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -15,13 +32,13 @@ namespace IW4MAdmin.Application
public static BuildNumber Version { get; private set; } = BuildNumber.Parse(Utilities.GetVersionAsString()); public static BuildNumber Version { get; private set; } = BuildNumber.Parse(Utilities.GetVersionAsString());
public static ApplicationManager ServerManager; public static ApplicationManager ServerManager;
private static Task ApplicationTask; private static Task ApplicationTask;
private static readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99"); private static ServiceProvider serviceProvider;
/// <summary> /// <summary>
/// entrypoint of the application /// entrypoint of the application
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static async Task Main() public static async Task Main(string[] args)
{ {
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory); AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
@ -36,7 +53,7 @@ namespace IW4MAdmin.Application
Console.WriteLine($" Version {Utilities.GetVersionAsString()}"); Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("====================================================="); Console.WriteLine("=====================================================");
await LaunchAsync(); await LaunchAsync(args);
} }
/// <summary> /// <summary>
@ -55,31 +72,32 @@ namespace IW4MAdmin.Application
/// task that initializes application and starts the application monitoring and runtime tasks /// task that initializes application and starts the application monitoring and runtime tasks
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private static async Task LaunchAsync() private static async Task LaunchAsync(string[] args)
{ {
restart: restart:
ITranslationLookup translationLookup = null;
try try
{ {
ServerManager = ApplicationManager.GetInstance();
var configuration = ServerManager.GetApplicationSettings().Configuration();
Localization.Configure.Initialize(configuration?.EnableCustomLocale ?? false ? (configuration.CustomLocale ?? "en-US") : "en-US");
// do any needed housekeeping file/folder migrations // do any needed housekeeping file/folder migrations
ConfigurationMigration.MoveConfigFolder10518(null); ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories(); ConfigurationMigration.CheckDirectories();
var services = ConfigureServices(args);
serviceProvider = services.BuildServiceProvider();
var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>();
ServerManager = (ApplicationManager)serviceProvider.GetRequiredService<IManager>();
translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>();
ServerManager.Logger.WriteInfo(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_VERSION"].FormatExt(Version)); ServerManager.Logger.WriteInfo(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_VERSION"].FormatExt(Version));
ConfigureServices(); await versionChecker.CheckVersion();
await CheckVersion();
await ServerManager.Init(); await ServerManager.Init();
} }
catch (Exception e) catch (Exception e)
{ {
var loc = Utilities.CurrentLocalization.LocalizationIndex; string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
string failMessage = loc == null ? "Failed to initalize IW4MAdmin" : loc["MANAGER_INIT_FAIL"]; string exitMessage = translationLookup == null ? "Press enter to exit..." : translationLookup["MANAGER_EXIT"];
string exitMessage = loc == null ? "Press any key to exit..." : loc["MANAGER_EXIT"];
Console.WriteLine(failMessage); Console.WriteLine(failMessage);
@ -88,9 +106,26 @@ namespace IW4MAdmin.Application
e = e.InnerException; e = e.InnerException;
} }
Console.WriteLine(e.Message); if (e is ConfigurationException configException)
{
if (translationLookup != null)
{
Console.WriteLine(translationLookup[configException.Message].FormatExt(configException.ConfigurationFileName));
}
foreach (string error in configException.Errors)
{
Console.WriteLine(error);
}
}
else
{
Console.WriteLine(e.Message);
}
Console.WriteLine(exitMessage); Console.WriteLine(exitMessage);
Console.ReadKey(); await Console.In.ReadAsync(new char[1], 0, 1);
return; return;
} }
@ -100,12 +135,18 @@ namespace IW4MAdmin.Application
await ApplicationTask; await ApplicationTask;
} }
catch { } catch (Exception e)
{
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}");
}
if (ServerManager.IsRestartRequested) if (ServerManager.IsRestartRequested)
{ {
goto restart; goto restart;
} }
serviceProvider.Dispose();
} }
/// <summary> /// <summary>
@ -115,7 +156,7 @@ namespace IW4MAdmin.Application
private static async Task RunApplicationTasksAsync() private static async Task RunApplicationTasksAsync()
{ {
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ? var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ?
WebfrontCore.Program.Init(ServerManager, ServerManager.CancellationToken) : WebfrontCore.Program.Init(ServerManager, serviceProvider, ServerManager.CancellationToken) :
Task.CompletedTask; Task.CompletedTask;
// we want to run this one on a manual thread instead of letting the thread pool handle it, // we want to run this one on a manual thread instead of letting the thread pool handle it,
@ -127,6 +168,7 @@ namespace IW4MAdmin.Application
{ {
ServerManager.Start(), ServerManager.Start(),
webfrontTask, webfrontTask,
serviceProvider.GetRequiredService<IMasterCommunication>().RunUploadStatus(ServerManager.CancellationToken)
}; };
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
@ -134,68 +176,6 @@ namespace IW4MAdmin.Application
ServerManager.Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]); 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()
{
var api = API.Master.Endpoint.Get();
var loc = Utilities.CurrentLocalization.LocalizationIndex;
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;
}
}
/// <summary> /// <summary>
/// reads input from the console and executes entered commands on the default server /// reads input from the console and executes entered commands on the default server
@ -203,6 +183,12 @@ namespace IW4MAdmin.Application
/// <returns></returns> /// <returns></returns>
private static async Task ReadConsoleInput() private static async Task ReadConsoleInput()
{ {
if (Console.IsInputRedirected)
{
ServerManager.Logger.WriteInfo("Disabling console input as it has been redirected");
return;
}
string lastCommand; string lastCommand;
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]); var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
@ -210,7 +196,7 @@ namespace IW4MAdmin.Application
{ {
while (!ServerManager.CancellationToken.IsCancellationRequested) while (!ServerManager.CancellationToken.IsCancellationRequested)
{ {
lastCommand = Console.ReadLine(); lastCommand = await Console.In.ReadLineAsync();
if (lastCommand?.Length > 0) if (lastCommand?.Length > 0)
{ {
@ -224,7 +210,7 @@ namespace IW4MAdmin.Application
Owner = ServerManager.Servers[0] Owner = ServerManager.Servers[0]
}; };
ServerManager.GetEventHandler().AddEvent(E); ServerManager.AddEvent(E);
await E.WaitAsync(Utilities.DefaultCommandTimeout, ServerManager.CancellationToken); await E.WaitAsync(Utilities.DefaultCommandTimeout, ServerManager.CancellationToken);
Console.Write('>'); Console.Write('>');
} }
@ -235,12 +221,105 @@ namespace IW4MAdmin.Application
{ } { }
} }
private static void ConfigureServices() /// <summary>
/// Configures the dependency injection services
/// </summary>
private static IServiceCollection ConfigureServices(string[] args)
{ {
var serviceProvider = new ServiceCollection(); var defaultLogger = new Logger("IW4MAdmin-Manager");
serviceProvider.AddSingleton<IManager>(ServerManager); var pluginImporter = new PluginImporter(defaultLogger);
var builder = serviceProvider.BuildServiceProvider();
builder.Dispose(); 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() ?? new ApplicationConfiguration())
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>().Configuration() ?? new CommandConfiguration())
.AddSingleton<ILogger>(_serviceProvider => defaultLogger)
.AddSingleton<IPluginImporter, PluginImporter>()
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
.AddSingleton<IRConConnectionFactory, RConConnectionFactory>()
.AddSingleton<IGameServerInstanceFactory, GameServerInstanceFactory>()
.AddSingleton<IConfigurationHandlerFactory, ConfigurationHandlerFactory>()
.AddSingleton<IParserRegexFactory, ParserRegexFactory>()
.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>()
.AddSingleton<IGameLogReaderFactory, GameLogReaderFactory>()
.AddSingleton<IScriptCommandFactory, ScriptCommandFactory>()
.AddSingleton<IAuditInformationRepository, AuditInformationRepository>()
.AddSingleton<IEntityService<EFClient>, ClientService>()
.AddSingleton<IMetaService, MetaService>()
.AddSingleton<IMetaRegistration, MetaRegistration>()
.AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>, ReceivedPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>, AdministeredPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>, UpdatedAliasResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
.AddSingleton(_serviceProvider =>
{
var config = _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration();
return Localization.Configure.Initialize(useLocalTranslation: config?.UseLocalTranslations ?? false,
apiInstance: _serviceProvider.GetRequiredService<IMasterApi>(),
customLocale: config?.EnableCustomLocale ?? false ? (config.CustomLocale ?? "en-US") : "en-US");
})
.AddSingleton<IManager, ApplicationManager>()
.AddSingleton(_serviceProvider => RestClient
.For<IMasterApi>(Utilities.IsDevelopment ? new Uri("http://127.0.0.1:8080") : _serviceProvider
.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration()?.MasterUrl ??
new ApplicationConfiguration().MasterUrl))
.AddSingleton<IMasterCommunication, MasterCommunication>();
if (args.Contains("serialevents"))
{
serviceCollection.AddSingleton<IEventHandler, SerialGameEventHandler>();
}
else
{
serviceCollection.AddSingleton<IEventHandler, GameEventHandler>();
}
// register the native commands
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
.Where(_command => _command.BaseType == typeof(Command)))
{
defaultLogger.WriteInfo($"Registered native command type {commandType.Name}");
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
// register the plugin implementations
var pluginImplementations = pluginImporter.DiscoverAssemblyPluginImplementations();
foreach (var pluginType in pluginImplementations.Item1)
{
defaultLogger.WriteInfo($"Registered plugin type {pluginType.FullName}");
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
}
// register the plugin commands
foreach (var commandType in pluginImplementations.Item2)
{
defaultLogger.WriteInfo($"Registered plugin command type {commandType.FullName}");
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
}
// register any script plugins
foreach (var scriptPlugin in pluginImporter.DiscoverScriptPlugins())
{
serviceCollection.AddSingleton(scriptPlugin);
}
// register any eventable types
foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))
.Union(pluginImplementations
.Item1.SelectMany(_asm => _asm.Assembly.GetTypes())
.Distinct()
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
{
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
serviceCollection.AddSingleton(instance);
}
return serviceCollection;
} }
} }
} }

View File

@ -0,0 +1,64 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
namespace IW4MAdmin.Application.Meta
{
/// <summary>
/// implementation of IResourceQueryHelper
/// query helper that retrieves administered penalties for provided client id
/// </summary>
public class AdministeredPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
public AdministeredPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
_logger = logger;
}
public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => query.ClientId == _penalty.PunisherId)
.Where(_penalty => _penalty.When < query.Before)
.OrderByDescending(_penalty => _penalty.When);
var penalties = await iqPenalties
.Take(query.Count)
.Select(_penalty => new AdministeredPenaltyResponse()
{
PenaltyId = _penalty.PenaltyId,
Offense = _penalty.Offense,
AutomatedOffense = _penalty.AutomatedOffense,
ClientId = _penalty.OffenderId,
OffenderName = _penalty.Offender.CurrentAlias.Name,
OffenderClientId = _penalty.Offender.ClientId,
PunisherClientId = _penalty.PunisherId,
PunisherName = _penalty.Punisher.CurrentAlias.Name,
PenaltyType = _penalty.Type,
When = _penalty.When,
ExpirationDate = _penalty.Expires,
IsLinked = _penalty.OffenderId != query.ClientId,
IsSensitive = _penalty.Type == EFPenalty.PenaltyType.Flag
})
.ToListAsync();
return new ResourceQueryHelperResult<AdministeredPenaltyResponse>
{
// todo: might need to do count at some point
RetrievedResultCount = penalties.Count,
Results = penalties
};
}
}
}

View File

@ -0,0 +1,165 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Meta
{
public class MetaRegistration : IMetaRegistration
{
private readonly ILogger _logger;
private ITranslationLookup _transLookup;
private readonly IMetaService _metaService;
private readonly IEntityService<EFClient> _clientEntityService;
private readonly IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> _receivedPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> _administeredPenaltyHelper;
private readonly IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> _updatedAliasHelper;
public MetaRegistration(ILogger logger, IMetaService metaService, ITranslationLookup transLookup, IEntityService<EFClient> clientEntityService,
IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> receivedPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> administeredPenaltyHelper,
IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper)
{
_logger = logger;
_transLookup = transLookup;
_metaService = metaService;
_clientEntityService = clientEntityService;
_receivedPenaltyHelper = receivedPenaltyHelper;
_administeredPenaltyHelper = administeredPenaltyHelper;
_updatedAliasHelper = updatedAliasHelper;
}
public void Register()
{
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, GetProfileMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, ReceivedPenaltyResponse>(MetaType.ReceivedPenalty, GetReceivedPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, AdministeredPenaltyResponse>(MetaType.Penalized, GetAdministeredPenaltiesMeta);
_metaService.AddRuntimeMeta<ClientPaginationRequest, UpdatedAliasResponse>(MetaType.AliasUpdate, GetUpdatedAliasMeta);
}
private async Task<IEnumerable<InformationResponse>> GetProfileMeta(ClientPaginationRequest request)
{
var metaList = new List<InformationResponse>();
var lastMapMeta = await _metaService.GetPersistentMeta("LastMapPlayed", new EFClient() { ClientId = request.ClientId });
if (lastMapMeta != null)
{
metaList.Add(new InformationResponse()
{
ClientId = request.ClientId,
MetaId = lastMapMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_MAP"],
Value = lastMapMeta.Value,
ShouldDisplay = true,
Type = MetaType.Information,
Column = 1,
Order = 6
});
}
var lastServerMeta = await _metaService.GetPersistentMeta("LastServerPlayed", new EFClient() { ClientId = request.ClientId });
if (lastServerMeta != null)
{
metaList.Add(new InformationResponse()
{
ClientId = request.ClientId,
MetaId = lastServerMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_SERVER"],
Value = lastServerMeta.Value,
ShouldDisplay = true,
Type = MetaType.Information,
Column = 0,
Order = 6
});
}
var client = await _clientEntityService.Get(request.ClientId);
if (client == null)
{
_logger.WriteWarning($"No client found with id {request.ClientId} when generating profile meta");
return metaList;
}
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = _transLookup["WEBFRONT_PROFILE_META_PLAY_TIME"],
Value = TimeSpan.FromHours(client.TotalConnectionTime / 3600.0).HumanizeForCurrentCulture(),
ShouldDisplay = true,
Column = 1,
Order = 0,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = _transLookup["WEBFRONT_PROFILE_META_FIRST_SEEN"],
Value = (DateTime.UtcNow - client.FirstConnection).HumanizeForCurrentCulture(),
ShouldDisplay = true,
Column = 1,
Order = 1,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = _transLookup["WEBFRONT_PROFILE_META_LAST_SEEN"],
Value = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(),
ShouldDisplay = true,
Column = 1,
Order = 2,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_CONNECTIONS"],
Value = client.Connections.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
ShouldDisplay = true,
Column = 1,
Order = 3,
Type = MetaType.Information
});
metaList.Add(new InformationResponse()
{
ClientId = client.ClientId,
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"],
IsSensitive = true,
Column = 1,
Order = 4,
Type = MetaType.Information
});
return metaList;
}
private async Task<IEnumerable<ReceivedPenaltyResponse>> GetReceivedPenaltiesMeta(ClientPaginationRequest request)
{
var penalties = await _receivedPenaltyHelper.QueryResource(request);
return penalties.Results;
}
private async Task<IEnumerable<AdministeredPenaltyResponse>> GetAdministeredPenaltiesMeta(ClientPaginationRequest request)
{
var penalties = await _administeredPenaltyHelper.QueryResource(request);
return penalties.Results;
}
private async Task<IEnumerable<UpdatedAliasResponse>> GetUpdatedAliasMeta(ClientPaginationRequest request)
{
var aliases = await _updatedAliasHelper.QueryResource(request);
return aliases.Results;
}
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
namespace IW4MAdmin.Application.Meta
{
/// <summary>
/// implementation of IResourceQueryHelper
/// used to pull in penalties applied to a given client id
/// </summary>
public class ReceivedPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
public ReceivedPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
_logger = logger;
}
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query)
{
var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
using var ctx = _contextFactory.CreateContext(enableTracking: false);
var linkId = await ctx.Clients.AsNoTracking()
.Where(_client => _client.ClientId == query.ClientId)
.Select(_client => _client.AliasLinkId)
.FirstOrDefaultAsync();
var iqPenalties = ctx.Penalties.AsNoTracking()
.Where(_penalty => _penalty.OffenderId == query.ClientId || (linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId))
.Where(_penalty => _penalty.When < query.Before)
.OrderByDescending(_penalty => _penalty.When);
var penalties = await iqPenalties
.Take(query.Count)
.Select(_penalty => new ReceivedPenaltyResponse()
{
PenaltyId = _penalty.PenaltyId,
ClientId = query.ClientId,
Offense = _penalty.Offense,
AutomatedOffense = _penalty.AutomatedOffense,
OffenderClientId = _penalty.OffenderId,
OffenderName = _penalty.Offender.CurrentAlias.Name,
PunisherClientId = _penalty.PunisherId,
PunisherName = _penalty.Punisher.CurrentAlias.Name,
PenaltyType = _penalty.Type,
When = _penalty.When,
ExpirationDate = _penalty.Expires,
IsLinked = _penalty.OffenderId != query.ClientId,
IsSensitive = _penalty.Type == EFPenalty.PenaltyType.Flag
})
.ToListAsync();
return new ResourceQueryHelperResult<ReceivedPenaltyResponse>
{
// todo: maybe actually count
RetrievedResultCount = penalties.Count,
Results = penalties
};
}
}
}

View File

@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System.Linq;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Meta
{
/// <summary>
/// implementation if IResrouceQueryHerlp
/// used to pull alias changes for given client id
/// </summary>
public class UpdatedAliasResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>
{
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
public UpdatedAliasResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_contextFactory = contextFactory;
}
public async Task<ResourceQueryHelperResult<UpdatedAliasResponse>> QueryResource(ClientPaginationRequest query)
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId;
var iqAliasUpdates = ctx.Aliases
.Where(_alias => _alias.LinkId == linkId)
.Where(_alias => _alias.DateAdded < query.Before)
.Where(_alias => _alias.IPAddress != null)
.OrderByDescending(_alias => _alias.DateAdded)
.Select(_alias => new UpdatedAliasResponse
{
MetaId = _alias.AliasId,
Name = _alias.Name,
IPAddress = _alias.IPAddress.ConvertIPtoString(),
When = _alias.DateAdded,
Type = MetaType.AliasUpdate,
IsSensitive = true
});
var result = (await iqAliasUpdates
.Take(query.Count)
.ToListAsync())
.Distinct();
return new ResourceQueryHelperResult<UpdatedAliasResponse>
{
Results = result, // we can potentially have duplicates
RetrievedResultCount = result.Count()
};
}
}
}

View File

@ -34,6 +34,11 @@ namespace IW4MAdmin.Application.Migration
{ {
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log")); Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log"));
} }
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Localization")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Localization"));
}
} }
/// <summary> /// <summary>

View File

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

View File

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

View File

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

View File

@ -1,12 +1,14 @@
using SharedLibraryCore; using IW4MAdmin.Application.IO;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
namespace IW4MAdmin.Application namespace IW4MAdmin.Application
{ {
class Logger : ILogger public class Logger : ILogger
{ {
enum LogType enum LogType
{ {
@ -77,9 +79,7 @@ namespace IW4MAdmin.Application
{ {
#if DEBUG #if DEBUG
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively) // lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
Console.WriteLine(LogLine); Console.WriteLine(msg);
//File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
//Debug.WriteLine(msg);
#else #else
if (type == LogType.Error || type == LogType.Verbose) if (type == LogType.Error || type == LogType.Verbose)
{ {

View File

@ -0,0 +1,209 @@
using IW4MAdmin.Application.API.Master;
using RestEase;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IMasterCommunication
/// talks to the master server
/// </summary>
class MasterCommunication : IMasterCommunication
{
private readonly ILogger _logger;
private readonly ITranslationLookup _transLookup;
private readonly IMasterApi _apiInstance;
private readonly IManager _manager;
private readonly ApplicationConfiguration _appConfig;
private readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99");
private readonly int _apiVersion = 1;
private bool firstHeartBeat = true;
public MasterCommunication(ILogger logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager)
{
_logger = logger;
_transLookup = translationLookup;
_apiInstance = apiInstance;
_appConfig = appConfig;
_manager = manager;
}
/// <summary>
/// checks for latest version of the application
/// notifies user if an update is available
/// </summary>
/// <returns></returns>
public async Task CheckVersion()
{
var version = new VersionInfo()
{
CurrentVersionStable = _fallbackVersion
};
try
{
version = await _apiInstance.GetVersion(_apiVersion);
}
catch (Exception e)
{
_logger.WriteWarning(_transLookup["MANAGER_VERSION_FAIL"]);
while (e.InnerException != null)
{
e = e.InnerException;
}
_logger.WriteDebug(e.Message);
}
if (version.CurrentVersionStable == _fallbackVersion)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(_transLookup["MANAGER_VERSION_FAIL"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
#if !PRERELEASE
else if (version.CurrentVersionStable > Program.Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin {_transLookup["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString()}]");
Console.WriteLine(_transLookup["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Program.Version.ToString()}]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#else
else if (version.CurrentVersionPrerelease > Program.Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin-Prerelease {_transLookup["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString()}-pr]");
Console.WriteLine(_transLookup["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Program.Version.ToString()}-pr]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#endif
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(_transLookup["MANAGER_VERSION_SUCCESS"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
}
public async Task RunUploadStatus(CancellationToken token)
{
// todo: clean up this logic
bool connected;
while (!token.IsCancellationRequested)
{
try
{
await UploadStatus();
}
catch (System.Net.Http.HttpRequestException e)
{
_logger.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(ApiException));
foreach (var ex in exceptions)
{
if (((ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
}
}
catch (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, token);
}
catch
{
break;
}
}
}
private async Task UploadStatus()
{
if (firstHeartBeat)
{
var token = await _apiInstance.Authenticate(new AuthenticationId
{
Id = _appConfig.Id
});
_apiInstance.AuthorizationToken = $"Bearer {token.AccessToken}";
}
var instance = new ApiInstance
{
Id = _appConfig.Id,
Uptime = (int)(DateTime.UtcNow - (_manager as ApplicationManager).StartTime).TotalSeconds,
Version = Program.Version,
Servers = _manager.GetServers().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.EndPoint,
Port = (short)s.Port,
IPAddress = s.IP
}).ToList()
};
Response<ResultMessage> response = null;
if (firstHeartBeat)
{
response = await _apiInstance.AddInstance(instance);
}
else
{
response = await _apiInstance.UpdateInstance(instance.Id, instance);
firstHeartBeat = false;
}
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
{
_logger.WriteWarning($"Response code from master is {response.ResponseMessage.StatusCode}, message is {response.StringContent}");
}
}
}
}

View File

@ -0,0 +1,187 @@
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IMetaService
/// used to add and retrieve runtime and persistent meta
/// </summary>
public class MetaService : IMetaService
{
private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
public MetaService(ILogger logger, IDatabaseContextFactory contextFactory)
{
_logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory;
}
public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client)
{
// this seems to happen if the client disconnects before they've had time to authenticate and be added
if (client.ClientId < 1)
{
return;
}
using var ctx = _contextFactory.CreateContext();
var existingMeta = await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey)
.Where(_meta => _meta.ClientId == client.ClientId)
.FirstOrDefaultAsync();
if (existingMeta != null)
{
existingMeta.Value = metaValue;
existingMeta.Updated = DateTime.UtcNow;
}
else
{
ctx.EFMeta.Add(new EFMeta()
{
ClientId = client.ClientId,
Created = DateTime.UtcNow,
Key = metaKey,
Value = metaValue
});
}
await ctx.SaveChangesAsync();
}
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
{
using var ctx = _contextFactory.CreateContext(enableTracking: false);
return await ctx.EFMeta
.Where(_meta => _meta.Key == metaKey)
.Where(_meta => _meta.ClientId == client.ClientId)
.Select(_meta => new EFMeta()
{
MetaId = _meta.MetaId,
Key = _meta.Key,
ClientId = _meta.ClientId,
Value = _meta.Value
})
.FirstOrDefaultAsync();
}
public void AddRuntimeMeta<T, V>(MetaType metaKey, Func<T, Task<IEnumerable<V>>> metaAction) where T : PaginationRequest where V : IClientMeta
{
if (!_metaActions.ContainsKey(metaKey))
{
_metaActions.Add(metaKey, new List<dynamic>() { metaAction });
}
else
{
_metaActions[metaKey].Add(metaAction);
}
}
public async Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request)
{
var meta = new List<IClientMeta>();
foreach (var (type, actions) in _metaActions)
{
// information is not listed chronologically
if (type != MetaType.Information)
{
var metaItems = await actions[0](request);
meta.AddRange(metaItems);
}
}
return meta.OrderByDescending(_meta => _meta.When)
.Take(request.Count)
.ToList();
}
public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType) where T : IClientMeta
{
IEnumerable<T> meta;
if (metaType == MetaType.Information)
{
var allMeta = new List<T>();
foreach (var individualMetaRegistration in _metaActions[metaType])
{
allMeta.AddRange(await individualMetaRegistration(request));
}
return ProcessInformationMeta(allMeta);
}
else
{
meta = await _metaActions[metaType][0](request) as IEnumerable<T>;
}
return meta;
}
private static IEnumerable<T> ProcessInformationMeta<T>(IEnumerable<T> meta) where T : IClientMeta
{
var table = new List<List<T>>();
var metaWithColumn = meta
.Where(_meta => _meta.Column != null);
var columnGrouping = metaWithColumn
.GroupBy(_meta => _meta.Column);
var metaToSort = meta.Except(metaWithColumn).ToList();
foreach (var metaItem in columnGrouping)
{
table.Add(new List<T>(metaItem));
}
while (metaToSort.Count > 0)
{
var sortingMeta = metaToSort.First();
int indexOfSmallestColumn()
{
int index = 0;
int smallestColumnSize = int.MaxValue;
for (int i = 0; i < table.Count; i++)
{
if (table[i].Count < smallestColumnSize)
{
smallestColumnSize = table[i].Count;
index = i;
}
}
return index;
}
int columnIndex = indexOfSmallestColumn();
sortingMeta.Column = columnIndex;
sortingMeta.Order = columnGrouping
.First(_group => _group.Key == columnIndex)
.Count();
table[columnIndex].Add(sortingMeta);
metaToSort.Remove(sortingMeta);
}
return meta;
}
}
}

View File

@ -1,15 +1,29 @@
using SharedLibraryCore.Interfaces; using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace IW4MAdmin.Application.Misc namespace IW4MAdmin.Application.Misc
{ {
class MiddlewareActionHandler : IMiddlewareActionHandler class MiddlewareActionHandler : IMiddlewareActionHandler
{ {
private static readonly IDictionary<string, IList<object>> _actions = new Dictionary<string, IList<object>>(); 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) public async Task<T> Execute<T>(T value, string name = null)
{ {
string key = string.IsNullOrEmpty(name) ? typeof(T).ToString() : name; string key = string.IsNullOrEmpty(name) ? typeof(T).ToString() : name;
@ -22,8 +36,11 @@ namespace IW4MAdmin.Application.Misc
{ {
value = await ((IMiddlewareAction<T>)action).Invoke(value); value = await ((IMiddlewareAction<T>)action).Invoke(value);
} }
// todo: probably log this somewhere catch (Exception e)
catch { } {
_logger.WriteWarning($"Failed to invoke middleware action {name}");
_logger.WriteDebug(e.GetExceptionInfo());
}
} }
return value; return value;
@ -32,6 +49,13 @@ namespace IW4MAdmin.Application.Misc
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) public void Register<T>(T actionType, IMiddlewareAction<T> action, string name = null)
{ {
string key = string.IsNullOrEmpty(name) ? typeof(T).ToString() : name; string key = string.IsNullOrEmpty(name) ? typeof(T).ToString() : name;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,48 @@
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IScriptPluginServiceResolver
/// </summary>
public class ScriptPluginServiceResolver : IScriptPluginServiceResolver
{
private readonly IServiceProvider _serviceProvider;
public ScriptPluginServiceResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public object ResolveService(string serviceName)
{
var serviceType = DetermineRootType(serviceName);
return _serviceProvider.GetService(serviceType);
}
public object ResolveService(string serviceName, string[] genericParameters)
{
var serviceType = DetermineRootType(serviceName, genericParameters.Length);
var genericTypes = genericParameters.Select(_genericTypeParam => DetermineRootType(_genericTypeParam));
var resolvedServiceType = serviceType.MakeGenericType(genericTypes.ToArray());
return _serviceProvider.GetService(resolvedServiceType);
}
private Type DetermineRootType(string serviceName, int genericParamCount = 0)
{
var typeCollection = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(t => t.GetTypes());
string generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
var serviceType = typeCollection.FirstOrDefault(_type => _type.Name.ToLower() == generatedName);
if (serviceType == null)
{
throw new InvalidOperationException($"No object type '{serviceName}' defined in loaded assemblies");
}
return serviceType;
}
}
}

View File

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

View File

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

View File

@ -1,58 +1,43 @@
using SharedLibraryCore.Exceptions; using SharedLibraryCore;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SharedLibraryCore.RCon namespace IW4MAdmin.Application.RCon
{ {
class ConnectionState /// <summary>
{ /// implementation of IRConConnection
~ConnectionState() /// </summary>
{ public class RConConnection : IRConConnection
OnComplete.Dispose();
OnSentData.Dispose();
OnReceivedData.Dispose();
}
public int ConnectionAttempts { get; set; }
const int BufferSize = 4096;
public readonly byte[] ReceiveBuffer = new byte[BufferSize];
public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1);
public readonly ManualResetEventSlim OnSentData = new ManualResetEventSlim(false);
public readonly ManualResetEventSlim OnReceivedData = new ManualResetEventSlim(false);
public SocketAsyncEventArgs SendEventArgs { get; set; } = new SocketAsyncEventArgs();
public SocketAsyncEventArgs ReceiveEventArgs { get; set; } = new SocketAsyncEventArgs();
public DateTime LastQuery { get; set; } = DateTime.Now;
}
public class Connection
{ {
static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new ConcurrentDictionary<EndPoint, ConnectionState>(); static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new ConcurrentDictionary<EndPoint, ConnectionState>();
public IPEndPoint Endpoint { get; private set; } public IPEndPoint Endpoint { get; private set; }
public string RConPassword { get; private set; } public string RConPassword { get; private set; }
private readonly ILogger Log; private IRConParserConfiguration config;
private IRConParserConfiguration Config; private readonly ILogger _log;
private readonly Encoding defaultEncoding; private readonly Encoding _gameEncoding;
public Connection(string ipAddress, int port, string password, ILogger log, IRConParserConfiguration config) public RConConnection(string ipAddress, int port, string password, ILogger log, Encoding gameEncoding)
{ {
Endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), port); Endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
defaultEncoding = Encoding.GetEncoding("windows-1252"); _gameEncoding = gameEncoding;
RConPassword = password; RConPassword = password;
Log = log; _log = log;
Config = config;
} }
public void SetConfiguration(IRConParserConfiguration config) public void SetConfiguration(IRConParserConfiguration config)
{ {
Config = config; this.config = config;
} }
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "")
@ -65,7 +50,7 @@ namespace SharedLibraryCore.RCon
var connectionState = ActiveQueries[this.Endpoint]; var connectionState = ActiveQueries[this.Endpoint];
#if DEBUG == true #if DEBUG == true
Log.WriteDebug($"Waiting for semaphore to be released [{this.Endpoint}]"); _log.WriteDebug($"Waiting for semaphore to be released [{this.Endpoint}]");
#endif #endif
// enter the semaphore so only one query is sent at a time per server. // enter the semaphore so only one query is sent at a time per server.
await connectionState.OnComplete.WaitAsync(); await connectionState.OnComplete.WaitAsync();
@ -80,17 +65,17 @@ namespace SharedLibraryCore.RCon
connectionState.LastQuery = DateTime.Now; connectionState.LastQuery = DateTime.Now;
#if DEBUG == true #if DEBUG == true
Log.WriteDebug($"Semaphore has been released [{this.Endpoint}]"); _log.WriteDebug($"Semaphore has been released [{this.Endpoint}]");
Log.WriteDebug($"Query [{this.Endpoint},{type.ToString()},{parameters}]"); _log.WriteDebug($"Query [{this.Endpoint},{type.ToString()},{parameters}]");
#endif #endif
byte[] payload = null; byte[] payload = null;
bool waitForResponse = Config.WaitForResponse; bool waitForResponse = config.WaitForResponse;
string convertEncoding(string text) string convertEncoding(string text)
{ {
byte[] convertedBytes = Utilities.EncodingType.GetBytes(text); byte[] convertedBytes = Utilities.EncodingType.GetBytes(text);
return defaultEncoding.GetString(convertedBytes); return _gameEncoding.GetString(convertedBytes);
} }
try try
@ -102,25 +87,25 @@ namespace SharedLibraryCore.RCon
{ {
case StaticHelpers.QueryType.GET_DVAR: case StaticHelpers.QueryType.GET_DVAR:
waitForResponse |= true; waitForResponse |= true;
payload = string.Format(Config.CommandPrefixes.RConGetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray(); payload = string.Format(config.CommandPrefixes.RConGetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break; break;
case StaticHelpers.QueryType.SET_DVAR: case StaticHelpers.QueryType.SET_DVAR:
payload = string.Format(Config.CommandPrefixes.RConSetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray(); payload = string.Format(config.CommandPrefixes.RConSetDvar, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break; break;
case StaticHelpers.QueryType.COMMAND: case StaticHelpers.QueryType.COMMAND:
payload = string.Format(Config.CommandPrefixes.RConCommand, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray(); payload = string.Format(config.CommandPrefixes.RConCommand, convertedRConPassword, convertedParameters + '\0').Select(Convert.ToByte).ToArray();
break; break;
case StaticHelpers.QueryType.GET_STATUS: case StaticHelpers.QueryType.GET_STATUS:
waitForResponse |= true; waitForResponse |= true;
payload = (Config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray(); payload = (config.CommandPrefixes.RConGetStatus + '\0').Select(Convert.ToByte).ToArray();
break; break;
case StaticHelpers.QueryType.GET_INFO: case StaticHelpers.QueryType.GET_INFO:
waitForResponse |= true; waitForResponse |= true;
payload = (Config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray(); payload = (config.CommandPrefixes.RConGetInfo + '\0').Select(Convert.ToByte).ToArray();
break; break;
case StaticHelpers.QueryType.COMMAND_STATUS: case StaticHelpers.QueryType.COMMAND_STATUS:
waitForResponse |= true; waitForResponse |= true;
payload = string.Format(Config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0").Select(Convert.ToByte).ToArray(); payload = string.Format(config.CommandPrefixes.RConCommand, convertedRConPassword, "status\0").Select(Convert.ToByte).ToArray();
break; break;
} }
} }
@ -133,7 +118,7 @@ namespace SharedLibraryCore.RCon
throw new NetworkException($"Invalid character encountered when converting encodings - {parameters}"); throw new NetworkException($"Invalid character encountered when converting encodings - {parameters}");
} }
byte[] response = null; byte[][] response = null;
retrySend: retrySend:
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
@ -147,14 +132,15 @@ namespace SharedLibraryCore.RCon
connectionState.OnSentData.Reset(); connectionState.OnSentData.Reset();
connectionState.OnReceivedData.Reset(); connectionState.OnReceivedData.Reset();
connectionState.ConnectionAttempts++; connectionState.ConnectionAttempts++;
connectionState.BytesReadPerSegment.Clear();
#if DEBUG == true #if DEBUG == true
Log.WriteDebug($"Sending {payload.Length} bytes to [{this.Endpoint}] ({connectionState.ConnectionAttempts}/{StaticHelpers.AllowedConnectionFails})"); _log.WriteDebug($"Sending {payload.Length} bytes to [{this.Endpoint}] ({connectionState.ConnectionAttempts}/{StaticHelpers.AllowedConnectionFails})");
#endif #endif
try try
{ {
response = await SendPayloadAsync(payload, waitForResponse); response = await SendPayloadAsync(payload, waitForResponse);
if (response.Length == 0 && waitForResponse) if ((response.Length == 0 || response[0].Length == 0) && waitForResponse)
{ {
throw new NetworkException("Expected response but got 0 bytes back"); throw new NetworkException("Expected response but got 0 bytes back");
} }
@ -182,7 +168,15 @@ namespace SharedLibraryCore.RCon
} }
} }
string responseString = defaultEncoding.GetString(response, 0, response.Length) + '\n'; if (response.Length == 0)
{
_log.WriteWarning($"Received empty response for request [{type.ToString()}, {parameters}, {Endpoint.ToString()}]");
return new string[0];
}
string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ?
ReassembleSegmentedStatus(response) :
_gameEncoding.GetString(response[0]) + '\n';
// note: not all games respond if the pasword is wrong or not set // note: not all games respond if the pasword is wrong or not set
if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword")) if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword"))
@ -195,18 +189,52 @@ namespace SharedLibraryCore.RCon
throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_NOTSET"]); throw new NetworkException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_NOTSET"]);
} }
if (responseString.Contains(Config.ServerNotRunningResponse)) if (responseString.Contains(config.ServerNotRunningResponse))
{ {
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString())); throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString()));
} }
string[] splitResponse = responseString.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) string responseHeaderMatch = Regex.Match(responseString, config.CommandPrefixes.RConResponse).Value;
.Select(line => line.Trim()) string[] headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : responseHeaderMatch);
.ToArray();
if (headerSplit.Length != 2)
{
throw new NetworkException("Unexpected response header from server");
}
string[] splitResponse = headerSplit.Last().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
return splitResponse; return splitResponse;
} }
private async Task<byte[]> SendPayloadAsync(byte[] payload, bool waitForResponse) /// <summary>
/// reassembles broken status segments into the 'correct' ordering
/// <remarks>this is primarily for T7, and is really only reliable for 2 segments</remarks>
/// </summary>
/// <param name="segments">array of segmented byte arrays</param>
/// <returns></returns>
public string ReassembleSegmentedStatus(byte[][] segments)
{
var splitStatusStrings = new List<string>();
foreach (byte[] segment in segments)
{
string responseString = _gameEncoding.GetString(segment, 0, segment.Length);
var statusHeaderMatch = config.StatusHeader.PatternMatcher.Match(responseString);
if (statusHeaderMatch.Success)
{
splitStatusStrings.Insert(0, responseString.TrimEnd('\0'));
}
else
{
splitStatusStrings.Add(responseString.Replace(config.CommandPrefixes.RConResponse, "").TrimEnd('\0'));
}
}
return string.Join("", splitStatusStrings);
}
private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse)
{ {
var connectionState = ActiveQueries[this.Endpoint]; var connectionState = ActiveQueries[this.Endpoint];
var rconSocket = (Socket)connectionState.SendEventArgs.UserToken; var rconSocket = (Socket)connectionState.SendEventArgs.UserToken;
@ -240,7 +268,7 @@ namespace SharedLibraryCore.RCon
if (!waitForResponse) if (!waitForResponse)
{ {
return new byte[0]; return new byte[0][];
} }
connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer); connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer);
@ -250,7 +278,7 @@ namespace SharedLibraryCore.RCon
if (receiveDataPending) if (receiveDataPending)
{ {
if (!await Task.Run(() => connectionState.OnReceivedData.Wait(StaticHelpers.SocketTimeout))) if (!await Task.Run(() => connectionState.OnReceivedData.Wait(10000)))
{ {
rconSocket.Close(); rconSocket.Close();
throw new NetworkException("Timed out waiting for response", rconSocket); throw new NetworkException("Timed out waiting for response", rconSocket);
@ -259,25 +287,75 @@ namespace SharedLibraryCore.RCon
rconSocket.Close(); rconSocket.Close();
byte[] response = connectionState.ReceiveBuffer var responseList = new List<byte[]>();
.Take(connectionState.ReceiveEventArgs.BytesTransferred) int totalBytesRead = 0;
.ToArray();
return response; foreach (int bytesRead in connectionState.BytesReadPerSegment)
{
responseList.Add(connectionState.ReceiveBuffer
.Skip(totalBytesRead)
.Take(bytesRead)
.ToArray());
totalBytesRead += bytesRead;
}
return responseList.ToArray();
} }
private void OnDataReceived(object sender, SocketAsyncEventArgs e) private void OnDataReceived(object sender, SocketAsyncEventArgs e)
{ {
#if DEBUG == true #if DEBUG == true
Log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}"); _log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}");
#endif #endif
ActiveQueries[this.Endpoint].OnReceivedData.Set();
// this occurs when we close the socket
if (e.BytesTransferred == 0)
{
ActiveQueries[this.Endpoint].OnReceivedData.Set();
return;
}
if (sender is Socket sock)
{
var state = ActiveQueries[this.Endpoint];
state.BytesReadPerSegment.Add(e.BytesTransferred);
try
{
// we still have available data so the payload was segmented
if (sock.Available > 0)
{
state.ReceiveEventArgs.SetBuffer(state.ReceiveBuffer, e.BytesTransferred, state.ReceiveBuffer.Length - e.BytesTransferred);
if (!sock.ReceiveAsync(state.ReceiveEventArgs))
{
#if DEBUG == true
_log.WriteDebug($"Read {state.ReceiveEventArgs.BytesTransferred} synchronous bytes from {e.RemoteEndPoint.ToString()}");
#endif
// we need to increment this here because the callback isn't executed if there's no pending IO
state.BytesReadPerSegment.Add(state.ReceiveEventArgs.BytesTransferred);
ActiveQueries[this.Endpoint].OnReceivedData.Set();
}
}
else
{
ActiveQueries[this.Endpoint].OnReceivedData.Set();
}
}
catch (ObjectDisposedException)
{
ActiveQueries[this.Endpoint].OnReceivedData.Set();
}
}
} }
private void OnDataSent(object sender, SocketAsyncEventArgs e) private void OnDataSent(object sender, SocketAsyncEventArgs e)
{ {
#if DEBUG == true #if DEBUG == true
Log.WriteDebug($"Sent {e.Buffer?.Length} bytes to {e.ConnectSocket?.RemoteEndPoint?.ToString()}"); _log.WriteDebug($"Sent {e.Buffer?.Length} bytes to {e.ConnectSocket?.RemoteEndPoint?.ToString()}");
#endif #endif
ActiveQueries[this.Endpoint].OnSentData.Set(); ActiveQueries[this.Endpoint].OnSentData.Set();
} }

View File

@ -12,15 +12,11 @@ using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.RconParsers namespace IW4MAdmin.Application.RconParsers
{ {
#if DEBUG
public class BaseRConParser : IRConParser public class BaseRConParser : IRConParser
#else
class BaseRConParser : IRConParser
#endif
{ {
public BaseRConParser() public BaseRConParser(IParserRegexFactory parserRegexFactory)
{ {
Configuration = new DynamicRConParserConfiguration() Configuration = new DynamicRConParserConfiguration(parserRegexFactory)
{ {
CommandPrefixes = new CommandPrefix() CommandPrefixes = new CommandPrefix()
{ {
@ -35,6 +31,7 @@ namespace IW4MAdmin.Application.RconParsers
RConGetStatus = "ÿÿÿÿgetstatus", RConGetStatus = "ÿÿÿÿgetstatus",
RConGetInfo = "ÿÿÿÿgetinfo", RConGetInfo = "ÿÿÿÿgetinfo",
RConResponse = "ÿÿÿÿprint", RConResponse = "ÿÿÿÿprint",
RconGetInfoResponseHeader = "ÿÿÿÿinfoResponse"
}, },
ServerNotRunningResponse = "Server is not running." ServerNotRunningResponse = "Server is not running."
}; };
@ -54,32 +51,47 @@ namespace IW4MAdmin.Application.RconParsers
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5);
Configuration.StatusHeader.Pattern = "num +score +ping +guid +name +lastmsg +address +qport +rate *";
Configuration.GametypeStatus.Pattern = "";
Configuration.MapStatus.Pattern = @"map: (([a-z]|_|\d)+)"; Configuration.MapStatus.Pattern = @"map: (([a-z]|_|\d)+)";
Configuration.MapStatus.AddMapping(ParserRegex.GroupType.RConStatusMap, 1); Configuration.MapStatus.AddMapping(ParserRegex.GroupType.RConStatusMap, 1);
if (!Configuration.DefaultDvarValues.ContainsKey("mapname"))
{
Configuration.DefaultDvarValues.Add("mapname", "Unknown");
}
} }
public IRConParserConfiguration Configuration { get; set; } public IRConParserConfiguration Configuration { get; set; }
public virtual string Version { get; set; } = "CoD"; public virtual string Version { get; set; } = "CoD";
public Game GameName { get; set; } = Game.COD; public Game GameName { get; set; } = Game.COD;
public bool CanGenerateLogPath { get; set; } = true; public bool CanGenerateLogPath { get; set; } = true;
public string Name { get; set; } = "Call of Duty";
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command) public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command)
{ {
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command); var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
return response.Skip(1).ToArray(); return response.Skip(1).ToArray();
} }
public async Task<Dvar<T>> GetDvarAsync<T>(Connection connection, string dvarName) public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default)
{ {
string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName); string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName);
string response = string.Join('\n', lineSplit.Skip(1)); string response = string.Join('\n', lineSplit).TrimEnd('\0');
var match = Regex.Match(response, Configuration.Dvar.Pattern); var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (!lineSplit[0].Contains(Configuration.CommandPrefixes.RConResponse) || if (response.Contains("Unknown command") ||
response.Contains("Unknown command") ||
!match.Success) !match.Success)
{ {
if (fallbackValue != null)
{
return new Dvar<T>()
{
Name = dvarName,
Value = fallbackValue
};
}
throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName)); throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName));
} }
@ -89,7 +101,6 @@ namespace IW4MAdmin.Application.RconParsers
string removeTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", ""); string removeTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", "");
value = removeTrailingColorCode(value); value = removeTrailingColorCode(value);
defaultValue = removeTrailingColorCode(defaultValue); defaultValue = removeTrailingColorCode(defaultValue);
latchedValue = removeTrailingColorCode(latchedValue); latchedValue = removeTrailingColorCode(latchedValue);
@ -104,7 +115,7 @@ namespace IW4MAdmin.Application.RconParsers
}; };
} }
public virtual async Task<(List<EFClient>, string)> GetStatusAsync(Connection connection) public virtual async Task<(List<EFClient>, string, string)> GetStatusAsync(IRConConnection connection)
{ {
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS); string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
#if DEBUG #if DEBUG
@ -113,7 +124,7 @@ namespace IW4MAdmin.Application.RconParsers
Console.WriteLine(line); Console.WriteLine(line);
} }
#endif #endif
return (ClientsFromStatus(response), MapFromStatus(response)); return (ClientsFromStatus(response), MapFromStatus(response), GameTypeFromStatus(response));
} }
private string MapFromStatus(string[] response) private string MapFromStatus(string[] response)
@ -131,45 +142,76 @@ namespace IW4MAdmin.Application.RconParsers
return map; return map;
} }
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue) private string GameTypeFromStatus(string[] response)
{ {
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, $"{dvarName} {dvarValue}")).Length > 0; if (string.IsNullOrWhiteSpace(Configuration.GametypeStatus.Pattern))
{
return null;
}
string gametype = null;
foreach (var line in response)
{
var regex = Regex.Match(line, Configuration.GametypeStatus.Pattern);
if (regex.Success)
{
gametype = regex.Groups[Configuration.GametypeStatus.GroupMapping[ParserRegex.GroupType.RConStatusGametype]].ToString();
}
}
return gametype;
}
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue)
{
string dvarString = (dvarValue is string str)
? $"{dvarName} \"{str}\""
: $"{dvarName} {dvarValue.ToString()}";
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0;
} }
private List<EFClient> ClientsFromStatus(string[] Status) private List<EFClient> ClientsFromStatus(string[] Status)
{ {
List<EFClient> StatusPlayers = new List<EFClient>(); List<EFClient> StatusPlayers = new List<EFClient>();
if (Status.Length < 4) bool parsedHeader = false;
{
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNEXPECTED_STATUS"]);
}
int validMatches = 0;
foreach (string statusLine in Status) foreach (string statusLine in Status)
{ {
string responseLine = statusLine.Trim(); string responseLine = statusLine.Trim();
var regex = Regex.Match(responseLine, Configuration.Status.Pattern, RegexOptions.IgnoreCase); if (Configuration.StatusHeader.PatternMatcher.Match(responseLine).Success)
if (regex.Success)
{ {
validMatches++; parsedHeader = true;
int clientNumber = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]].Value); continue;
int score = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]].Value); }
var match = Configuration.Status.PatternMatcher.Match(responseLine);
if (match.Success)
{
int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]);
int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]);
int ping = 999; int ping = 999;
// their state can be CNCT, ZMBI etc // their state can be CNCT, ZMBI etc
if (regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value.Length <= 3) if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3)
{ {
ping = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value); ping = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]]);
} }
long networkId; long networkId;
string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString;
try try
{ {
networkId = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].Value.ConvertGuidToLong(); networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkId = networkIdString.IsBotGuid() ?
name.GenerateGuidFromString() :
networkIdString.ConvertGuidToLong(Configuration.GuidNumberStyle);
} }
catch (FormatException) catch (FormatException)
@ -177,8 +219,7 @@ namespace IW4MAdmin.Application.RconParsers
continue; continue;
} }
string name = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].Value.Trim(); int? ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
int? ip = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Value.Split(':')[0].ConvertToIP();
var client = new EFClient() var client = new EFClient()
{ {
@ -194,25 +235,33 @@ namespace IW4MAdmin.Application.RconParsers
State = EFClient.ClientState.Connecting State = EFClient.ClientState.Connecting
}; };
#if DEBUG client.SetAdditionalProperty("BotGuid", networkIdString);
if (client.NetworkId < 1000 && client.NetworkId > 0)
{
client.IPAddress = 2147483646;
client.Ping = 0;
}
#endif
StatusPlayers.Add(client); StatusPlayers.Add(client);
} }
} }
// this happens if status is requested while map is rotating // this can happen if status is requested while map is rotating and we get a log dump back
if (Status.Length > 5 && validMatches == 0) if (!parsedHeader)
{ {
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_ROTATING_MAP"]); throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNEXPECTED_STATUS"]);
} }
return StatusPlayers; return StatusPlayers;
} }
public string GetOverrideDvarName(string dvarName)
{
if (Configuration.OverrideDvarNameMapping.ContainsKey(dvarName))
{
return Configuration.OverrideDvarNameMapping[dvarName];
}
return dvarName;
}
public T GetDefaultDvarValue<T>(string dvarName) => Configuration.DefaultDvarValues.ContainsKey(dvarName) ?
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
default;
} }
} }

View File

@ -1,4 +1,6 @@
namespace IW4MAdmin.Application.RconParsers using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.RconParsers
{ {
/// <summary> /// <summary>
/// empty implementation of the IW4RConParser /// empty implementation of the IW4RConParser
@ -6,5 +8,8 @@
/// </summary> /// </summary>
sealed internal class DynamicRConParser : BaseRConParser sealed internal class DynamicRConParser : BaseRConParser
{ {
public DynamicRConParser(IParserRegexFactory parserRegexFactory) : base(parserRegexFactory)
{
}
} }
} }

View File

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

View File

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

View File

@ -1,214 +0,0 @@
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

@ -1,99 +0,0 @@
<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

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

View File

@ -1,7 +0,0 @@
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

@ -1,170 +0,0 @@
#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
#include maps\mp\gametypes\_playerlogic;
init()
{
/*
SetDvarIfUninitialized("sv_team_balance_assignments", "");
SetDvarIfUninitialized("sv_iw4madmin_serverid", 0);
SetDvarIfUninitialized("sv_iw4madmin_apiurl", "http://127.0.0.1:1624/api/gsc/");
level.apiUrl = GetDvar("sv_iw4madmin_apiurl");
level thread WaitForCommand();
level thread onPlayerConnect();
level thread onPlayerDisconnect();
*/
}
onPlayerConnect()
{
for(;;)
{
level waittill( "connected", player );
player thread onJoinedTeam();
}
}
onPlayerDisconnect()
{
for(;;)
{
level waittill( "disconnected", player );
logPrint("player disconnected\n");
level.players[0] SetTeamBalanceAssignments(true);
}
}
onJoinedTeam()
{
self endon("disconnect");
for(;;)
{
self waittill( "joined_team" );
self SetTeamBalanceAssignments(false);
}
}
SetTeamBalanceAssignments(isDisconnect)
{
assignments = GetDvar("sv_team_balance_assignments");
dc = "";
if (isDisconnect)
{
dc = "&isDisconnect=true";
}
url = level.apiUrl + "GetTeamAssignments/" + self.guid + "/?teams=" + assignments + dc + "&serverId=" + GetDvar("sv_iw4madmin_serverid");
newAssignments = GetHttpString(url);
SetDvar("sv_team_balance_assignments", newAssignments.data);
if (newAssignments.success)
{
BalanceTeams(strtok(newAssignments.data, ","));
}
}
WaitForCommand()
{
for(;;)
{
commandInfo = strtok(getDvar("sv_iw4madmin_command"), ";");
command = commandInfo[0];
commandArgs = strtok(commandInfo[1], ",");
switch(command)
{
case "balance":
BalanceTeams(commandArgs);
break;
case "alert":
//clientId alertType sound message
SendAlert(commandArgs[0], commandArgs[1], commandArgs[2], commandArgs[3]);
break;
}
setDvar("sv_iw4madmin_command", "");
wait(1);
}
}
SendAlert(clientId, alertType, sound, message)
{
client = getPlayerFromClientNum(clientId);
client thread playLeaderDialogOnPlayer(sound, client.team);
client playLocalSound(sound);
client iPrintLnBold("^1" + alertType + ": ^3" + message);
}
GetHttpString(url)
{
response = spawnStruct();
response.success = false;
response.data = undefined;
logPrint("Making request to " + url + "\n");
request = httpGet(url);
request waittill("done", success, data);
if(success != 0){
logPrint("Request succeeded\n");
response.success = true;
response.data = data;
}
else
{
logPrint("Request failed\n");
}
return response;
}
BalanceTeams(commandArgs)
{
if (level.teamBased)
{
printOnPlayers("^5Balancing Teams...");
for (i = 0; i < commandArgs.size; i+= 2)
{
teamNum = int(commandArgs[i+1]);
clientNum = int(commandArgs[i]);
//printOnPlayers("[" + teamNum + "," + clientNum + "]");
if (teamNum == 2)
{
newTeam = "allies";
}
else
{
newTeam = "axis";
}
player = getPlayerFromClientNum(clientNum);
//if (!isPlayer(player))
// continue;
switch (newTeam)
{
case "axis":
if (player.team != "axis")
{
//printOnPlayers("moving " + player.name + " to axis");
player[[level.axis]]();
}
break;
case "allies":
if (player.team != "allies")
{
//printOnPlayers("moving " + player.name + " to allies");
player[[level.allies]]();
}
break;
}
}
}
}

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

@ -230,14 +230,14 @@ Process_Hit( type, attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon )
Callback_PlayerDamage( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime ) 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 ) if ( self.health - iDamage > 0 )
{ {
self Process_Hit( "Damage", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon ); isFriendlyFire = level.teamBased && isDefined( attacker ) && ( self != attacker ) && isDefined( attacker.team ) && ( self.pers[ "team" ] == attacker.team );
if ( !isFriendlyFire )
{
self Process_Hit( "Damage", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
}
} }
self maps\mp\gametypes\_damage::Callback_PlayerDamage( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime ); self maps\mp\gametypes\_damage::Callback_PlayerDamage( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime );

View File

@ -0,0 +1,283 @@
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
#include common_scripts\utility;
init()
{
level.clientid = 0;
level thread onplayerconnect();
level thread IW4MA_init();
}
onplayerconnect()
{
for ( ;; )
{
level waittill( "connecting", player );
player.clientid = level.clientid;
level.clientid++;
}
}
IW4MA_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 IW4MA_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;
}
//Does not exist in T6
SetDvarIfUninitialized(dvar, val)
{
curval = getDvar(dvar);
if (curval == "")
SetDvar(dvar,val);
}
IW4MA_onPlayerConnect( player )
{
for( ;; )
{
level waittill( "connected", player );
player thread waitForFrameThread();
//player thread waitForAttack();
}
}
//Does not work in T6
/*waitForAttack()
{
self endon( "disconnect" );
self.lastAttackTime = 0;
for( ;; )
{
self notifyOnPlayerCommand( "player_shot", "+attack" );
self waittill( "player_shot" );
self.lastAttackTime = getTime();
}
}*/
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 = 100;//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 = false;
if(!isPlayer(attacker))
{
isKillstreakKill = true;
}
if(maps/mp/killstreaks/_killstreaks::iskillstreakweapon(sWeapon))
{
isKillstreakKill = true;
}
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, boneIndex )
{
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/_globallogic_player::callback_playerdamage]]( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex );
}
Callback_PlayerKilled(eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration)
{
Process_Hit( "Kill", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
self [[maps/mp/gametypes/_globallogic_player::callback_playerkilled]]( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration );
}
Callback_PlayerDisconnect()
{
level notify( "disconnected", self );
self [[maps/mp/gametypes/_globallogic_player::callback_playerdisconnect]]();
}

View File

@ -1,118 +0,0 @@
<?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

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

View File

@ -1,118 +0,0 @@
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

@ -1,16 +0,0 @@
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

@ -1,29 +0,0 @@
#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

@ -1,14 +0,0 @@
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

@ -1,12 +0,0 @@
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

@ -1,15 +0,0 @@
"""
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

@ -6,10 +6,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
GameFiles\IW4x\userraw\_commands.gsc = GameFiles\IW4x\userraw\_commands.gsc GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc
GameFiles\IW4x\userraw\_customcallbacks.gsc = GameFiles\IW4x\userraw\_customcallbacks.gsc GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc
azure-pipelines.yml = azure-pipelines.yml
PostPublish.ps1 = PostPublish.ps1 PostPublish.ps1 = PostPublish.ps1
pre-release-pipeline.yml = pre-release-pipeline.yml
README.md = README.md README.md = README.md
RunPublishPre.cmd = RunPublishPre.cmd RunPublishPre.cmd = RunPublishPre.cmd
RunPublishRelease.cmd = RunPublishRelease.cmd RunPublishRelease.cmd = RunPublishRelease.cmd
@ -30,25 +30,23 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProfanityDeterment", "Plugi
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Login", "Plugins\Login\Login.csproj", "{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Login", "Plugins\Login\Login.csproj", "{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}"
EndProject EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Master", "Master\Master.pyproj", "{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}"
EndProject
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}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
Plugins\ScriptPlugins\ActionOnReport.js = Plugins\ScriptPlugins\ActionOnReport.js
Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js
Plugins\ScriptPlugins\ParserIW4x.js = Plugins\ScriptPlugins\ParserIW4x.js Plugins\ScriptPlugins\ParserIW4x.js = Plugins\ScriptPlugins\ParserIW4x.js
Plugins\ScriptPlugins\ParserPIW5.js = Plugins\ScriptPlugins\ParserPIW5.js
Plugins\ScriptPlugins\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js Plugins\ScriptPlugins\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js
Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js
Plugins\ScriptPlugins\ParserT7.js = Plugins\ScriptPlugins\ParserT7.js
Plugins\ScriptPlugins\ParserTeknoMW3.js = Plugins\ScriptPlugins\ParserTeknoMW3.js Plugins\ScriptPlugins\ParserTeknoMW3.js = Plugins\ScriptPlugins\ParserTeknoMW3.js
Plugins\ScriptPlugins\SampleScriptPluginCommand.js = Plugins\ScriptPlugins\SampleScriptPluginCommand.js
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
EndProjectSection EndProjectSection
EndProject 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{A848FCF1-8527-4AA8-A1AA-50D29695C678}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatsWeb", "Plugins\Web\StatsWeb\StatsWeb.csproj", "{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatsWeb", "Plugins\Web\StatsWeb\StatsWeb.csproj", "{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}"
@ -57,6 +55,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveRadar", "Plugins\LiveRadar\LiveRadar.csproj", "{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveRadar", "Plugins\LiveRadar\LiveRadar.csproj", "{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3065279E-17F0-4CE0-AF5B-014E04263D77}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationTests", "Tests\ApplicationTests\ApplicationTests.csproj", "{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -241,53 +243,8 @@ Global
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x64.Build.0 = Release|Any CPU {D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x64.Build.0 = Release|Any CPU
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x86.ActiveCfg = Release|Any CPU {D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x86.ActiveCfg = Release|Any CPU
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x86.Build.0 = Release|Any CPU {D9F2ED28-6FA5-40CA-9912-E7A849147AB1}.Release|x86.Build.0 = Release|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|x64.ActiveCfg = Debug|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|x64.Build.0 = Debug|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|x86.ActiveCfg = Debug|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Debug|x86.Build.0 = Debug|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Prerelease|Mixed Platforms.ActiveCfg = Prerelease|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Prerelease|Mixed Platforms.Build.0 = Prerelease|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Prerelease|x64.ActiveCfg = Prerelease|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Prerelease|x64.Build.0 = Prerelease|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Prerelease|x86.ActiveCfg = Prerelease|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Prerelease|x86.Build.0 = Prerelease|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|Any CPU.Build.0 = Release|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|x64.ActiveCfg = Release|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|x64.Build.0 = Release|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|x86.ActiveCfg = Release|Any CPU
{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}.Release|x86.Build.0 = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|x64.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|x64.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|x86.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Debug|x86.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|x64.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Prerelease|x86.Build.0 = Debug|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x64.ActiveCfg = Release|Any CPU
{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.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.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.Build.0 = 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.ActiveCfg = Debug|Any CPU
@ -295,6 +252,7 @@ Global
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x86.ActiveCfg = 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}.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.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.ActiveCfg = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Mixed Platforms.Build.0 = 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.ActiveCfg = Debug|Any CPU
@ -302,34 +260,13 @@ Global
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x86.ActiveCfg = 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}.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.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.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Mixed Platforms.Build.0 = 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.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = 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.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = 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.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
@ -402,6 +339,29 @@ Global
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x64.Build.0 = 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.ActiveCfg = Release|Any CPU
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x86.Build.0 = Release|Any CPU {00A1FED2-2254-4AF7-A5DB-2357FA7C88CD}.Release|x86.Build.0 = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x64.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x64.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x86.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Debug|x86.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x64.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Prerelease|x86.Build.0 = Debug|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Any CPU.Build.0 = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x64.ActiveCfg = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x64.Build.0 = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x86.ActiveCfg = Release|Any CPU
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -411,13 +371,13 @@ Global
{179140D3-97AA-4CB4-8BF6-A0C73CA75701} = {26E8B310-269E-46D4-A612-24601F16065F} {179140D3-97AA-4CB4-8BF6-A0C73CA75701} = {26E8B310-269E-46D4-A612-24601F16065F}
{958FF7EC-0226-4E85-A85B-B84EC768197D} = {26E8B310-269E-46D4-A612-24601F16065F} {958FF7EC-0226-4E85-A85B-B84EC768197D} = {26E8B310-269E-46D4-A612-24601F16065F}
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {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} {6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F}
{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {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} {A848FCF1-8527-4AA8-A1AA-50D29695C678} = {26E8B310-269E-46D4-A612-24601F16065F}
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B} = {A848FCF1-8527-4AA8-A1AA-50D29695C678} {776B348B-F818-4A0F-A625-D0AF8BAD3E9B} = {A848FCF1-8527-4AA8-A1AA-50D29695C678}
{F5815359-CFC7-44B4-9A3B-C04BACAD5836} = {26E8B310-269E-46D4-A612-24601F16065F} {F5815359-CFC7-44B4-9A3B-C04BACAD5836} = {26E8B310-269E-46D4-A612-24601F16065F}
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD} = {26E8B310-269E-46D4-A612-24601F16065F} {00A1FED2-2254-4AF7-A5DB-2357FA7C88CD} = {26E8B310-269E-46D4-A612-24601F16065F}
{581FA7AF-FEF6-483C-A7D0-2D13EF50801B} = {3065279E-17F0-4CE0-AF5B-014E04263D77}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87} SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

View File

@ -1,173 +0,0 @@
<?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>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>master\runserver.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<LaunchProvider>Standard Python launcher</LaunchProvider>
<WebBrowserUrl>http://localhost</WebBrowserUrl>
<OutputPath>.</OutputPath>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<Name>Master</Name>
<RootNamespace>Master</RootNamespace>
<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>
<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="master\context\base.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\context\history.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\context\__init__.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\models\instancemodel.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\models\servermodel.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\models\__init__.py">
<SubType>Code</SubType>
</Compile>
<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">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\resources\localization.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\resources\null.py">
<SubType>Code</SubType>
</Compile>
<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>
<Compile Include="master\schema\serverschema.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\schema\__init__.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\__init__.py" />
<Compile Include="master\views.py" />
</ItemGroup>
<ItemGroup>
<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\" />
</ItemGroup>
<ItemGroup>
<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" />
</ItemGroup>
<ItemGroup>
<Interpreter Include="env_master\">
<Id>env_master</Id>
<Version>3.6</Version>
<Description>env_master (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
</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

@ -1,19 +0,0 @@
"""
The flask application package.
"""
from flask import Flask
from flask_restful import Resource, Api
from flask_jwt_extended import JWTManager
from master.context.base import Base
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'my key!'
app.config['PROPAGATE_EXCEPTIONS'] = True
jwt = JWTManager(app)
api = Api(app)
ctx = Base()
#config = json.load(open('./master/config/master.json'))
import master.routes
import master.views

View File

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

View File

@ -1 +0,0 @@

View File

@ -1,89 +0,0 @@
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
from master.context.history import History
from master.schema.instanceschema import InstanceSchema
import time
class Base():
def __init__(self):
self.history = History()
self.instance_list = {}
self.token_list = {}
self.scheduler = BackgroundScheduler()
self.scheduler.start()
self.scheduler.add_job(
func=self._remove_staleinstances,
trigger=IntervalTrigger(seconds=60),
id='stale_instance_remover',
name='Remove stale instances if no heartbeat in 60 seconds',
replace_existing=True
)
self.scheduler.add_job(
func=self._update_history_count,
trigger=IntervalTrigger(seconds=30),
id='update history',
name='update client and instance count every 30 seconds',
replace_existing=True
)
def _update_history_count(self):
servers = [instance.servers for instance in self.instance_list.values()]
servers = [inner for outer in servers for inner in outer]
client_num = 0
# force it being a number
for server in servers:
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 > 60:
print('[_remove_staleinstances] removing stale instance {id}'.format(id=key))
del self.instance_list[key]
del self.token_list[key]
print('[_remove_staleinstances] {count} active instances'.format(count=len(self.instance_list.items())))
def get_instances(self):
return self.instance_list.values()
def get_instance_count(self):
return self.instance_list.count
def get_instance(self, id):
return self.instance_list[id]
def instance_exists(self, instance_id):
if instance_id in self.instance_list.keys():
return instance_id
else:
False
def add_instance(self, instance):
if instance.id in self.instance_list:
print('[add_instance] instance {id} already added, updating instead'.format(id=instance.id))
return self.update_instance(instance)
else:
print('[add_instance] adding instance {id}'.format(id=instance.id))
self.instance_list[instance.id] = instance
def update_instance(self, instance):
if instance.id not in self.instance_list:
print('[update_instance] instance {id} not added, adding instead'.format(id=instance.id))
return self.add_instance(instance)
else:
print('[update_instance] updating instance {id}'.format(id=instance.id))
self.instance_list[instance.id] = instance
def add_token(self, instance_id, token):
print('[add_token] adding {token} for id {id}'.format(token=token, id=instance_id))
self.token_list[instance_id] = token
def get_token(self, instance_id):
try:
return self.token_list[instance_id]
except KeyError:
return False

View File

@ -1,32 +0,0 @@
import time
from random import randint
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) > 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) > 20160:
self.instance_history = self.instance_history[1:]
self.instance_history.append({
'count' : instance_num,
'time' : int(time.time())
})

View File

@ -1 +0,0 @@

View File

@ -1,12 +0,0 @@
import time
class InstanceModel(object):
def __init__(self, id, version, uptime, servers):
self.id = id
self.version = version
self.uptime = uptime
self.servers = servers
self.last_heartbeat = int(time.time())
def __repr__(self):
return '<InstanceModel(id={id})>'.format(id=self.id)

View File

@ -1,16 +0,0 @@
class ServerModel(object):
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

@ -1 +0,0 @@

View File

@ -1,18 +0,0 @@
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
class Authenticate(Resource):
def post(self):
instance_id = request.json['id']
#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

@ -1,49 +0,0 @@
from flask_restful import Resource
from pygal.style import Style
from master import ctx
import pygal
import timeago
from math import ceil
class HistoryGraph(Resource):
def get(self, history_count):
try:
custom_style = Style(
background='transparent',
plot_background='transparent',
foreground='#6c757d',
foreground_strong='#6c757d',
foreground_subtle='#6c757d',
opacity='0.1',
opacity_hover='0.2',
transition='0ms',
colors=('#749363','#007acc'),
)
graph = pygal.Line(
stroke_style={'width': 0.4},
#show_dots=False,
show_legend=False,
fill=True,
style=custom_style,
disable_xml_declaration=True)
instance_count = [history['time'] for history in ctx.history.instance_history][-history_count:]
if len(instance_count) > 0:
graph.x_labels = [ timeago.format(instance_count[0])]
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

@ -1,49 +0,0 @@
from flask_restful import Resource
from flask import request
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):
if id is None:
schema = InstanceSchema(many=True)
instances = schema.dump(ctx.get_instances())
return instances
else:
try:
instance = ctx.get_instance(id)
return InstanceSchema().dump(instance)
except KeyError:
return {'message' : 'instance not found'}, 404
@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
ctx.update_instance(instance)
return { 'message' : 'instance updated successfully' }, 200
@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
ctx.add_instance(instance)
return { 'message' : 'instance added successfully' }, 200

View File

@ -1,59 +0,0 @@
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

@ -1,11 +0,0 @@
from flask_restful import Resource
from master.models.servermodel import ServerModel
from master.schema.serverschema import ServerSchema
from master.models.instancemodel import InstanceModel
from master.schema.instanceschema import InstanceSchema
class Null(Resource):
def get(self):
server = ServerModel(1, 'T6M', 'test', 0, 18, 'mp_test', 'tdm')
instance = InstanceModel(1, 1.5, 132, [server])
return InstanceSchema().dump(instance)

View File

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

View File

@ -1,10 +0,0 @@
from flask_restful import Resource
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']
}, 200

View File

@ -1,17 +0,0 @@
from master import api
from master.resources.null import Null
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

@ -1,9 +0,0 @@
"""
This script runs the Master application using a development server.
"""
from os import environ
from master import app
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80, debug=True)

View File

@ -1 +0,0 @@

View File

@ -1,29 +0,0 @@
from marshmallow import Schema, fields, post_load, validate
from master.models.instancemodel import InstanceModel
from master.schema.serverschema import ServerSchema
class InstanceSchema(Schema):
id = fields.String(
required=True
)
version = fields.Float(
required=True,
validate=validate.Range(1.0, 10.0, 'invalid version number')
)
servers = fields.Nested(
ServerSchema,
many=True,
validate=validate.Length(0, 32, 'invalid server count')
)
uptime = fields.Int(
required=True,
validate=validate.Range(0, 2147483647, 'invalid uptime')
)
last_heartbeat = fields.Int(
required=False
)
@post_load
def make_instance(self, data):
return InstanceModel(**data)

View File

@ -1,47 +0,0 @@
from marshmallow import Schema, fields, post_load, validate
from master.models.servermodel import ServerModel
class ServerSchema(Schema):
id = fields.Int(
required=True,
validate=validate.Range(0, 25525525525565535, 'invalid id')
)
ip = fields.Str(
required=True
)
port = fields.Int(
required=True,
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, 5, 'invalid game name')
)
hostname = fields.String(
required=True,
validate=validate.Length(1, 128, 'invalid hostname')
)
clientnum = fields.Int(
required=True,
validate=validate.Range(0, 128, 'invalid clientnum')
)
maxclientnum = fields.Int(
required=True,
validate=validate.Range(1, 128, 'invalid maxclientnum')
)
map = fields.String(
required=True,
validate=validate.Length(0, 64, 'invalid map name')
)
gametype = fields.String(
required=True,
validate=validate.Length(1, 16, 'invalid gametype')
)
@post_load
def make_instance(self, data):
return ServerModel(**data)

View File

@ -1,64 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<div class="row">
<div class="col-12">
<figure>
<div id="history_graph">{{history_graph|safe}}</div>
<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 class="col-12">
</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 maxPoints = 2880;
maxPoints = Math.min(maxPoints, dataPoints);
let zoomLevel = Math.floor(maxPoints);
let performingZoom = false;
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;
}
zoomLevel = Math.floor(zoomLevel * 2) <= maxPoints ? Math.floor(zoomLevel * 2) : maxPoints;
updateHistoryGraph();
});
$('#history_graph_zoom_in').click(function () {
if (performingZoom === true) {
return false;
}
zoomLevel = zoomLevel / 2 > 2 ? Math.ceil(zoomLevel / 2) : 2;
updateHistoryGraph();
});
</script>
{% endblock %}

View File

@ -1,45 +0,0 @@
<!DOCTYPE html>
<html class="bg-dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IW4MAdmin Master | {{ title }}</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/open-iconic/1.1.1/font/css/open-iconic-bootstrap.min.css" integrity="sha256-BJ/G+e+y7bQdrYkS2RBTyNfBHpA9IuGaPmf9htub5MQ=" crossorigin="anonymous" />
<style type="text/css">
.active {
stroke-width: initial !important;
}
.oi:hover {
color: #fff !important;
}
.dot {
opacity: 0;
padding: 5px;
}
.dot:hover {
opacity: 1;
}
.tooltip-box {
fill: #343a40 !important;
}
</style>
</head>
<body class="bg-dark">
<div class="container body-content bg-dark">
{% block content %}{% endblock %}
</div>
<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

@ -1,113 +0,0 @@
{% 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

@ -1,35 +0,0 @@
"""
Routes and views for the flask application.
"""
from datetime import datetime
from flask import render_template
from master import app, ctx
from master.resources.history_graph import HistoryGraph
from collections import defaultdict
@app.route('/')
def home():
_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'],
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,26 +0,0 @@
aniso8601==3.0.2
APScheduler==3.5.3
certifi==2018.10.15
chardet==3.0.4
click==6.7
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.3
psutil==5.4.8
pygal==2.4.0
PyJWT==1.4.2
pytz==2018.7
requests==2.20.0
setuptools==40.5.0
six==1.11.0
timeago==1.0.8
tzlocal==1.5.1
urllib3==1.24
Werkzeug==0.15.3

View File

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" /> <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.4" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

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

View File

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

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