Compare commits

..

61 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
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
244 changed files with 7759 additions and 4244 deletions

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

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

1
.gitignore vendored
View File

@ -243,3 +243,4 @@ launchSettings.json
/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.Text;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using RestEase;
@ -37,16 +35,6 @@ namespace IW4MAdmin.Application.API.Master
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>
/// Defines the capabilities of the master API
/// </summary>

View File

@ -25,20 +25,20 @@
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-1632" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.3" />
<PackageReference Include="RestEase" Version="1.4.10" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" />
<PackageReference Include="RestEase" Version="1.5.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" />
</ItemGroup>
<PropertyGroup>
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation>
<LangVersion>7.1</LangVersion>
<LangVersion>Latest</LangVersion>
<StartupObject></StartupObject>
</PropertyGroup>

View File

@ -1,5 +1,6 @@
using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RconParsers;
using SharedLibraryCore;
@ -12,6 +13,7 @@ using SharedLibraryCore.Dtos;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Services;
using System;
using System.Collections;
@ -43,6 +45,7 @@ namespace IW4MAdmin.Application
public string ExternalIPAddress { get; private set; }
public bool IsRestartRequested { get; private set; }
public IMiddlewareActionHandler MiddlewareActionHandler { get; }
public event EventHandler<GameEvent> OnGameEventExecuted;
private readonly List<IManagerCommand> _commands;
private readonly ILogger _logger;
private readonly List<MessageToken> MessageTokens;
@ -52,7 +55,7 @@ namespace IW4MAdmin.Application
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList;
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 CancellationTokenSource _tokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
@ -63,27 +66,30 @@ namespace IW4MAdmin.Application
private readonly IEnumerable<IRegisterEvent> _customParserEvents;
private readonly IEventHandler _eventHandler;
private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IMetaRegistration _metaRegistration;
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
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)
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>();
ClientSvc = new ClientService();
ClientSvc = new ClientService(contextFactory);
AliasSvc = new AliasService();
PenaltySvc = new PenaltyService();
ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow;
PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger) };
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, appConfigHandler.Configuration()) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication();
_logger = logger;
_metaService = new MetaService();
_metaService = metaService;
_tokenSource = new CancellationTokenSource();
_loggers.Add(0, logger);
_commands = commands.ToList();
@ -94,6 +100,8 @@ namespace IW4MAdmin.Application
_customParserEvents = customParserEvents;
_eventHandler = eventHandler;
_scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver;
Plugins = plugins;
}
@ -164,6 +172,8 @@ namespace IW4MAdmin.Application
skip:
// tell anyone waiting for the output that we're done
newEvent.Complete();
OnGameEventExecuted?.Invoke(this, newEvent);
#if DEBUG == true
Logger.WriteDebug($"Exiting event process for {newEvent.Id}");
#endif
@ -269,12 +279,12 @@ namespace IW4MAdmin.Application
{
if (plugin is ScriptPlugin scriptPlugin)
{
await scriptPlugin.Initialize(this, _scriptCommandFactory);
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
scriptPlugin.Watcher.Changed += async (sender, e) =>
{
try
{
await scriptPlugin.Initialize(this, _scriptCommandFactory);
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
}
catch (Exception ex)
@ -424,19 +434,25 @@ namespace IW4MAdmin.Application
else
{
var unsavedCommands = _commands.Where(_cmd => !cmdConfig.Commands.Keys.Contains(_cmd.GetType().Name));
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.GetType().Name,
cmdConfig.Commands.Add(cmd.CommandConfigNameForType(),
new CommandProperties()
{
Name = cmd.Name,
Alias = cmd.Alias,
MinimumPermission = cmd.Permission,
AllowImpersonation = cmd.AllowImpersonation
AllowImpersonation = cmd.AllowImpersonation,
SupportedGames = cmd.SupportedGames
});
}
@ -444,133 +460,7 @@ namespace IW4MAdmin.Application
await _commandConfiguration.Save();
#endregion
#region META
async Task<List<ProfileMeta>> getProfileMeta(int clientId, int offset, int count, DateTime? startAt)
{
var metaList = new List<ProfileMeta>();
// we don't want to return anything because it means we're trying to retrieve paged meta data
if (count > 1)
{
return metaList;
}
var lastMapMeta = await _metaService.GetPersistentMeta("LastMapPlayed", new EFClient() { ClientId = clientId });
if (lastMapMeta != null)
{
metaList.Add(new ProfileMeta()
{
Id = lastMapMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_MAP"],
Value = lastMapMeta.Value,
Show = true,
Type = ProfileMeta.MetaType.Information,
});
}
var lastServerMeta = await _metaService.GetPersistentMeta("LastServerPlayed", new EFClient() { ClientId = clientId });
if (lastServerMeta != null)
{
metaList.Add(new ProfileMeta()
{
Id = lastServerMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_SERVER"],
Value = lastServerMeta.Value,
Show = true,
Type = ProfileMeta.MetaType.Information
});
}
var client = await GetClientService().Get(clientId);
if (client == null)
{
_logger.WriteWarning($"No client found with id {clientId} when generating profile meta");
return metaList;
}
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
_metaRegistration.Register();
#region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
@ -644,85 +534,7 @@ namespace IW4MAdmin.Application
}
}
private async Task SendHeartbeat()
{
bool connected = false;
while (!_tokenSource.IsCancellationRequested)
{
if (!connected)
{
try
{
await Heartbeat.Send(this, true);
connected = true;
}
catch (Exception e)
{
connected = false;
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
}
}
else
{
try
{
await Heartbeat.Send(this);
}
catch (System.Net.Http.HttpRequestException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
}
catch (AggregateException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(RestEase.ApiException));
foreach (var ex in exceptions)
{
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
}
}
catch (RestEase.ApiException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
}
catch (Exception e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
}
}
try
{
await Task.Delay(30000, _tokenSource.Token);
}
catch { break; }
}
}
public async Task Start()
{
await Task.WhenAll(new[]
{
SendHeartbeat(),
UpdateServerStates()
});
}
public async Task Start() => await UpdateServerStates();
public void Stop()
{
@ -803,7 +615,7 @@ namespace IW4MAdmin.Application
public IEventParser GenerateDynamicEventParser(string name)
{
return new DynamicEventParser(_parserRegexFactory, _logger)
return new DynamicEventParser(_parserRegexFactory, _logger, ConfigHandler.Configuration())
{
Name = name
};

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%\*.json" "%PublishDir%\Lib\"
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"
echo making start scripts

View File

@ -1,3 +1,6 @@
set SolutionDir=%1
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,4 +1,5 @@
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System;
@ -12,11 +13,13 @@ namespace IW4MAdmin.Application.EventParsers
{
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)
public BaseEventParser(IParserRegexFactory parserRegexFactory, ILogger logger, ApplicationConfiguration appConfig)
{
_customEventRegistrations = new Dictionary<string, (string, Func<string, IEventParserConfiguration, GameEvent, GameEvent>)>();
_logger = logger;
_appConfig = appConfig;
Configuration = new DynamicEventParserConfiguration(parserRegexFactory)
{
@ -42,7 +45,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.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.TargetNetworkId, 2);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -57,7 +60,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0);(-?[0-9]+);(axis|allies|world)?;([^;]{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+|0)?;(-?[0-9]+);(axis|allies|world)?;([^;]{1,24})?;((?:[0-9]+|[a-z]+|_|\+)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.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.TargetNetworkId, 2);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -127,8 +130,7 @@ namespace IW4MAdmin.Application.EventParsers
int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
// todo: these need to defined outside of here
if (message[0] == '!' || message[0] == '@')
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
{
return new GameEvent()
{
@ -253,6 +255,7 @@ namespace IW4MAdmin.Application.EventParsers
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting,
},
Extra = originIdString,
RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true,
GameTime = gameTime,

View File

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

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

@ -13,17 +13,19 @@ namespace IW4MAdmin.Application.Factories
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)
public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService)
{
_translationLookup = translationLookup;
_rconConnectionFactory = rconConnectionFactory;
_gameLogReaderFactory = gameLogReaderFactory;
_metaService = metaService;
}
/// <summary>
@ -34,7 +36,7 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns>
public Server CreateServer(ServerConfiguration config, IManager manager)
{
return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory, _gameLogReaderFactory);
return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory, _gameLogReaderFactory, _metaService);
}
}
}

View File

@ -25,7 +25,7 @@ namespace IW4MAdmin.Application.Factories
}
/// <inheritdoc/>
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction)
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
@ -34,7 +34,7 @@ namespace IW4MAdmin.Application.Factories
Required = _arg.Item2
}).ToArray();
return new ScriptCommand(name, alias, description, permissionEnum, argsArray, executeAction, _config, _transLookup);
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction, _config, _transLookup);
}
}
}

View File

@ -43,15 +43,6 @@ namespace IW4MAdmin.Application
EventApi.OnGameEvent(gameEvent);
Task.Factory.StartNew(() => manager.ExecuteEvent(gameEvent));
/*if (!_eventLog.ContainsKey(gameEvent.Owner.EndPoint))
{
_eventLog.Add(gameEvent.Owner.EndPoint,new List<GameEvent>());
}
_eventLog[gameEvent.Owner.EndPoint].Add(gameEvent);
string serializedEvents = JsonConvert.SerializeObject(_eventLog, EventLog.BuildVcrSerializationSettings());
System.IO.File.WriteAllText("output.json", serializedEvents);*/
//Task.Run(() => GameEventHandler_GameEventAdded(this, new GameEventArgs(null, false, gameEvent)));
}
#if DEBUG
else

View File

@ -7,7 +7,6 @@ using SharedLibraryCore.Dtos;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Services;
using System;
using System.Collections.Generic;
using System.IO;
@ -26,15 +25,17 @@ namespace IW4MAdmin
private static readonly SharedLibraryCore.Localization.TranslationLookup loc = Utilities.CurrentLocalization.LocalizationIndex;
public GameLogEventDetection LogEvent;
private readonly ITranslationLookup _translationLookup;
private readonly IMetaService _metaService;
private const int REPORT_FLAG_COUNT = 4;
private int lastGameTime = 0;
public int Id { get; private set; }
public IW4MServer(IManager mgr, ServerConfiguration cfg, ITranslationLookup lookup,
IRConConnectionFactory connectionFactory, IGameLogReaderFactory gameLogReaderFactory) : base(cfg, mgr, connectionFactory, gameLogReaderFactory)
IRConConnectionFactory connectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService) : base(cfg, mgr, connectionFactory, gameLogReaderFactory)
{
_translationLookup = lookup;
_metaService = metaService;
}
override public async Task<EFClient> OnClientConnected(EFClient clientFromLog)
@ -139,7 +140,7 @@ namespace IW4MAdmin
{
try
{
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E);
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration());
}
catch (CommandException e)
@ -475,8 +476,8 @@ namespace IW4MAdmin
Time = DateTime.UtcNow
});
await new MetaService().AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin);
await new MetaService().AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin);
await _metaService.AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin);
await _metaService.AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin);
}
else if (E.Type == GameEvent.EventType.PreDisconnect)
@ -540,14 +541,18 @@ namespace IW4MAdmin
.First(_qm => _qm.Game == GameName)
.Messages[E.Data.Substring(1)];
}
catch { }
catch
{
message = E.Data.Substring(1);
}
}
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
Message = message,
Time = DateTime.UtcNow
Time = DateTime.UtcNow,
IsHidden = !string.IsNullOrEmpty(GamePassword)
});
}
}
@ -610,13 +615,10 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.Broadcast)
{
#if DEBUG == false
// this is a little ugly but I don't want to change the abstract class
if (E.Data != null)
if (!Utilities.IsDevelopment && E.Data != null) // hides broadcast when in development mode
{
await E.Owner.ExecuteCommandAsync(E.Data);
}
#endif
}
lock (ChatHistory)
@ -667,7 +669,7 @@ namespace IW4MAdmin
}
}
else if ((client.IPAddress != null && client.State == ClientState.Disconnecting) ||
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...");
@ -703,6 +705,7 @@ namespace IW4MAdmin
var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients);
UpdateMap(statusResponse.Item2);
UpdateGametype(statusResponse.Item3);
return new List<EFClient>[]
{
@ -724,6 +727,14 @@ namespace IW4MAdmin
}
}
private void UpdateGametype(string gameType)
{
if (!string.IsNullOrEmpty(gameType))
{
Gametype = gameType;
}
}
private async Task ShutdownInternal()
{
foreach (var client in GetClientsAsList())
@ -774,7 +785,7 @@ namespace IW4MAdmin
var polledClients = await PollPlayersAsync();
foreach (var disconnectingClient in polledClients[1])
foreach (var disconnectingClient in polledClients[1].Where(_client => !_client.IsZombieClient /* ignores "fake" zombie clients */))
{
disconnectingClient.CurrentServer = this;
var e = new GameEvent()
@ -805,6 +816,7 @@ namespace IW4MAdmin
Origin = client,
Owner = this,
IsBlocking = true,
Extra = client.GetAdditionalProperty<string>("BotGuid"),
Source = GameEvent.EventSource.Status
};
@ -939,7 +951,7 @@ namespace IW4MAdmin
RemoteConnection.SetConfiguration(RconParser.Configuration);
var version = await this.GetDvarAsync<string>("version");
var version = await this.GetMappedDvarValueOrDefaultAsync<string>("version");
Version = version.Value;
GameName = Utilities.GetGame(version?.Value ?? RconParser.Version);
@ -956,8 +968,7 @@ namespace IW4MAdmin
Version = RconParser.Version;
}
// these T7 specific things aren't ideal , but it's a quick fix
var svRunning = await this.GetDvarAsync("sv_running", GameName == Game.T7 ? "1" : null);
var svRunning = await this.GetMappedDvarValueOrDefaultAsync<string>("sv_running");
if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1")
{
@ -965,29 +976,18 @@ namespace IW4MAdmin
}
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("mapname", "Unknown")).Value :
infoResponse["mapname"];
int maxplayers = (GameName == Game.IW4) ? // gotta love IW4 idiosyncrasies
(await this.GetDvarAsync<int>("party_maxplayers")).Value :
infoResponse == null || !infoResponse.ContainsKey("sv_maxclients") ?
(await this.GetDvarAsync<int>("sv_maxclients")).Value :
Convert.ToInt32(infoResponse["sv_maxclients"]);
var gametype = infoResponse == null ?
(await this.GetDvarAsync("g_gametype", GameName == Game.T7 ? "" : null)).Value :
infoResponse.Where(kvp => kvp.Key.Contains("gametype")).Select(kvp => kvp.Value).First();
var basepath = await this.GetDvarAsync("fs_basepath", GameName == Game.T7 ? "" : null);
var basegame = await this.GetDvarAsync("fs_basegame", GameName == Game.T7 ? "" : null);
var game = infoResponse == null || !infoResponse.ContainsKey("fs_game") ?
(await this.GetDvarAsync("fs_game", GameName == Game.T7 ? "" : null)).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");
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)
{
@ -996,7 +996,7 @@ namespace IW4MAdmin
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
@ -1018,9 +1018,10 @@ namespace IW4MAdmin
WorkingDirectory = basepath.Value;
this.Hostname = hostname;
this.MaxClients = maxplayers;
this.FSGame = game;
this.FSGame = game.Value;
this.Gametype = gametype;
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
this.GamePassword = gamePassword.Value;
UpdateMap(mapname);
if (RconParser.CanGenerateLogPath)
@ -1070,7 +1071,7 @@ namespace IW4MAdmin
BaseGameDirectory = basegame.Value,
BasePathDirectory = basepath.Value,
GameDirectory = EventParser.Configuration.GameDirectory ?? "",
ModDirectory = game ?? "",
ModDirectory = game.Value ?? "",
LogFile = logfile.Value,
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
};
@ -1087,9 +1088,11 @@ namespace IW4MAdmin
Logger.WriteInfo($"Log file is {LogPath}");
_ = Task.Run(() => LogEvent.PollForChanges());
#if !DEBUG
Broadcast(loc["BROADCAST_ONLINE"]);
#endif
if (!Utilities.IsDevelopment)
{
Broadcast(loc["BROADCAST_ONLINE"]);
}
}
public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl)
@ -1237,6 +1240,7 @@ namespace IW4MAdmin
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);
}
}

View File

@ -11,7 +11,7 @@ namespace IW4MAdmin.Application.Localization
{
public class Configure
{
public static ITranslationLookup Initialize(bool useLocalTranslation, string customLocale = null)
public static ITranslationLookup Initialize(bool useLocalTranslation, IMasterApi apiInstance, string customLocale = null)
{
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
@ -20,8 +20,7 @@ namespace IW4MAdmin.Application.Localization
{
try
{
var api = Endpoint.Get();
var localization = api.GetLocalization(currentLocale).Result;
var localization = apiInstance.GetLocalization(currentLocale).Result;
Utilities.CurrentLocalization = localization;
return localization.LocalizationIndex;
}

View File

@ -1,15 +1,24 @@
using IW4MAdmin.Application.EventParsers;
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 RestEase;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
using SharedLibraryCore.Repositories;
using SharedLibraryCore.Services;
using Stats.Dtos;
using StatsWeb;
using System;
using System.Linq;
using System.Text;
@ -23,7 +32,6 @@ namespace IW4MAdmin.Application
public static BuildNumber Version { get; private set; } = BuildNumber.Parse(Utilities.GetVersionAsString());
public static ApplicationManager ServerManager;
private static Task ApplicationTask;
private static readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99");
private static ServiceProvider serviceProvider;
/// <summary>
@ -76,12 +84,13 @@ namespace IW4MAdmin.Application
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));
await CheckVersion(translationLookup);
await versionChecker.CheckVersion();
await ServerManager.Init();
}
@ -126,7 +135,11 @@ namespace IW4MAdmin.Application
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)
{
@ -155,6 +168,7 @@ namespace IW4MAdmin.Application
{
ServerManager.Start(),
webfrontTask,
serviceProvider.GetRequiredService<IMasterCommunication>().RunUploadStatus(ServerManager.CancellationToken)
};
await Task.WhenAll(tasks);
@ -162,68 +176,6 @@ namespace IW4MAdmin.Application
ServerManager.Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
}
/// <summary>
/// checks for latest version of the application
/// notifies user if an update is available
/// </summary>
/// <returns></returns>
private static async Task CheckVersion(ITranslationLookup translationLookup)
{
var api = API.Master.Endpoint.Get();
var loc = translationLookup;
var version = new API.Master.VersionInfo()
{
CurrentVersionStable = _fallbackVersion
};
try
{
version = await api.GetVersion(1);
}
catch (Exception e)
{
ServerManager.Logger.WriteWarning(loc["MANAGER_VERSION_FAIL"]);
while (e.InnerException != null)
{
e = e.InnerException;
}
ServerManager.Logger.WriteDebug(e.Message);
}
if (version.CurrentVersionStable == _fallbackVersion)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
#if !PRERELEASE
else if (version.CurrentVersionStable > Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString()}]");
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString()}]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#else
else if (version.CurrentVersionPrerelease > Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin-Prerelease {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString()}-pr]");
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString()}-pr]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#endif
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
}
/// <summary>
/// reads input from the console and executes entered commands on the default server
@ -231,6 +183,12 @@ namespace IW4MAdmin.Application
/// <returns></returns>
private static async Task ReadConsoleInput()
{
if (Console.IsInputRedirected)
{
ServerManager.Logger.WriteInfo("Disabling console input as it has been redirected");
return;
}
string lastCommand;
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
@ -275,7 +233,7 @@ namespace IW4MAdmin.Application
serviceCollection.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection)
.AddSingleton(new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings") as IConfigurationHandler<ApplicationConfiguration>)
.AddSingleton(new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as IConfigurationHandler<CommandConfiguration>)
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration())
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration() ?? new ApplicationConfiguration())
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>().Configuration() ?? new CommandConfiguration())
.AddSingleton<ILogger>(_serviceProvider => defaultLogger)
.AddSingleton<IPluginImporter, PluginImporter>()
@ -288,14 +246,28 @@ namespace IW4MAdmin.Application
.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<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"))
{

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

@ -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

@ -15,7 +15,7 @@ namespace IW4MAdmin.Application.Misc
{
private readonly Action<GameEvent> _executeAction;
public ScriptCommand(string name, string alias, string description, Permission permission,
public ScriptCommand(string name, string alias, string description, bool isTargetRequired, Permission permission,
CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout)
: base(config, layout)
{
@ -24,6 +24,7 @@ namespace IW4MAdmin.Application.Misc
Name = name;
Alias = alias;
Description = description;
RequiresTarget = isTargetRequired;
Permission = permission;
Arguments = args;
}

View File

@ -61,7 +61,7 @@ namespace IW4MAdmin.Application.Misc
_onProcessing.Dispose();
}
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory)
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory, IScriptPluginServiceResolver serviceResolver)
{
await _onProcessing.WaitAsync();
@ -114,6 +114,7 @@ namespace IW4MAdmin.Application.Misc
_scriptEngine.Execute(script);
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
Author = pluginObject.author;
@ -164,6 +165,11 @@ namespace IW4MAdmin.Application.Misc
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;
@ -246,6 +252,7 @@ namespace IW4MAdmin.Application.Misc
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;
@ -260,6 +267,16 @@ namespace IW4MAdmin.Application.Misc
// arguments are optional
}
try
{
targetRequired = dynamicCommand.targetRequired;
}
catch (RuntimeBinderException)
{
// arguments are optional
}
if (arguments != null)
{
foreach (var arg in dynamicCommand.arguments)
@ -284,7 +301,7 @@ namespace IW4MAdmin.Application.Misc
}
}
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, args, execute));
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

@ -14,8 +14,6 @@ namespace IW4MAdmin.Application.RconParsers
{
public class BaseRConParser : IRConParser
{
private const int MAX_FAULTY_STATUS_LINES = 7;
public BaseRConParser(IParserRegexFactory parserRegexFactory)
{
Configuration = new DynamicRConParserConfiguration(parserRegexFactory)
@ -54,12 +52,17 @@ namespace IW4MAdmin.Application.RconParsers
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.AddMapping(ParserRegex.GroupType.RConStatusMap, 1);
if (!Configuration.DefaultDvarValues.ContainsKey("mapname"))
{
Configuration.DefaultDvarValues.Add("mapname", "Unknown");
}
}
public IRConParserConfiguration Configuration { get; set; }
public virtual string Version { get; set; } = "CoD";
public Game GameName { get; set; } = Game.COD;
public bool CanGenerateLogPath { get; set; } = true;
@ -112,7 +115,7 @@ namespace IW4MAdmin.Application.RconParsers
};
}
public virtual async Task<(List<EFClient>, string)> GetStatusAsync(IRConConnection connection)
public virtual async Task<(List<EFClient>, string, string)> GetStatusAsync(IRConConnection connection)
{
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
#if DEBUG
@ -121,7 +124,7 @@ namespace IW4MAdmin.Application.RconParsers
Console.WriteLine(line);
}
#endif
return (ClientsFromStatus(response), MapFromStatus(response));
return (ClientsFromStatus(response), MapFromStatus(response), GameTypeFromStatus(response));
}
private string MapFromStatus(string[] response)
@ -139,6 +142,26 @@ namespace IW4MAdmin.Application.RconParsers
return map;
}
private string GameTypeFromStatus(string[] response)
{
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)
@ -180,10 +203,11 @@ namespace IW4MAdmin.Application.RconParsers
long networkId;
string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString;
try
{
string networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkId = networkIdString.IsBotGuid() ?
name.GenerateGuidFromString() :
@ -211,13 +235,7 @@ namespace IW4MAdmin.Application.RconParsers
State = EFClient.ClientState.Connecting
};
//#if DEBUG
// if (client.NetworkId < 1000 && client.NetworkId > 0)
// {
// client.IPAddress = 2147483646;
// client.Ping = 0;
// }
//#endif
client.SetAdditionalProperty("BotGuid", networkIdString);
StatusPlayers.Add(client);
}
@ -231,5 +249,19 @@ namespace IW4MAdmin.Application.RconParsers
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,6 +1,6 @@
using IW4MAdmin.Application.Factories;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
using System.Collections.Generic;
using System.Globalization;
namespace IW4MAdmin.Application.RconParsers
@ -14,16 +14,20 @@ namespace IW4MAdmin.Application.RconParsers
public CommandPrefix CommandPrefixes { get; set; }
public ParserRegex Status { get; set; }
public ParserRegex MapStatus { get; set; }
public ParserRegex GametypeStatus { get; set; }
public ParserRegex Dvar { get; set; }
public ParserRegex StatusHeader { get; set; }
public string ServerNotRunningResponse { get; set; }
public bool WaitForResponse { get; set; } = true;
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public 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

@ -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

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

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

@ -8,8 +8,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc
GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc
azure-pipelines.yml = azure-pipelines.yml
PostPublish.ps1 = PostPublish.ps1
pre-release-pipeline.yml = pre-release-pipeline.yml
README.md = README.md
RunPublishPre.cmd = RunPublishPre.cmd
RunPublishRelease.cmd = RunPublishRelease.cmd
@ -30,14 +30,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProfanityDeterment", "Plugi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Login", "Plugins\Login\Login.csproj", "{D9F2ED28-6FA5-40CA-9912-E7A849147AB1}"
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}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
ProjectSection(SolutionItems) = preProject
Plugins\ScriptPlugins\ActionOnReport.js = Plugins\ScriptPlugins\ActionOnReport.js
Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js
Plugins\ScriptPlugins\ParserIW4x.js = Plugins\ScriptPlugins\ParserIW4x.js
Plugins\ScriptPlugins\ParserPIW5.js = Plugins\ScriptPlugins\ParserPIW5.js
@ -50,8 +47,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
EndProjectSection
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "GameLogServer", "GameLogServer\GameLogServer.pyproj", "{42EFDA12-10D3-4C40-A210-9483520116BC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{A848FCF1-8527-4AA8-A1AA-50D29695C678}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatsWeb", "Plugins\Web\StatsWeb\StatsWeb.csproj", "{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}"
@ -248,52 +243,6 @@ Global
{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.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.Build.0 = Debug|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@ -318,28 +267,6 @@ Global
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.Build.0 = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@ -444,7 +371,6 @@ Global
{179140D3-97AA-4CB4-8BF6-A0C73CA75701} = {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}
{B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F}
{6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F}
{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {26E8B310-269E-46D4-A612-24601F16065F}
{A848FCF1-8527-4AA8-A1AA-50D29695C678} = {26E8B310-269E-46D4-A612-24601F16065F}

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,60 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<div class="row">
<div class="col-12 col-sm-8 ml-auto mr-auto">
<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>
{% 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>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.11" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.11" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -74,14 +74,14 @@ namespace LiveRadar.Web.Controllers
[Route("Radar/Update")]
public IActionResult Update(string payload)
{
var radarUpdate = RadarEvent.Parse(payload);
/*var radarUpdate = RadarEvent.Parse(payload);
var client = _manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
if (client != null)
{
radarUpdate.Name = client.Name.StripColors();
client.SetAdditionalProperty("LiveRadar", radarUpdate);
}
}*/
return Ok();
}

View File

@ -0,0 +1,33 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using EventGeneratorCallback = System.ValueTuple<string, string,
System.Func<string, SharedLibraryCore.Interfaces.IEventParserConfiguration,
SharedLibraryCore.GameEvent,
SharedLibraryCore.GameEvent>>;
namespace LiveRadar.Events
{
public class Script : IRegisterEvent
{
private const string EVENT_LIVERADAR = "LiveRadar";
private EventGeneratorCallback LiveRadar()
{
return (EVENT_LIVERADAR, EVENT_LIVERADAR, (string eventLine, IEventParserConfiguration config, GameEvent autoEvent) =>
{
string[] lineSplit = eventLine.Split(";");
autoEvent.Type = GameEvent.EventType.Other;
autoEvent.Subtype = EVENT_LIVERADAR;
autoEvent.Origin = new EFClient() { NetworkId = 0 };
autoEvent.Extra = lineSplit[1]; // guid
return autoEvent;
}
);
}
public IEnumerable<EventGeneratorCallback> Events => new[] { LiveRadar() };
}
}

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.11" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -17,12 +18,14 @@ namespace LiveRadar
public string Author => "RaidMax";
private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler;
private readonly Dictionary<string, long> _botGuidLookups;
private bool addedPage;
private readonly object lockObject = new object();
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory)
{
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration");
_botGuidLookups = new Dictionary<string, long>();
}
public Task OnEventAsync(GameEvent E, Server S)
@ -41,28 +44,45 @@ namespace LiveRadar
}
}
if (E.Type == GameEvent.EventType.Unknown)
if (E.Type == GameEvent.EventType.PreConnect && E.Origin.IsBot)
{
if (E.Data?.StartsWith("LiveRadar") ?? false)
string botKey = $"BotGuid_{E.Extra}";
lock (lockObject)
{
try
if (!_botGuidLookups.ContainsKey(botKey))
{
var radarUpdate = RadarEvent.Parse(E.Data);
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
_botGuidLookups.Add(botKey, E.Origin.NetworkId);
}
}
}
if (client != null)
{
radarUpdate.Name = client.Name.StripColors();
client.SetAdditionalProperty("LiveRadar", radarUpdate);
}
if (E.Type == GameEvent.EventType.Other && E.Subtype == "LiveRadar")
{
try
{
string botKey = $"BotGuid_{E.Extra}";
long generatedBotGuid;
lock (lockObject)
{
generatedBotGuid = _botGuidLookups.ContainsKey(botKey) ? _botGuidLookups[botKey] : 0;
}
catch (Exception e)
var radarUpdate = RadarEvent.Parse(E.Data, generatedBotGuid);
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
if (client != null)
{
S.Logger.WriteWarning($"Could not parse live radar output: {e.Data}");
S.Logger.WriteDebug(e.GetExceptionInfo());
radarUpdate.Name = client.Name.StripColors();
client.SetAdditionalProperty("LiveRadar", radarUpdate);
}
}
catch (Exception e)
{
S.Logger.WriteWarning($"Could not parse live radar output: {e.Data}");
S.Logger.WriteDebug(e.GetExceptionInfo());
}
}
return Task.CompletedTask;

View File

@ -1,9 +1,7 @@
using SharedLibraryCore;
using SharedLibraryCore.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LiveRadar
{
@ -39,13 +37,13 @@ namespace LiveRadar
return false;
}
public static RadarEvent Parse(string input)
public static RadarEvent Parse(string input, long generatedBotGuid)
{
var items = input.Split(';').Skip(1).ToList();
var parsedEvent = new RadarEvent()
{
Guid = items[0].ConvertGuidToLong(System.Globalization.NumberStyles.HexNumber),
Guid = generatedBotGuid,
Location = Vector3.Parse(items[1]),
ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(),
Team = items[3],

View File

@ -81,7 +81,7 @@
weapons["wa2000"] = "wa2000";
weapons["m21"] = "m14ebr";
weapons["cheytac"] = "cheytac";
weapons["dragunov"] = "hud_dragunovsvd";
weapons["dragunov"] = "dragunovsvd";
weapons["beretta"] = "m9beretta";
weapons["usp"] = "usp_45";

View File

@ -23,7 +23,7 @@
</Target>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.11" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.11" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -0,0 +1,51 @@
let plugin = {
author: 'RaidMax',
version: 1.0,
name: 'Action on Report',
enabled: false, // indicates if the plugin is enabled
reportAction: 'TempBan', // can be TempBan or Ban
maxReportCount: 5, // how many reports before action is taken
tempBanDurationMinutes: 60, // how long to temporarily ban the player
eventTypes: { 'report': 103 },
permissionTypes: { 'trusted': 2 },
onEventAsync: function (gameEvent, server) {
if (!this.enabled) {
return;
}
if (gameEvent.Type === this.eventTypes['report']) {
if (!gameEvent.Target.IsIngame || gameEvent.Target.Level >= this.permissionTypes['trusted']) {
server.Logger.WriteInfo(`Ignoring report for client (id) ${gameEvent.Target.ClientId} because they are privileged or not ingame`);
return;
}
let reportCount = this.reportCounts[gameEvent.Target.NetworkId] === undefined ? 0 : this.reportCounts[gameEvent.Target.NetworkId];
reportCount++;
this.reportCounts[gameEvent.Target.NetworkId] = reportCount;
if (reportCount >= this.maxReportCount) {
switch (this.reportAction) {
case 'TempBan':
server.Logger.WriteInfo(`TempBanning client (id) ${gameEvent.Target.ClientId} because they received ${reportCount} reports`);
gameEvent.Target.TempBan(_localization.LocalizationIndex['PLUGINS_REPORT_ACTION'], System.TimeSpan.FromMinutes(this.tempBanDurationMinutes), _IW4MAdminClient);
break;
case 'Ban':
server.Logger.WriteInfo(`Banning client (id) ${gameEvent.Target.ClientId} because they received ${reportCount} reports`);
gameEvent.Target.Ban(_localization.LocalizationIndex['PLUGINS_REPORT_ACTION'], _IW4MAdminClient, false);
break;
}
}
}
},
onLoadAsync: function (manager) {
this.reportCounts = {};
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -15,7 +15,7 @@ var plugin = {
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +playerid +steamid +name +lastmsg +address +qport +rate *';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){16,32}|(?:[a-z]|[0-9]){32}|bot[0-9]+) ([0-9+]) *(.{0,32}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$'
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]{16,32})|bot[0-9]+) ([0-9]+) +(.{0,32}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$';
rconParser.Configuration.Status.AddMapping(104, 6); // RConName
rconParser.Configuration.Status.AddMapping(105, 8); // RConIPAddress

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.3,
version: 0.5,
name: 'Plutonium IW5 Parser',
isParser: true,
@ -28,7 +28,7 @@ var plugin = {
rconParser.Configuration.CanGenerateLogPath = true;
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+|0) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(?:[0-1]{1}) +([0-9]{1,4}|[A-Z]{4}) +([a-f|A-F|0-9]{16}) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$';
rconParser.Configuration.Status.AddMapping(100, 1);
rconParser.Configuration.Status.AddMapping(101, 2);
rconParser.Configuration.Status.AddMapping(102, 3);
@ -42,6 +42,7 @@ var plugin = {
eventParser.GameName = 3; // IW5
eventParser.Configuration.GameDirectory = '';
eventParser.URLProtocolFormat = 'plutonium://play/iw5mp/{{ip}}:{{port}}';
},
onUnloadAsync: function () {

View File

@ -20,6 +20,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick_for_reason {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick_for_reason {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConGetDvar = '\xff\xff\xff\xffrcon {0} get {1}';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; // adding this in here temporarily until getInfo is fixed in new T6 version
rconParser.Configuration.Dvar.Pattern = '^(.+) is "(.+)?"$';
rconParser.Configuration.Dvar.AddMapping(106, 1);

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.1,
version: 0.3,
name: 'Black Ops 3 Parser',
isParser: true,
@ -14,7 +14,7 @@ var plugin = {
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown)(?:\\([0-9]+\\)) +(-*[0-9]+) *$'
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown)(?:\\([0-9]+\\)) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +xuid +name +address +qport';
rconParser.Configuration.CommandPrefixes.Kick = 'clientkick {0}';
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0}';
@ -23,9 +23,20 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.RConGetDvar = '\xff\xff\xff\xff\x00{0} {1}';
rconParser.Configuration.CommandPrefixes.RConSetDvar = '\xff\xff\xff\xff\x00{0} set {1}';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff\x01';
rconParser.Configuration.GametypeStatus.Pattern = 'Gametype: (.+)';
rconParser.Configuration.MapStatus.Pattern = 'Map: (.+)';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; // disables this, because it's useless on T7
rconParser.Configuration.ServerNotRunningResponse = 'this is here to prevent a hiberating server from being detected as not running';
rconParser.Configuration.OverrideDvarNameMapping.Add('sv_hostname', 'live_steam_server_name');
rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1');
rconParser.Configuration.DefaultDvarValues.Add('g_gametype', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_basepath', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_basegame', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_game', '');
rconParser.Configuration.Status.AddMapping(105, 6); // ip address
rconParser.Configuration.GametypeStatus.AddMapping(112, 1); // gametype
rconParser.Version = '[local] ship win64 CODBUILD8-764 (3421987) Mon Dec 16 10:44:20 2019 10d27bef';
rconParser.GameName = 8; // BO3
rconParser.CanGenerateLogPath = false;
@ -34,7 +45,6 @@ var plugin = {
eventParser.GameName = 8; // BO3
eventParser.Configuration.GameDirectory = 'usermaps';
eventParser.Configuration.Say.Pattern = '^(chat|chatteam);(?:[0-9]+);([0-9]+);([0-9]+);(.+);(.*)$';
},
onUnloadAsync: function () {
@ -42,4 +52,4 @@ var plugin = {
onTickAsync: function (server) {
}
};
};

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.5,
version: 0.7,
name: 'Tekno MW3 Parser',
isParser: true,
@ -27,6 +27,10 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.TempBan = 'tempbanclient {0} "{1}"';
rconParser.Configuration.Dvar.AddMapping(107, 1); // RCon DvarValue
rconParser.Configuration.Dvar.Pattern = '^(.*)$';
rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1');
rconParser.Configuration.OverrideDvarNameMapping.Add('_website', 'sv_clanWebsite');
rconParser.Version = 'IW5 MP 1.4 build 382 latest Thu Jan 19 2012 11:09:49AM win-x86';
rconParser.GameName = 3; // IW5
rconParser.CanGenerateLogPath = false;

View File

@ -7,6 +7,8 @@ let commands = [{
alias: "pp",
// required
permission: "User",
// optional (defaults to false)
targetRequired: false,
// optional
arguments: [{
name: "times to ping",
@ -44,6 +46,8 @@ let plugin = {
},
onLoadAsync: function (manager) {
this.logger = _serviceResolver.ResolveService("ILogger");
this.logger.WriteDebug("sample plugin loaded");
},
onUnloadAsync: function () {

View File

@ -20,7 +20,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Offset,
Strain,
Recoil,
Snap
Snap,
Button
};
public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
@ -38,11 +39,12 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
ILogger Log;
Strain Strain;
readonly DateTime ConnectionTime = DateTime.UtcNow;
private double sessionAverageRecoilAmount;
private double mapAverageRecoilAmount;
private double sessionAverageSnapAmount;
private int sessionSnapHits;
private EFClientKill lastHit;
private int validRecoilHitCount;
private int validButtonHitCount;
private class HitInfo
{
@ -282,18 +284,30 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
#region RECOIL
float hitRecoilAverage = 0;
if (!Plugin.Config.Configuration().RecoilessWeapons.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex)))
bool shouldIgnoreDetection = false;
try
{
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Recoil]
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
}
catch (KeyNotFoundException)
{
}
if (!shouldIgnoreDetection)
{
validRecoilHitCount++;
hitRecoilAverage = (hit.AnglesList.Sum(_angle => _angle.Z) + hit.ViewAngles.Z) / (hit.AnglesList.Count + 1);
sessionAverageRecoilAmount = (sessionAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount;
mapAverageRecoilAmount = (mapAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount;
if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && sessionAverageRecoilAmount == 0)
if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && mapAverageRecoilAmount == 0)
{
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = sessionAverageRecoilAmount,
Value = mapAverageRecoilAmount,
HitCount = HitCount,
Type = DetectionType.Recoil
});
@ -301,6 +315,37 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
}
#endregion
#region BUTTON
try
{
shouldIgnoreDetection = false;
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Button]
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
}
catch (KeyNotFoundException)
{
}
if (!shouldIgnoreDetection)
{
validButtonHitCount++;
}
double lastDiff = hit.TimeOffset - hit.TimeSinceLastAttack;
if (validButtonHitCount > 0 && lastDiff <= 0)
{
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = lastDiff,
HitCount = HitCount,
Type = DetectionType.Button
});
}
#endregion
#region SESSION_RATIOS
if (Kills >= Thresholds.LowSampleMinKills)
{
@ -384,7 +429,19 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
#region CHEST_ABDOMEN_RATIO_SESSION
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count;
if (chestHits >= Thresholds.MediumSampleMinKills)
try
{
shouldIgnoreDetection = false; // reset previous value
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Chest]
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
}
catch (KeyNotFoundException)
{
}
if (chestHits >= Thresholds.MediumSampleMinKills && !shouldIgnoreDetection)
{
double marginOfError = Thresholds.GetMarginOfError(chestHits);
double lerpAmount = Math.Min(1.0, (chestHits - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills));
@ -466,5 +523,11 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
return results;
}
public void OnMapChange()
{
mapAverageRecoilAmount = 0;
validRecoilHitCount = 0;
}
}
}

View File

@ -17,6 +17,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
class MostKillsCommand : Command
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly CommandConfiguration _config;
public MostKillsCommand(CommandConfiguration config, ITranslationLookup translationLookup, IDatabaseContextFactory contextFactory) : base(config, translationLookup)
{
@ -26,12 +27,13 @@ namespace IW4MAdmin.Plugins.Stats.Commands
Permission = EFClient.Permission.User;
_contextFactory = contextFactory;
_config = config;
}
public override async Task ExecuteAsync(GameEvent E)
{
var mostKills = await GetMostKills(StatManager.GetIdForServer(E.Owner), Plugin.Config.Configuration(), _contextFactory, _translationLookup);
if (!E.Message.IsBroadcastCommand())
if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{
foreach (var stat in mostKills)
{

View File

@ -22,7 +22,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
List<string> mostPlayed = new List<string>()
{
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--"
$"^5--{translationLookup["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--"
};
using (var db = new DatabaseContext(true))
@ -51,14 +51,15 @@ namespace IW4MAdmin.Plugins.Stats.Commands
var iqList = await iqStats.ToListAsync();
mostPlayed.AddRange(iqList.Select(stats =>
$"^3{stats.Name}^7 - ^5{stats.Kills} ^7{translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{Utilities.GetTimePassed(DateTime.UtcNow.AddSeconds(-stats.TotalConnectionTime), false)} ^7{translationLookup["WEBFRONT_PROFILE_PLAYER"].ToLower()}"));
mostPlayed.AddRange(iqList.Select(stats => translationLookup["COMMANDS_MOST_PLAYED_FORMAT"].FormatExt(stats.Name, stats.Kills, (DateTime.UtcNow - DateTime.UtcNow.AddSeconds(-stats.TotalConnectionTime)).HumanizeForCurrentCulture())));
}
return mostPlayed;
}
private readonly CommandConfiguration _config;
public MostPlayedCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{
Name = "mostplayed";
@ -66,12 +67,14 @@ namespace IW4MAdmin.Plugins.Stats.Commands
Alias = "mp";
Permission = EFClient.Permission.User;
RequiresTarget = false;
_config = config;
}
public override async Task ExecuteAsync(GameEvent E)
{
var topStats = await GetMostPlayed(E.Owner, _translationLookup);
if (!E.Message.IsBroadcastCommand())
if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{
foreach (var stat in topStats)
{

View File

@ -65,6 +65,8 @@ namespace IW4MAdmin.Plugins.Stats.Commands
return topStatsText;
}
private readonly CommandConfiguration _config;
public TopStats(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{
Name = "topstats";
@ -72,12 +74,14 @@ namespace IW4MAdmin.Plugins.Stats.Commands
Alias = "ts";
Permission = EFClient.Permission.User;
RequiresTarget = false;
_config = config;
}
public override async Task ExecuteAsync(GameEvent E)
{
var topStats = await GetTopStats(E.Owner, _translationLookup);
if (!E.Message.IsBroadcastCommand())
if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{
foreach (var stat in topStats)
{

View File

@ -30,8 +30,12 @@ namespace IW4MAdmin.Plugins.Stats.Commands
Required = false
}
};
_config = config;
}
private readonly CommandConfiguration _config;
public override async Task ExecuteAsync(GameEvent E)
{
string statLine;
@ -90,7 +94,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
statLine = $"^5{pStats.Kills} ^7{_translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{_translationLookup["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{_translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}";
}
if (E.Message.IsBroadcastCommand())
if (E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
{
string name = E.Target == null ? E.Origin.Name : E.Target.Name;
E.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name));

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
using static SharedLibraryCore.Server;
namespace Stats.Config
{
public class AnticheatConfiguration
{
public bool Enable { get; set; }
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; } = new Dictionary<long, DetectionType[]>();
public IList<long> IgnoredClientIds { get; set; } = new List<long>();
public IDictionary<Game, IDictionary<DetectionType, string[]>> IgnoredDetectionSpecification{ get; set; } = new Dictionary<Game, IDictionary<DetectionType, string[]>>
{
{
Game.IW4, new Dictionary<DetectionType, string[]>
{
{ DetectionType.Chest, new[] { "m21.+" } },
{ DetectionType.Recoil, new[] { "ranger.*_mp", "model1887.*_mp", ".+shotgun.*_mp" } },
{ DetectionType.Button, new[] { ".*akimbo.*" } }
}
}
};
}
}

View File

@ -1,6 +1,7 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using Stats.Config;
using System;
using System.Collections.Generic;
using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
@ -8,21 +9,41 @@ namespace IW4MAdmin.Plugins.Stats.Config
{
public class StatsConfiguration : IBaseConfiguration
{
public bool EnableAntiCheat { get; set; }
[Obsolete]
public bool? EnableAntiCheat { get; set; }
public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
public List<StreakMessageConfiguration> DeathstreakMessages { get; set; }
public List<string> RecoilessWeapons { get; set; }
public int TopPlayersMinPlayTime { get; set; }
public bool StoreClientKills { get; set; }
public int MostKillsMaxInactivityDays { get; set; } = 30;
public int MostKillsClientLimit { get; set; } = 5;
public IDictionary<DetectionType, DistributionConfiguration> DetectionDistributions { get; set; }
[Obsolete]
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; }
public AnticheatConfiguration AnticheatConfiguration { get; set; } = new AnticheatConfiguration();
#pragma warning disable CS0612 // Type or member is obsolete
public void ApplyMigration()
{
if (ServerDetectionTypes != null)
{
AnticheatConfiguration.ServerDetectionTypes = ServerDetectionTypes;
}
ServerDetectionTypes = null;
if (EnableAntiCheat != null)
{
AnticheatConfiguration.Enable = EnableAntiCheat.Value;
}
EnableAntiCheat = null;
}
#pragma warning restore CS0612 // Type or member is obsolete
public string Name() => "StatsPluginSettings";
public IBaseConfiguration Generate()
{
EnableAntiCheat = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]);
AnticheatConfiguration.Enable = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]);
KillstreakMessages = new List<StreakMessageConfiguration>()
{
new StreakMessageConfiguration(){
@ -57,16 +78,9 @@ namespace IW4MAdmin.Plugins.Stats.Config
},
};
RecoilessWeapons = new List<string>()
{
"ranger.*_mp",
"model1887.*_mp",
".+shotgun.*_mp"
};
TopPlayersMinPlayTime = 3600 * 3;
StoreClientKills = false;
return this;
}
}

View File

@ -0,0 +1,38 @@
using SharedLibraryCore.QueryHelper;
using System;
namespace Stats.Dtos
{
public class ChatSearchQuery : ClientPaginationRequest
{
/// <summary>
/// specifies the partial content of the message to search for
/// </summary>
public string MessageContains { get; set; }
/// <summary>
/// identifier for the server
/// </summary>
public string ServerId { get; set; }
/// <summary>
/// identifier for the client
/// </summary>
public new int? ClientId { get; set; }
/// <summary>
/// only look for messages sent after this date
/// </summary>
public DateTime SentAfter { get; set; } = DateTime.UtcNow.AddYears(-100);
/// <summary>
/// only look for messages sent before this date0
/// </summary>
public DateTime SentBefore { get; set; } = DateTime.UtcNow;
/// <summary>
/// indicates if the chat is on the meta page
/// </summary>
public bool IsProfileMeta { get; set; }
}
}

View File

@ -0,0 +1,10 @@
namespace Stats.Dtos
{
public class StatsInfoRequest
{
/// <summary>
/// client identifier
/// </summary>
public int? ClientId { get; set; }
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Text.Json.Serialization;
namespace Stats.Dtos
{
public class StatsInfoResult
{
/// <summary>
/// client name
/// </summary>
public string Name { get; set; }
/// <summary>
/// ranking on the server
/// </summary>
public int Ranking { get; set; }
/// <summary>
/// number of kills
/// </summary>
public int Kills { get; set; }
/// <summary>
/// number of deaths
/// </summary>
public int Deaths { get; set; }
/// <summary>
/// performance level (elo rating + skill) / 2
/// </summary>
public double Performance { get; set; }
/// <summary>
/// SPM
/// </summary>
public double ScorePerMinute { get; set; }
/// <summary>
/// last connection
/// </summary>
public DateTime LastPlayed { get; set; }
/// <summary>
/// how many seconds played on the server
/// </summary>
public double TotalSecondsPlayed { get; set; }
/// <summary>
/// name of the server
/// </summary>
public string ServerName { get; set; }
/// <summary>
/// server game
/// </summary>
public string ServerGame { get; set; }
[JsonIgnore]
public long ServerId { get; set; }
}
}

View File

@ -156,7 +156,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Deaths = s.Deaths,
Kills = s.Kills,
KDR = Math.Round(s.KDR, 2),
LastSeen = Utilities.GetTimePassed(clientRatingsDict[s.ClientId].LastConnection, false),
LastSeen = (DateTime.UtcNow - clientRatingsDict[s.ClientId].LastConnection).HumanizeForCurrentCulture(),
Name = clientRatingsDict[s.ClientId].Name,
Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2),
RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
@ -225,7 +225,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Port = sv.Port,
EndPoint = sv.ToString(),
ServerId = serverId,
GameName = sv.GameName
GameName = sv.GameName,
HostName = sv.Hostname
};
server = serverSet.Add(server).Entity;
@ -240,6 +241,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
ctx.SaveChanges();
}
if (server.HostName == null || server.HostName != sv.Hostname)
{
server.HostName = sv.Hostname;
ctx.Entry(server).Property(_prop => _prop.HostName).IsModified = true;
ctx.SaveChanges();
}
ctx.Entry(server).Property(_prop => _prop.IsPasswordProtected).IsModified = true;
server.IsPasswordProtected = !string.IsNullOrEmpty(sv.GamePassword);
ctx.SaveChanges();
}
// check to see if the stats have ever been initialized
@ -469,7 +481,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
IsKill = !isDamage,
AnglesList = snapshotAngles,
IsAlive = isAlive == "1",
TimeSinceLastAttack = long.Parse(lastAttackTime)
TimeSinceLastAttack = long.Parse(lastAttackTime),
GameName = attacker.CurrentServer.GameName
};
if (hit.HitLoc == IW4Info.HitLocation.shield)
@ -527,7 +540,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
}
if (Plugin.Config.Configuration().EnableAntiCheat && !attacker.IsBot && attacker.ClientId != victim.ClientId)
if (Plugin.Config.Configuration().AnticheatConfiguration.Enable && !attacker.IsBot && attacker.ClientId != victim.ClientId)
{
clientDetection.TrackedHits.Add(hit);
@ -543,10 +556,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (oldestHit.IsAlive)
{
var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker.CurrentServer.EndPoint);
#if !DEBUG
await ApplyPenalty(result, attacker);
#endif
var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker);
if (!Utilities.IsDevelopment)
{
await ApplyPenalty(result, attacker);
}
if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any)
{
@ -582,10 +597,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
}
private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> results, long serverId)
private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> results, EFClient client)
{
// allow disabling of certain detection types
results = results.Where(_result => ShouldUseDetection(serverId, _result.Type));
results = results.Where(_result => ShouldUseDetection(client.CurrentServer, _result.Type, client.ClientId));
return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ??
results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
new DetectionPenaltyResult()
@ -605,21 +620,31 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
}
private bool ShouldUseDetection(long serverId, DetectionType detectionType)
private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
{
var detectionTypes = Plugin.Config.Configuration().ServerDetectionTypes;
var detectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes;
var ignoredClients = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredClientIds;
if (detectionTypes == null)
if (ignoredClients.Contains(clientId))
{
return true;
return false;
}
if (!detectionTypes.ContainsKey(serverId))
try
{
return true;
if (detectionTypes[server.EndPoint].Contains(detectionType))
{
return false;
}
}
return detectionTypes[serverId].Contains(detectionType);
catch (KeyNotFoundException)
{
}
return true;
}
async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker)
@ -655,6 +680,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}";
penaltyClient.AdministeredPenalties = new List<EFPenalty>()
{
new EFPenalty()
{
AutomatedOffense = flagReason
}
};
await attacker.Flag(flagReason, penaltyClient, new TimeSpan(168, 0, 0)).WaitAsync(Utilities.DefaultCommandTimeout, attacker.CurrentServer.Manager.CancellationToken);
break;
}
@ -1119,10 +1152,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public void ResetKillstreaks(Server sv)
{
foreach (var stat in sv.GetClientsAsList()
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY)))
foreach (var session in sv.GetClientsAsList()
.Select(_client => new
{
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
}))
{
stat?.StartNewSession();
session.stat?.StartNewSession();
session.detection?.OnMapChange();
}
}
@ -1212,6 +1250,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return 886229536;
}
// todo: this is not stable and will need to be migrated again...
long id = HashCode.Combine(server.IP, server.Port);
id = id < 0 ? Math.Abs(id) : id;
long? serverId;

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