Compare commits
39 Commits
2.3-Prerel
...
2.3-Prerel
Author | SHA1 | Date | |
---|---|---|---|
e56f574af4 | |||
513f495304 | |||
910faf427b | |||
9117440566 | |||
ad6b6a6465 | |||
eb8145a168 | |||
a560e05df8 | |||
ac06b41a0b | |||
cce6482541 | |||
bc7dc3a71a | |||
2be719d8f9 | |||
8a8dec8bbd | |||
2b3e21d4ba | |||
4590d94d7d | |||
5842073f91 | |||
c783a04a52 | |||
4735864113 | |||
d70d8fd0ae | |||
0dc4e12d61 | |||
778e339a61 | |||
1ef2ba5344 | |||
126f2fcc47 | |||
d5789dac81 | |||
25e2438e7f | |||
19107f9e85 | |||
0e44fa10f7 | |||
ebb54ebfd7 | |||
03a27d113e | |||
22f9e581ed | |||
b59504a882 | |||
ed2b01f229 | |||
f040dd5159 | |||
6c00cceb7a | |||
04a95aa58a | |||
6155493181 | |||
297e2c283f | |||
021c0244b4 | |||
214d15384d | |||
39fb3b9966 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
ko_fi: raidmax
|
@ -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>
|
||||
|
||||
|
@ -13,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;
|
||||
@ -54,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>>();
|
||||
@ -65,12 +66,15 @@ 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, IDatabaseContextFactory contextFactory)
|
||||
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
|
||||
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver)
|
||||
{
|
||||
MiddlewareActionHandler = actionHandler;
|
||||
_servers = new ConcurrentBag<Server>();
|
||||
@ -81,11 +85,11 @@ namespace IW4MAdmin.Application
|
||||
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();
|
||||
@ -96,6 +100,8 @@ namespace IW4MAdmin.Application
|
||||
_customParserEvents = customParserEvents;
|
||||
_eventHandler = eventHandler;
|
||||
_scriptCommandFactory = scriptCommandFactory;
|
||||
_metaRegistration = metaRegistration;
|
||||
_scriptPluginServiceResolver = scriptPluginServiceResolver;
|
||||
Plugins = plugins;
|
||||
}
|
||||
|
||||
@ -273,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)
|
||||
@ -432,6 +438,11 @@ namespace IW4MAdmin.Application
|
||||
commandsToAddToConfig.AddRange(unsavedCommands);
|
||||
}
|
||||
|
||||
// this is because I want to store the command prefix in IW4MAdminSettings, but can't easily
|
||||
// inject it to all the places that need it
|
||||
cmdConfig.CommandPrefix = config.CommandPrefix;
|
||||
cmdConfig.BroadcastCommandPrefix = config.BroadcastCommandPrefix;
|
||||
|
||||
foreach (var cmd in commandsToAddToConfig)
|
||||
{
|
||||
cmdConfig.Commands.Add(cmd.CommandConfigNameForType(),
|
||||
@ -440,7 +451,8 @@ namespace IW4MAdmin.Application
|
||||
Name = cmd.Name,
|
||||
Alias = cmd.Alias,
|
||||
MinimumPermission = cmd.Permission,
|
||||
AllowImpersonation = cmd.AllowImpersonation
|
||||
AllowImpersonation = cmd.AllowImpersonation,
|
||||
SupportedGames = cmd.SupportedGames
|
||||
});
|
||||
}
|
||||
|
||||
@ -448,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))
|
||||
@ -729,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
|
||||
};
|
||||
|
12
Application/BuildScripts/DownloadTranslations.ps1
Normal file
12
Application/BuildScripts/DownloadTranslations.ps1
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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%
|
@ -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)
|
||||
{
|
||||
@ -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,
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -975,6 +987,7 @@ namespace IW4MAdmin
|
||||
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)
|
||||
{
|
||||
@ -1008,6 +1021,7 @@ namespace IW4MAdmin
|
||||
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)
|
||||
@ -1074,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)
|
||||
@ -1224,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);
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,23 @@
|
||||
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;
|
||||
@ -128,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)
|
||||
{
|
||||
@ -222,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>()
|
||||
@ -235,6 +246,14 @@ 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 =>
|
||||
{
|
||||
|
64
Application/Meta/AdministeredPenaltyResourceQueryHelper.cs
Normal file
64
Application/Meta/AdministeredPenaltyResourceQueryHelper.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
165
Application/Meta/MetaRegistration.cs
Normal file
165
Application/Meta/MetaRegistration.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
72
Application/Meta/ReceivedPenaltyResourceQueryHelper.cs
Normal file
72
Application/Meta/ReceivedPenaltyResourceQueryHelper.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
60
Application/Meta/UpdatedAliasResourceQueryHelper.cs
Normal file
60
Application/Meta/UpdatedAliasResourceQueryHelper.cs
Normal 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
187
Application/Misc/MetaService.cs
Normal file
187
Application/Misc/MetaService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
48
Application/Misc/ScriptPluginServiceResolver.cs
Normal file
48
Application/Misc/ScriptPluginServiceResolver.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -52,6 +52,7 @@ 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);
|
||||
|
||||
@ -114,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
|
||||
@ -123,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)
|
||||
@ -141,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)
|
||||
@ -182,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() :
|
||||
@ -213,6 +235,8 @@ namespace IW4MAdmin.Application.RconParsers
|
||||
State = EFClient.ClientState.Connecting
|
||||
};
|
||||
|
||||
client.SetAdditionalProperty("BotGuid", networkIdString);
|
||||
|
||||
StatusPlayers.Add(client);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ 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; }
|
||||
@ -26,6 +27,7 @@ namespace IW4MAdmin.Application.RconParsers
|
||||
{
|
||||
Status = parserRegexFactory.CreateParserRegex();
|
||||
MapStatus = parserRegexFactory.CreateParserRegex();
|
||||
GametypeStatus = parserRegexFactory.CreateParserRegex();
|
||||
Dvar = parserRegexFactory.CreateParserRegex();
|
||||
StatusHeader = parserRegexFactory.CreateParserRegex();
|
||||
}
|
||||
|
@ -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()
|
@ -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>
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"IW4MAdminUrl": "",
|
||||
"DiscordWebhookNotificationUrl": "",
|
||||
"DiscordWebhookInformationUrl": "",
|
||||
"NotifyRoleIds": []
|
||||
}
|
@ -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
|
BIN
GameFiles/PT6/t6r/data/maps/mp/gametypes/_clientids.gsc
Normal file
BIN
GameFiles/PT6/t6r/data/maps/mp/gametypes/_clientids.gsc
Normal file
Binary file not shown.
283
GameFiles/PT6/t6r/data/maps/mp/gametypes/_clientids.gsc.src
Normal file
283
GameFiles/PT6/t6r/data/maps/mp/gametypes/_clientids.gsc.src
Normal 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]]();
|
||||
}
|
@ -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|game_log_server_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="game_log_server_env\">
|
||||
<Id>game_log_server_env</Id>
|
||||
<Version>3.8</Version>
|
||||
<Description>game_log_server_env (Python 3.8 (64-bit))</Description>
|
||||
<InterpreterPath>Scripts\python.exe</InterpreterPath>
|
||||
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
|
||||
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
|
||||
<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>
|
@ -1,9 +0,0 @@
|
||||
"""
|
||||
The flask application package.
|
||||
"""
|
||||
|
||||
from flask import Flask
|
||||
from flask_restful import Api
|
||||
|
||||
app = Flask(__name__)
|
||||
api = Api(app)
|
@ -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()
|
@ -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']
|
||||
}
|
@ -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
|
@ -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')
|
@ -1,11 +0,0 @@
|
||||
aniso8601==8.0.0
|
||||
click==7.1.2
|
||||
Flask==1.1.2
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.11.2
|
||||
MarkupSafe==1.1.1
|
||||
pip==20.1
|
||||
pytz==2020.1
|
||||
setuptools==46.4.0
|
||||
six==1.14.0
|
||||
Werkzeug==1.0.1
|
@ -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)
|
@ -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,8 +30,6 @@ 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("{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}"
|
||||
@ -49,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}"
|
||||
@ -247,28 +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
|
||||
{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
|
||||
@ -293,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
|
||||
@ -419,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}
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -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();
|
||||
}
|
||||
|
33
Plugins/LiveRadar/Events/Script.cs
Normal file
33
Plugins/LiveRadar/Events/Script.cs
Normal 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() };
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -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;
|
||||
|
@ -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],
|
||||
|
@ -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";
|
||||
|
@ -23,7 +23,7 @@
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -7,6 +7,7 @@ let plugin = {
|
||||
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) {
|
||||
@ -14,7 +15,8 @@ let plugin = {
|
||||
}
|
||||
|
||||
if (gameEvent.Type === this.eventTypes['report']) {
|
||||
if (!gameEvent.Target.IsIngame) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -46,4 +48,4 @@ let plugin = {
|
||||
|
||||
onTickAsync: function (server) {
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -3,7 +3,7 @@ var eventParser;
|
||||
|
||||
var plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 0.2,
|
||||
version: 0.3,
|
||||
name: 'Black Ops 3 Parser',
|
||||
isParser: true,
|
||||
|
||||
@ -23,6 +23,8 @@ 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';
|
||||
|
||||
@ -34,6 +36,7 @@ var plugin = {
|
||||
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;
|
||||
@ -49,4 +52,4 @@ var plugin = {
|
||||
|
||||
onTickAsync: function (server) {
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ var eventParser;
|
||||
|
||||
var plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 0.6,
|
||||
version: 0.7,
|
||||
name: 'Tekno MW3 Parser',
|
||||
isParser: true,
|
||||
|
||||
@ -29,6 +29,7 @@ var plugin = {
|
||||
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
|
||||
|
@ -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 () {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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));
|
||||
|
24
Plugins/Stats/Config/AnticheatConfiguration.cs
Normal file
24
Plugins/Stats/Config/AnticheatConfiguration.cs
Normal 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.*" } }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using System;
|
||||
|
||||
namespace StatsWeb.Dtos
|
||||
namespace Stats.Dtos
|
||||
{
|
||||
public class ChatSearchQuery : PaginationInfo
|
||||
public class ChatSearchQuery : ClientPaginationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// specifies the partial content of the message to search for
|
||||
@ -18,7 +18,7 @@ namespace StatsWeb.Dtos
|
||||
/// <summary>
|
||||
/// identifier for the client
|
||||
/// </summary>
|
||||
public int? ClientId { get; set; }
|
||||
public new int? ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// only look for messages sent after this date
|
||||
@ -29,5 +29,10 @@ namespace StatsWeb.Dtos
|
||||
/// 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; }
|
||||
}
|
||||
}
|
@ -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,
|
||||
@ -248,6 +248,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
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
|
||||
@ -477,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)
|
||||
@ -535,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);
|
||||
|
||||
@ -551,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)
|
||||
{
|
||||
@ -590,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()
|
||||
@ -613,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)
|
||||
@ -663,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;
|
||||
}
|
||||
@ -1127,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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1220,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;
|
||||
|
@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Collections.Generic;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Models
|
||||
{
|
||||
@ -44,6 +45,8 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
||||
public float AdsPercent { get; set; }
|
||||
[NotMapped]
|
||||
public List<Vector3> AnglesList { get; set; }
|
||||
[NotMapped]
|
||||
public Game GameName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the attacker was alive after last captured angle
|
||||
|
@ -16,5 +16,6 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
||||
public string EndPoint { get; set; }
|
||||
public Game? GameName { get; set; }
|
||||
public string HostName { get; set; }
|
||||
public bool IsPasswordProtected { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,11 @@ using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Services;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using Stats.Dtos;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -33,13 +34,17 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
#endif
|
||||
private readonly IDatabaseContextFactory _databaseContextFactory;
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
private readonly IMetaService _metaService;
|
||||
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatQueryHelper;
|
||||
|
||||
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory,
|
||||
ITranslationLookup translationLookup)
|
||||
ITranslationLookup translationLookup, IMetaService metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper)
|
||||
{
|
||||
Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
|
||||
_databaseContextFactory = databaseContextFactory;
|
||||
_translationLookup = translationLookup;
|
||||
_metaService = metaService;
|
||||
_chatQueryHelper = chatQueryHelper;
|
||||
}
|
||||
|
||||
public async Task OnEventAsync(GameEvent E, Server S)
|
||||
@ -177,8 +182,9 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
if (Config.Configuration() == null)
|
||||
{
|
||||
Config.Set((StatsConfiguration)new StatsConfiguration().Generate());
|
||||
await Config.Save();
|
||||
}
|
||||
Config.Configuration().ApplyMigration();
|
||||
await Config.Save();
|
||||
|
||||
// register the topstats page
|
||||
// todo:generate the URL/Location instead of hardcoding
|
||||
@ -188,17 +194,14 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
"/Stats/TopPlayersAsync");
|
||||
|
||||
// meta data info
|
||||
async Task<List<ProfileMeta>> getStats(int clientId, int offset, int count, DateTime? startAt)
|
||||
async Task<IEnumerable<InformationResponse>> getStats(ClientPaginationRequest request)
|
||||
{
|
||||
if (count > 1)
|
||||
{
|
||||
return new List<ProfileMeta>();
|
||||
}
|
||||
|
||||
IList<EFClientStatistics> clientStats;
|
||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||
int messageCount = 0;
|
||||
using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false))
|
||||
{
|
||||
clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == clientId).ToListAsync();
|
||||
clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == request.ClientId).ToListAsync();
|
||||
messageCount = await ctx.Set<EFClientMessage>().CountAsync(_message => _message.ClientId == request.ClientId);
|
||||
}
|
||||
|
||||
int kills = clientStats.Sum(c => c.Kills);
|
||||
@ -209,73 +212,76 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
double performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
|
||||
double spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Where(c => c.SPM > 0).Count(), 1);
|
||||
|
||||
return new List<ProfileMeta>()
|
||||
return new List<InformationResponse>()
|
||||
{
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
|
||||
Value = "#" + (await Manager.GetClientOverallRanking(clientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Value = "#" + (await Manager.GetClientOverallRanking(request.ClientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 0,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"],
|
||||
Value = kills.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 1,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"],
|
||||
Value = deaths.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 2,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"],
|
||||
Value = kdr.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 3,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PERFORMANCE"],
|
||||
Value = performance.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 4,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_META_SPM"],
|
||||
Value = spm.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 5,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGES"],
|
||||
Value = messageCount.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 1,
|
||||
Order = 4,
|
||||
Type = MetaType.Information
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async Task<List<ProfileMeta>> getAnticheatInfo(int clientId, int offset, int count, DateTime? startAt)
|
||||
async Task<IEnumerable<InformationResponse>> getAnticheatInfo(ClientPaginationRequest request)
|
||||
{
|
||||
if (count > 1)
|
||||
{
|
||||
return new List<ProfileMeta>();
|
||||
}
|
||||
|
||||
IList<EFClientStatistics> clientStats;
|
||||
|
||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||
using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false))
|
||||
{
|
||||
clientStats = await ctx.Set<EFClientStatistics>()
|
||||
.Include(c => c.HitLocations)
|
||||
.Where(c => c.ClientId == clientId)
|
||||
.Where(c => c.ClientId == request.ClientId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@ -310,159 +316,103 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
averageSnapValue = clientStats.Any(_stats => _stats.AverageSnapValue > 0) ? clientStats.Where(_stats => _stats.AverageSnapValue > 0).Average(_stat => _stat.AverageSnapValue) : 0;
|
||||
}
|
||||
|
||||
return new List<ProfileMeta>()
|
||||
return new List<InformationResponse>()
|
||||
{
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 1",
|
||||
Value = chestRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 0,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM1"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM1"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 2",
|
||||
Value = abdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 1,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM2"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM2"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 3",
|
||||
Value = chestAbdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 2,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM3"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM3"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 4",
|
||||
Value = headRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 3,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM4"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM4"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 5",
|
||||
// todo: make sure this is wrapped somewhere else
|
||||
Value = $"{Math.Round(((float)hitOffsetAverage), 4).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))}°",
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 4,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM5"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM5"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 6",
|
||||
Value = Math.Round(maxStrain, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 5,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM6"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM6"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 7",
|
||||
Value = Math.Round(averageSnapValue, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 6,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM7"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM7"],
|
||||
IsSensitive = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async Task<List<ProfileMeta>> getMessages(int clientId, int offset, int count, DateTime? startAt)
|
||||
async Task<IEnumerable<MessageResponse>> getMessages(ClientPaginationRequest request)
|
||||
{
|
||||
if (count <= 1)
|
||||
var query = new ChatSearchQuery()
|
||||
{
|
||||
using (var ctx = new DatabaseContext(true))
|
||||
{
|
||||
return new List<ProfileMeta>
|
||||
{
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGES"],
|
||||
Value = (await ctx.Set<EFClientMessage>()
|
||||
.CountAsync(_message => _message.ClientId == clientId))
|
||||
.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 1,
|
||||
Order= 4,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
ClientId = request.ClientId,
|
||||
Before = request.Before,
|
||||
SentBefore = request.Before ?? DateTime.UtcNow,
|
||||
Count = request.Count,
|
||||
IsProfileMeta = true
|
||||
};
|
||||
|
||||
List<ProfileMeta> messageMeta;
|
||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||
{
|
||||
var messages = ctx.Set<EFClientMessage>()
|
||||
.Where(m => m.ClientId == clientId)
|
||||
.Where(_message => _message.TimeSent < startAt)
|
||||
.OrderByDescending(_message => _message.TimeSent)
|
||||
.Skip(offset)
|
||||
.Take(count);
|
||||
|
||||
messageMeta = await messages.Select(m => new ProfileMeta()
|
||||
{
|
||||
Key = null,
|
||||
Value = new { m.Message, m.Server.GameName },
|
||||
When = m.TimeSent,
|
||||
Extra = m.ServerId.ToString(),
|
||||
Type = ProfileMeta.MetaType.ChatMessage
|
||||
}).ToListAsync();
|
||||
|
||||
foreach (var message in messageMeta)
|
||||
{
|
||||
if ((message.Value.Message as string).IsQuickMessage())
|
||||
{
|
||||
try
|
||||
{
|
||||
var quickMessages = ServerManager.GetApplicationSettings().Configuration()
|
||||
.QuickMessages
|
||||
.First(_qm => _qm.Game == message.Value.GameName);
|
||||
message.Value = quickMessages.Messages[(message.Value.Message as string).Substring(1)];
|
||||
message.Type = ProfileMeta.MetaType.QuickMessage;
|
||||
}
|
||||
catch
|
||||
{
|
||||
message.Value = message.Value.Message;
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
message.Value = message.Value.Message;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return messageMeta;
|
||||
return (await _chatQueryHelper.QueryResource(query)).Results;
|
||||
}
|
||||
|
||||
if (Config.Configuration().EnableAntiCheat)
|
||||
if (Config.Configuration().AnticheatConfiguration.Enable)
|
||||
{
|
||||
MetaService.AddRuntimeMeta(getAnticheatInfo);
|
||||
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, getAnticheatInfo);
|
||||
}
|
||||
|
||||
MetaService.AddRuntimeMeta(getStats);
|
||||
MetaService.AddRuntimeMeta(getMessages);
|
||||
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, getStats);
|
||||
_metaService.AddRuntimeMeta<ClientPaginationRequest, MessageResponse>(MetaType.ChatMessage, getMessages);
|
||||
|
||||
async Task<string> totalKills(Server server)
|
||||
{
|
||||
@ -547,6 +497,6 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
/// </summary>
|
||||
/// <param name="s"></param>
|
||||
/// <returns></returns>
|
||||
private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().EnableAntiCheat && s.GameName == Server.Game.IW5;
|
||||
private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().AnticheatConfiguration.Enable && s.GameName == Server.Game.IW5;
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -1,349 +0,0 @@
|
||||
using IW4MAdmin.Application;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Events;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Tests
|
||||
{
|
||||
[Collection("ManagerCollection")]
|
||||
public class ClientTests
|
||||
{
|
||||
private readonly ApplicationManager _manager;
|
||||
const int TestTimeout = 10000;
|
||||
|
||||
public ClientTests(ManagerFixture fixture)
|
||||
{
|
||||
_manager = fixture.Manager;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetAdditionalPropertyShouldSucceed()
|
||||
{
|
||||
var client = new EFClient();
|
||||
int newProp = 5;
|
||||
client.SetAdditionalProperty("NewProp", newProp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAdditionalPropertyShouldSucceed()
|
||||
{
|
||||
var client = new EFClient();
|
||||
int newProp = 5;
|
||||
client.SetAdditionalProperty("NewProp", newProp);
|
||||
|
||||
Assert.True(client.GetAdditionalProperty<int>("NewProp") == 5, "added property does not match retrieved property");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BanEvasionShouldLink()
|
||||
{
|
||||
var server = _manager.Servers[0];
|
||||
var waiter = new ManualResetEventSlim();
|
||||
|
||||
_manager.GetApplicationSettings().Configuration().RConPollRate = 5000;
|
||||
|
||||
|
||||
while (!server.IsInitialized)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
var e = new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.PreConnect,
|
||||
Owner = server,
|
||||
Origin = new EFClient()
|
||||
{
|
||||
NetworkId = 1337,
|
||||
ClientNumber = 0,
|
||||
CurrentAlias = new EFAlias()
|
||||
{
|
||||
Name = "Ban Me",
|
||||
IPAddress = 1337
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_manager.AddEvent(e);
|
||||
e.Complete();
|
||||
|
||||
e = new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.PreConnect,
|
||||
Owner = server,
|
||||
Origin = new EFClient()
|
||||
{
|
||||
NetworkId = 1338,
|
||||
ClientNumber = 1,
|
||||
CurrentAlias = new EFAlias()
|
||||
{
|
||||
Name = "Ban Me",
|
||||
IPAddress = null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_manager.AddEvent(e);
|
||||
e.Complete();
|
||||
|
||||
e = new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Update,
|
||||
Owner = server,
|
||||
Origin = new EFClient()
|
||||
{
|
||||
NetworkId = 1338,
|
||||
ClientNumber = 1,
|
||||
CurrentAlias = new EFAlias()
|
||||
{
|
||||
Name = "Ban Me",
|
||||
IPAddress = 1337
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_manager.AddEvent(e);
|
||||
e.Complete();
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarnClientShouldSucceed()
|
||||
{
|
||||
var onJoined = new ManualResetEventSlim();
|
||||
var server = _manager.Servers[0];
|
||||
|
||||
while (!server.IsInitialized)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
//_manager.OnServerEvent += (sender, eventArgs) =>
|
||||
//{
|
||||
// if (eventArgs.Event.Type == GameEvent.EventType.Connect)
|
||||
// {
|
||||
// onJoined.Set();
|
||||
// }
|
||||
//};
|
||||
|
||||
server.EmulateClientJoinLog();
|
||||
onJoined.Wait();
|
||||
|
||||
var client = server.Clients[0];
|
||||
|
||||
var warnEvent = client.Warn("test warn", Utilities.IW4MAdminClient(server));
|
||||
warnEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
|
||||
|
||||
Assert.False(warnEvent.Failed);
|
||||
|
||||
warnEvent = client.Warn("test warn", new EFClient() { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
|
||||
warnEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
|
||||
|
||||
Assert.True(warnEvent.FailReason == GameEvent.EventFailReason.Permission &&
|
||||
client.Warnings == 1, "warning was applied without proper permissions");
|
||||
|
||||
// warn clear
|
||||
var warnClearEvent = client.WarnClear(new EFClient { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
|
||||
warnClearEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
|
||||
|
||||
Assert.True(warnClearEvent.FailReason == GameEvent.EventFailReason.Permission &&
|
||||
client.Warnings == 1, "warning was removed without proper permissions");
|
||||
|
||||
warnClearEvent = client.WarnClear(Utilities.IW4MAdminClient(server));
|
||||
warnClearEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
|
||||
|
||||
Assert.True(!warnClearEvent.Failed && client.Warnings == 0, "warning was not cleared");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReportClientShouldSucceed()
|
||||
{
|
||||
while (!_manager.IsInitialized)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
|
||||
Assert.False(client == null, "no client found to report");
|
||||
|
||||
// fail
|
||||
var player = new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer };
|
||||
player.SetAdditionalProperty("_reportCount", 3);
|
||||
var reportEvent = client.Report("test report", player);
|
||||
reportEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
|
||||
|
||||
Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Throttle &
|
||||
client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 0, $"too many reports were applied [{reportEvent.FailReason.ToString()}]");
|
||||
|
||||
// succeed
|
||||
reportEvent = client.Report("test report", new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
|
||||
reportEvent.WaitAsync(new TimeSpan(0, 0, 10), new CancellationToken()).Wait();
|
||||
|
||||
Assert.True(!reportEvent.Failed &&
|
||||
client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1, $"report was not applied [{reportEvent.FailReason.ToString()}]");
|
||||
|
||||
// fail
|
||||
reportEvent = client.Report("test report", new EFClient() { ClientId = 1, NetworkId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
|
||||
|
||||
Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Permission &&
|
||||
client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1,
|
||||
$"report was applied without proper permission [{reportEvent.FailReason.ToString()},{ client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId)}]");
|
||||
|
||||
// fail
|
||||
reportEvent = client.Report("test report", client);
|
||||
|
||||
Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Invalid &&
|
||||
client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1, $"report was applied to self");
|
||||
|
||||
// fail
|
||||
reportEvent = client.Report("test report", new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
|
||||
|
||||
Assert.True(reportEvent.FailReason == GameEvent.EventFailReason.Exception &&
|
||||
client.CurrentServer.Reports.Count(r => r.Target.NetworkId == client.NetworkId) == 1, $"duplicate report was applied");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlagClientShouldSucceed()
|
||||
{
|
||||
while (!_manager.IsInitialized)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
|
||||
Assert.False(client == null, "no client found to flag");
|
||||
|
||||
var flagEvent = client.Flag("test flag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
|
||||
flagEvent.Complete();
|
||||
|
||||
// succeed
|
||||
Assert.True(!flagEvent.Failed &&
|
||||
client.Level == EFClient.Permission.Flagged, $"player is not flagged [{flagEvent.FailReason.ToString()}]");
|
||||
Assert.False(client.ReceivedPenalties.FirstOrDefault(p => p.Offense == "test flag") == null, "flag was not applied");
|
||||
|
||||
flagEvent = client.Flag("test flag", new EFClient { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
|
||||
flagEvent.Complete();
|
||||
|
||||
// fail
|
||||
Assert.True(client.ReceivedPenalties.Count == 1, "flag was applied without permisions");
|
||||
|
||||
flagEvent = client.Flag("test flag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
|
||||
flagEvent.Complete();
|
||||
|
||||
// fail
|
||||
Assert.True(client.ReceivedPenalties.Count == 1, "duplicate flag was applied");
|
||||
|
||||
var unflagEvent = client.Unflag("test unflag", new EFClient { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
|
||||
unflagEvent.Complete();
|
||||
|
||||
// fail
|
||||
Assert.False(client.Level == EFClient.Permission.User, "user was unflagged without permissions");
|
||||
|
||||
unflagEvent = client.Unflag("test unflag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
|
||||
unflagEvent.Complete();
|
||||
|
||||
// succeed
|
||||
Assert.True(client.Level == EFClient.Permission.User, "user was not unflagged");
|
||||
|
||||
unflagEvent = client.Unflag("test unflag", new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
|
||||
unflagEvent.Complete();
|
||||
|
||||
// succeed
|
||||
Assert.True(unflagEvent.FailReason == GameEvent.EventFailReason.Invalid, "user was not flagged");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
void KickClientShouldSucceed()
|
||||
{
|
||||
while (!_manager.IsInitialized)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
|
||||
Assert.False(client == null, "no client found to kick");
|
||||
|
||||
var kickEvent = client.Kick("test kick", new EFClient() { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
|
||||
kickEvent.Complete();
|
||||
|
||||
Assert.True(kickEvent.FailReason == GameEvent.EventFailReason.Permission, "client was kicked without permission");
|
||||
|
||||
kickEvent = client.Kick("test kick", new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
|
||||
kickEvent.Complete();
|
||||
|
||||
Assert.True(_manager.Servers.First().GetClientsAsList().FirstOrDefault(c => c.NetworkId == client.NetworkId) == null, "client was not kicked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
void TempBanClientShouldSucceed()
|
||||
{
|
||||
while (!_manager.IsInitialized)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
|
||||
Assert.False(client == null, "no client found to tempban");
|
||||
|
||||
/* var tbCommand = new TempBanCommand();
|
||||
tbCommand.ExecuteAsync(new GameEvent()
|
||||
{
|
||||
Origin = new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer },
|
||||
Target = client,
|
||||
Data = "5days test tempban",
|
||||
Type = GameEvent.EventType.Command,
|
||||
Owner = client.CurrentServer
|
||||
}).Wait();
|
||||
|
||||
Assert.True(_manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId).Result.Count(p => p.Type == EFPenalty.PenaltyType.TempBan) == 1,
|
||||
"tempban was not added");*/
|
||||
}
|
||||
|
||||
[Fact]
|
||||
void BanUnbanClientShouldSucceed()
|
||||
{
|
||||
while (!_manager.IsInitialized)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
|
||||
Assert.False(client == null, "no client found to ban");
|
||||
/*
|
||||
var banCommand = new BanCommand();
|
||||
banCommand.ExecuteAsync(new GameEvent()
|
||||
{
|
||||
Origin = new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer },
|
||||
Target = client,
|
||||
Data = "test ban",
|
||||
Type = GameEvent.EventType.Command,
|
||||
Owner = client.CurrentServer
|
||||
}).Wait();
|
||||
|
||||
Assert.True(_manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId).Result.Count(p => p.Type == EFPenalty.PenaltyType.Ban) == 1,
|
||||
"ban was not added");
|
||||
|
||||
var unbanCommand = new UnbanCommand();
|
||||
unbanCommand.ExecuteAsync(new GameEvent()
|
||||
{
|
||||
Origin = new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer },
|
||||
//Target = Manager.GetClientService().Find(c => c.NetworkId == client.NetworkId).Result.First(),
|
||||
Data = "test unban",
|
||||
Type = GameEvent.EventType.Command,
|
||||
Owner = client.CurrentServer
|
||||
}).Wait();
|
||||
|
||||
Assert.True(_manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId).Result.Count(p => p.Type == EFPenalty.PenaltyType.Ban) == 0,
|
||||
"ban was not removed");*/
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
using IW4MAdmin.Application;
|
||||
using IW4MAdmin.Application.Factories;
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Tests
|
||||
{
|
||||
public class ManagerFixture : IDisposable
|
||||
{
|
||||
public ApplicationManager Manager { get; private set; }
|
||||
|
||||
public ManagerFixture()
|
||||
{
|
||||
string logFile = @"X:\IW4MAdmin\Plugins\Tests\bin\Debug\netcoreapp2.2\test_mp.log";
|
||||
|
||||
File.WriteAllText(logFile, Environment.NewLine);
|
||||
|
||||
Manager = null;
|
||||
|
||||
var config = new ApplicationConfiguration
|
||||
{
|
||||
Servers = new[]
|
||||
{
|
||||
new ServerConfiguration()
|
||||
{
|
||||
IPAddress = "127.0.0.1",
|
||||
Password = "test",
|
||||
Port = 28960,
|
||||
RConParserVersion = "test",
|
||||
EventParserVersion = "IW4x (v0.6.0)",
|
||||
ManualLogPath = logFile
|
||||
}
|
||||
},
|
||||
RConPollRate = int.MaxValue
|
||||
};
|
||||
|
||||
Manager.ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("test");
|
||||
Manager.ConfigHandler.Set(config);
|
||||
|
||||
Manager.Init().Wait();
|
||||
|
||||
Task.Run(() => Manager.Start());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Manager.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
[CollectionDefinition("ManagerCollection")]
|
||||
public class ManagerCollection : ICollectionFixture<ManagerFixture>
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
using IW4MAdmin.Application;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
|
||||
namespace Tests
|
||||
{
|
||||
[Collection("ManagerCollection")]
|
||||
public class ManagerTests
|
||||
{
|
||||
readonly ApplicationManager Manager;
|
||||
|
||||
public ManagerTests(ManagerFixture fixture)
|
||||
{
|
||||
Manager = fixture.Manager;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AreCommandNamesUnique()
|
||||
{
|
||||
bool test = Manager.GetCommands().Count == Manager.GetCommands().Select(c => c.Name).Distinct().Count();
|
||||
Assert.True(test, "command names are not unique");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AreCommandAliasesUnique()
|
||||
{
|
||||
var mgr = Manager;
|
||||
bool test = mgr.GetCommands().Count == mgr.GetCommands().Select(c => c.Alias).Distinct().Count();
|
||||
|
||||
foreach (var duplicate in mgr.GetCommands().GroupBy(_cmd => _cmd.Alias).Where(_grp => _grp.Count() > 1).Select(_grp => new { Command = _grp.First().Name, Alias = _grp.Key }))
|
||||
{
|
||||
Debug.WriteLine($"{duplicate.Command}: {duplicate.Alias}");
|
||||
}
|
||||
|
||||
Assert.True(test, "command aliases are not unique");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrintCommands()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("|Name |Alias|Description |Requires Target|Syntax |Required Level|");
|
||||
sb.AppendLine("|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|");
|
||||
|
||||
foreach (var command in Manager.GetCommands().OrderByDescending(c => c.Permission).ThenBy(c => c.Name))
|
||||
{
|
||||
sb.AppendLine($"|{command.Name}|{command.Alias}|{command.Description}|{((Command)command).RequiresTarget}|{((Command)command).Syntax.Substring(8).EscapeMarkdown()}|{command.Permission}|");
|
||||
}
|
||||
|
||||
Assert.True(false, sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +0,0 @@
|
||||
using IW4MAdmin.Application;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace Tests
|
||||
{
|
||||
[Collection("ManagerCollection")]
|
||||
public class PluginTests
|
||||
{
|
||||
readonly ApplicationManager Manager;
|
||||
|
||||
public PluginTests(ManagerFixture fixture)
|
||||
{
|
||||
Manager = fixture.Manager;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClientSayObjectionalWordShouldWarn()
|
||||
{
|
||||
var e = new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Connect,
|
||||
Origin = new EFClient()
|
||||
{
|
||||
Name = $"Player1",
|
||||
NetworkId = 1,
|
||||
ClientNumber = 1
|
||||
},
|
||||
Owner = Manager.GetServers()[0]
|
||||
};
|
||||
|
||||
Manager.AddEvent(e);
|
||||
e.Complete();
|
||||
|
||||
var client = Manager.GetServers()[0].Clients[0];
|
||||
|
||||
e = new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Say,
|
||||
Origin = client,
|
||||
Data = "nigger",
|
||||
Owner = e.Owner
|
||||
};
|
||||
|
||||
Manager.AddEvent(e);
|
||||
e.Complete();
|
||||
|
||||
Assert.True(client.Warnings == 1, "client wasn't warned for objectional language");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
using IW4MAdmin.Application;
|
||||
using SharedLibraryCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
|
||||
namespace Tests
|
||||
{
|
||||
[Collection("ManagerCollection")]
|
||||
public class ServerTests
|
||||
{
|
||||
private readonly ApplicationManager _manager;
|
||||
|
||||
public ServerTests(ManagerFixture fixture)
|
||||
{
|
||||
_manager = fixture.Manager;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAndRemoveClientViaLog()
|
||||
{
|
||||
var resetEvent = new ManualResetEventSlim();
|
||||
var server = _manager.Servers[0];
|
||||
|
||||
var currentClientCount = server.ClientNum;
|
||||
int eventsProcessed = 0;
|
||||
|
||||
/*_manager.OnServerEvent += (sender, eventArgs) =>
|
||||
{
|
||||
if (eventArgs.Event.Type == GameEvent.EventType.Connect)
|
||||
{
|
||||
eventArgs.Event.Complete();
|
||||
Assert.False(eventArgs.Event.Failed, "connect event was not processed");
|
||||
Assert.True(server.ClientNum == currentClientCount + 1, "client count was not incremented");
|
||||
eventsProcessed++;
|
||||
resetEvent.Set();
|
||||
}
|
||||
|
||||
if (eventArgs.Event.Type == GameEvent.EventType.Disconnect)
|
||||
{
|
||||
eventArgs.Event.Complete();
|
||||
Assert.False(eventArgs.Event.Failed, "disconnect event was not processed");
|
||||
Assert.True(server.ClientNum == currentClientCount, "client count was not decremented");
|
||||
eventsProcessed++;
|
||||
resetEvent.Set();
|
||||
}
|
||||
};*/
|
||||
|
||||
server.EmulateClientJoinLog();
|
||||
|
||||
resetEvent.Wait(15000);
|
||||
resetEvent.Reset();
|
||||
|
||||
Assert.Equal(1, eventsProcessed);
|
||||
|
||||
server.EmulateClientQuitLog();
|
||||
|
||||
resetEvent.Wait(15000);
|
||||
|
||||
Assert.Equal(2, eventsProcessed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddAndRemoveClientViaRcon()
|
||||
{
|
||||
var resetEvent = new ManualResetEventSlim();
|
||||
var server = _manager.Servers[0];
|
||||
|
||||
var currentClientCount = server.ClientNum;
|
||||
int eventsProcessed = 0;
|
||||
|
||||
_manager.GetApplicationSettings().Configuration().RConPollRate = 5000;
|
||||
/*_manager.OnServerEvent += (sender, eventArgs) =>
|
||||
{
|
||||
if (eventArgs.Event.Type == GameEvent.EventType.Connect)
|
||||
{
|
||||
eventArgs.Event.Complete();
|
||||
Assert.False(eventArgs.Event.Failed, "connect event was not processed");
|
||||
Assert.True(server.ClientNum == currentClientCount + 1, "client count was not incremented");
|
||||
eventsProcessed++;
|
||||
resetEvent.Set();
|
||||
}
|
||||
|
||||
if (eventArgs.Event.Type == GameEvent.EventType.Disconnect)
|
||||
{
|
||||
eventArgs.Event.Complete();
|
||||
Assert.False(eventArgs.Event.Failed, "disconnect event was not processed");
|
||||
Assert.True(server.ClientNum == currentClientCount, "client count was not decremented");
|
||||
eventsProcessed++;
|
||||
resetEvent.Set();
|
||||
}
|
||||
};*/
|
||||
|
||||
(server.RconParser as TestRconParser).FakeClientCount = 1;
|
||||
|
||||
resetEvent.Wait(15000);
|
||||
resetEvent.Reset();
|
||||
|
||||
Assert.Equal(1, eventsProcessed);
|
||||
|
||||
(server.RconParser as TestRconParser).FakeClientCount = 0;
|
||||
|
||||
resetEvent.Wait(15000);
|
||||
|
||||
Assert.Equal(2, eventsProcessed);
|
||||
|
||||
_manager.GetApplicationSettings().Configuration().RConPollRate = int.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
using IW4MAdmin.Application;
|
||||
using SharedLibraryCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Tests
|
||||
{
|
||||
internal static class TestHelpers
|
||||
{
|
||||
internal static void EmulateClientJoinLog(this Server svr)
|
||||
{
|
||||
long guid = svr.ClientNum + 1;
|
||||
File.AppendAllText(svr.LogPath, $"0:00 J;{guid};{svr.ClientNum};test_client_{svr.ClientNum}\r\n");
|
||||
}
|
||||
|
||||
internal static void EmulateClientQuitLog(this Server svr)
|
||||
{
|
||||
long guid = Math.Max(1, svr.ClientNum);
|
||||
File.AppendAllText(svr.LogPath, $"0:00 Q;{guid};{svr.ClientNum};test_client_{svr.ClientNum}\r\n");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.RCon;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tests
|
||||
{
|
||||
class TestRconParser : IW4MAdmin.Application.RconParsers.BaseRConParser
|
||||
{
|
||||
public TestRconParser(IParserRegexFactory f) : base(f)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public int FakeClientCount { get; set; }
|
||||
public List<EFClient> FakeClients { get; set; } = new List<EFClient>();
|
||||
|
||||
public override string Version => "test";
|
||||
|
||||
public override async Task<(List<EFClient>, string)> GetStatusAsync(IRConConnection connection)
|
||||
{
|
||||
var clientList = new List<EFClient>();
|
||||
|
||||
for (int i = 0; i < FakeClientCount; i++)
|
||||
{
|
||||
clientList.Add(new EFClient()
|
||||
{
|
||||
ClientNumber = i,
|
||||
NetworkId = i + 1,
|
||||
CurrentAlias = new EFAlias()
|
||||
{
|
||||
Name = $"test_bot_{i}",
|
||||
IPAddress = i + 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return clientList.Count > 0 ? (clientList, "mp_rust") : (FakeClients, "mp_rust");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<ApplicationIcon />
|
||||
<StartupObject />
|
||||
<LangVersion>7.1</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DefineConstants>TRACE;DEBUG</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Application\Application.csproj" />
|
||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -1,9 +1,13 @@
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using StatsWeb.Dtos;
|
||||
using Stats.Dtos;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -12,31 +16,44 @@ namespace StatsWeb
|
||||
/// <summary>
|
||||
/// implementation of IResourceQueryHelper
|
||||
/// </summary>
|
||||
public class ChatResourceQueryHelper : IResourceQueryHelper<ChatSearchQuery, ChatSearchResult>
|
||||
public class ChatResourceQueryHelper : IResourceQueryHelper<ChatSearchQuery, MessageResponse>
|
||||
{
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
private List<EFServer> serverCache;
|
||||
|
||||
public ChatResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
|
||||
public ChatResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory, ApplicationConfiguration appConfig)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
_logger = logger;
|
||||
_appConfig = appConfig;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ResourceQueryHelperResult<ChatSearchResult>> QueryResource(ChatSearchQuery query)
|
||||
public async Task<ResourceQueryHelperResult<MessageResponse>> QueryResource(ChatSearchQuery query)
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
throw new ArgumentException("Query must be specified");
|
||||
}
|
||||
|
||||
var result = new ResourceQueryHelperResult<ChatSearchResult>();
|
||||
var result = new ResourceQueryHelperResult<MessageResponse>();
|
||||
using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
|
||||
if (serverCache == null)
|
||||
{
|
||||
serverCache = await context.Set<EFServer>().ToListAsync();
|
||||
}
|
||||
|
||||
if (int.TryParse(query.ServerId, out int serverId))
|
||||
{
|
||||
query.ServerId = serverCache.FirstOrDefault(_server => _server.ServerId == serverId)?.EndPoint ?? query.ServerId;
|
||||
}
|
||||
|
||||
var iqMessages = context.Set<EFClientMessage>()
|
||||
.Where(_message => _message.TimeSent >= query.SentAfter)
|
||||
.Where(_message => _message.TimeSent <= query.SentBefore);
|
||||
.Where(_message => _message.TimeSent < query.SentBefore);
|
||||
|
||||
if (query.ClientId != null)
|
||||
{
|
||||
@ -50,27 +67,29 @@ namespace StatsWeb
|
||||
|
||||
if (!string.IsNullOrEmpty(query.MessageContains))
|
||||
{
|
||||
iqMessages = iqMessages.Where(_message => EF.Functions.Like(_message.Message, $"%{query.MessageContains}%"));
|
||||
iqMessages = iqMessages.Where(_message => EF.Functions.Like(_message.Message.ToLower(), $"%{query.MessageContains.ToLower()}%"));
|
||||
}
|
||||
|
||||
var iqResponse = iqMessages
|
||||
.Select(_message => new ChatSearchResult
|
||||
.Select(_message => new MessageResponse
|
||||
{
|
||||
ClientId = _message.ClientId,
|
||||
ClientName = _message.Client.CurrentAlias.Name,
|
||||
Date = _message.TimeSent,
|
||||
ClientName = query.IsProfileMeta ? "" : _message.Client.CurrentAlias.Name,
|
||||
ServerId = _message.ServerId,
|
||||
When = _message.TimeSent,
|
||||
Message = _message.Message,
|
||||
ServerName = _message.Server.HostName
|
||||
ServerName = query.IsProfileMeta ? "" : _message.Server.HostName,
|
||||
GameName = _message.Server.GameName == null ? Server.Game.IW4 : _message.Server.GameName.Value
|
||||
});
|
||||
|
||||
if (query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending)
|
||||
{
|
||||
iqResponse = iqResponse.OrderByDescending(_message => _message.Date);
|
||||
iqResponse = iqResponse.OrderByDescending(_message => _message.When);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
iqResponse = iqResponse.OrderBy(_message => _message.Date);
|
||||
iqResponse = iqResponse.OrderBy(_message => _message.When);
|
||||
}
|
||||
|
||||
var resultList = await iqResponse
|
||||
@ -78,6 +97,27 @@ namespace StatsWeb
|
||||
.Take(query.Count)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var message in resultList)
|
||||
{
|
||||
message.IsHidden = serverCache.Any(server => server.ServerId == message.ServerId && server.IsPasswordProtected);
|
||||
|
||||
if (message.Message.IsQuickMessage())
|
||||
{
|
||||
try
|
||||
{
|
||||
var quickMessages = _appConfig
|
||||
.QuickMessages
|
||||
.First(_qm => _qm.Game == message.GameName);
|
||||
message.Message = quickMessages.Messages[message.Message.Substring(1)];
|
||||
message.IsQuickMessage = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
message.Message = message.Message.Substring(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.TotalResultCount = await iqResponse.CountAsync();
|
||||
result.Results = resultList;
|
||||
result.RetrievedResultCount = resultList.Count;
|
||||
|
@ -5,8 +5,9 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using StatsWeb.Dtos;
|
||||
using Stats.Dtos;
|
||||
using StatsWeb.Extensions;
|
||||
using System;
|
||||
using System.Linq;
|
||||
@ -18,10 +19,10 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IManager _manager;
|
||||
private readonly IResourceQueryHelper<ChatSearchQuery, ChatSearchResult> _chatResourceQueryHelper;
|
||||
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatResourceQueryHelper;
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
|
||||
public StatsController(ILogger logger, IManager manager, IResourceQueryHelper<ChatSearchQuery, ChatSearchResult> resourceQueryHelper,
|
||||
public StatsController(ILogger logger, IManager manager, IResourceQueryHelper<ChatSearchQuery, MessageResponse> resourceQueryHelper,
|
||||
ITranslationLookup translationLookup) : base(manager)
|
||||
{
|
||||
_logger = logger;
|
||||
@ -36,6 +37,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"];
|
||||
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"];
|
||||
ViewBag.Servers = _manager.GetServers().Select(_server => new ServerInfo() { Name = _server.Hostname, ID = _server.EndPoint });
|
||||
ViewBag.Localization = _translationLookup;
|
||||
|
||||
return View("Index");
|
||||
}
|
||||
@ -71,51 +73,24 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMessageAsync(int serverId, long when)
|
||||
public async Task<IActionResult> GetMessageAsync(string serverId, long when)
|
||||
{
|
||||
var whenTime = DateTime.FromFileTimeUtc(when);
|
||||
var whenUpper = whenTime.AddMinutes(5);
|
||||
var whenLower = whenTime.AddMinutes(-5);
|
||||
|
||||
using (var ctx = new SharedLibraryCore.Database.DatabaseContext(true))
|
||||
var messages = await _chatResourceQueryHelper.QueryResource(new ChatSearchQuery()
|
||||
{
|
||||
var iqMessages = from message in ctx.Set<Stats.Models.EFClientMessage>()
|
||||
where message.ServerId == serverId
|
||||
where message.TimeSent >= whenLower
|
||||
where message.TimeSent <= whenUpper
|
||||
select new ChatInfo()
|
||||
{
|
||||
ClientId = message.ClientId,
|
||||
Message = message.Message,
|
||||
Name = message.Client.CurrentAlias.Name,
|
||||
Time = message.TimeSent,
|
||||
ServerGame = message.Server.GameName ?? Server.Game.IW4
|
||||
};
|
||||
ServerId = serverId,
|
||||
SentBefore = whenUpper,
|
||||
SentAfter = whenLower
|
||||
});
|
||||
|
||||
var messages = await iqMessages.ToListAsync();
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (message.Message.IsQuickMessage())
|
||||
{
|
||||
try
|
||||
{
|
||||
var quickMessages = _manager.GetApplicationSettings().Configuration()
|
||||
.QuickMessages
|
||||
.First(_qm => _qm.Game == message.ServerGame);
|
||||
message.Message = quickMessages.Messages[message.Message.Substring(1)];
|
||||
message.IsQuickMessage = true;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
return View("_MessageContext", messages);
|
||||
}
|
||||
return View("_MessageContext", messages.Results);
|
||||
}
|
||||
|
||||
[HttpGet("Message/Find")]
|
||||
public async Task<IActionResult> FindMessage([FromQuery]string query)
|
||||
public async Task<IActionResult> FindMessage([FromQuery] string query)
|
||||
{
|
||||
ViewBag.Localization = _translationLookup;
|
||||
ViewBag.EnableColorCodes = _manager.GetApplicationSettings().Configuration().EnableColorCodes;
|
||||
@ -150,7 +125,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("Message/FindNext")]
|
||||
public async Task<IActionResult> FindNextMessages([FromQuery]string query, [FromQuery]int count, [FromQuery]int offset)
|
||||
public async Task<IActionResult> FindNextMessages([FromQuery] string query, [FromQuery] int count, [FromQuery] int offset)
|
||||
{
|
||||
ChatSearchQuery searchRequest;
|
||||
|
||||
|
@ -1,32 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace StatsWeb.Dtos
|
||||
{
|
||||
public class ChatSearchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// name of the client
|
||||
/// </summary>
|
||||
public string ClientName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// client id
|
||||
/// </summary>
|
||||
public int ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// hostname of the server
|
||||
/// </summary>
|
||||
public string ServerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// chat message
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// date the chat occured on
|
||||
/// </summary>
|
||||
public DateTime Date { get; set; }
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
using SharedLibraryCore.Dtos;
|
||||
using StatsWeb.Dtos;
|
||||
using Stats.Dtos;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
<RunPostBuildEvent>Always</RunPostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<ul class="nav nav-tabs border-top border-bottom nav-fill row" role="tablist" id="stats_top_players">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active top-players-link" href="#server_0" role="tab" data-toggle="tab" aria-selected="true" data-serverid="0">All Servers</a>
|
||||
<a class="nav-link active top-players-link" href="#server_0" role="tab" data-toggle="tab" aria-selected="true" data-serverid="0">@ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"]</a>
|
||||
</li>
|
||||
|
||||
@foreach (var server in ViewBag.Servers)
|
||||
|
@ -1,4 +1,5 @@
|
||||
@model SharedLibraryCore.Helpers.ResourceQueryHelperResult<StatsWeb.Dtos.ChatSearchResult>
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@model SharedLibraryCore.Helpers.ResourceQueryHelperResult<MessageResponse>
|
||||
|
||||
@if (ViewBag.Error != null)
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
@model IEnumerable<StatsWeb.Dtos.ChatSearchResult>
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@model IEnumerable<MessageResponse>
|
||||
|
||||
@foreach (var message in Model)
|
||||
{
|
||||
@ -10,13 +11,20 @@
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-light w-50 text-break">
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
@if (message.IsHidden && !ViewBag.Authorized)
|
||||
{
|
||||
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
</td>
|
||||
<td class="text-light">
|
||||
<color-code value="@(message.ServerName ?? "--")" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</td>
|
||||
<td class="text-right text-light">
|
||||
@message.Date
|
||||
@message.When
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -33,7 +41,14 @@
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
|
||||
<td class="text-light">
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
@if (message.IsHidden && !ViewBag.Authorized)
|
||||
{
|
||||
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -47,7 +62,7 @@
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary" style="border-bottom: 1px solid #222">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
|
||||
<td class="text-light mb-2 border-bottom">
|
||||
@message.Date
|
||||
@message.When
|
||||
</td>
|
||||
</tr>
|
||||
}
|
@ -1,20 +1,21 @@
|
||||
@model IEnumerable<SharedLibraryCore.Dtos.ChatInfo>
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@model IEnumerable<MessageResponse>
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<div class="client-message-context">
|
||||
<h5 class="bg-primary pt-2 pb-2 pl-3 mb-0 mt-2 text-white">@Model.First().Time.ToString()</h5>
|
||||
<h5 class="bg-primary pt-2 pb-2 pl-3 mb-0 mt-2 text-white">@Model.First().When.ToString()</h5>
|
||||
<div class="bg-dark p-3 mb-2 border-bottom">
|
||||
@foreach (var message in Model)
|
||||
{
|
||||
<span class="text-white">
|
||||
<color-code value="@message.Name" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
<color-code value="@message.ClientName" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
</span>
|
||||
<span>
|
||||
—
|
||||
<span class="@(message.IsQuickMessage ? "font-italic" : "")">
|
||||
<color-code value="@message.Message" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
<color-code value="@(message.IsHidden ? message.HiddenMessage : message.Message)" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
437
README.md
437
README.md
@ -1,26 +1,25 @@
|
||||
|
||||
# IW4MAdmin
|
||||
### Quick Start Guide
|
||||
### Version 2.4
|
||||
_______
|
||||
### About
|
||||
**IW4MAdmin** is an administration tool for [IW4x](https://iw4xcachep26muba.onion.link/), [Pluto T6](https://forum.plutonium.pw/category/6/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/14/plutonium-iw5), [CoD4x](https://cod4x.me/), [TeknoMW3](https://www.teknomw3.pw/), and most Call of Duty® dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze.
|
||||
# IW4MAdmin [](https://github.com/RaidMax/IW4M-Admin/blob/2.4-pr/LICENSE) [](https://github.com/RaidMax/IW4M-Admin/stargazers)
|
||||
[](https://ko-fi.com/J3J821KUJ)
|
||||
|
||||
## About
|
||||
**IW4MAdmin** is an administration tool for [IW4x](https://iw4x.org/), [Pluto T6](https://forum.plutonium.pw/category/6/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/14/plutonium-iw5), [CoD4x](https://cod4x.me/), [TeknoMW3](https://github.com/Musta1337/TeknoMW3), and most Call of Duty® dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze.
|
||||
### Download
|
||||
Latest binary builds are always available at:
|
||||
- [GitHub](https://github.com/RaidMax/IW4M-Admin/releases)
|
||||
- [RaidMax](https://raidmax.org/IW4MAdmin)
|
||||
|
||||
---
|
||||
### Setup
|
||||
|
||||
## Setup
|
||||
**IW4MAdmin** requires minimal effort to get up and running.
|
||||
#### Prerequisites
|
||||
### Prerequisites
|
||||
* [.NET Core 3.1.x Runtime](https://www.microsoft.com/net/download) *or newer*
|
||||
* [Direct Download (Windows)](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-aspnetcore-3.1.4-windows-hosting-bundle-installer)
|
||||
* [Package Installation (Linux)](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1910)
|
||||
#### Installation
|
||||
### Installation
|
||||
1. Install .NET Core Runtime
|
||||
2. Extract `IW4MAdmin-<version>.zip`
|
||||
#### Launching
|
||||
### Launching
|
||||
Windows
|
||||
1. Run `StartIW4MAdmin.cmd`
|
||||
2. Configure **IW4MAdmin**
|
||||
@ -41,421 +40,9 @@ Linux
|
||||
|
||||
_Your configuration and database will be saved_
|
||||
|
||||
---
|
||||
### Help
|
||||
## Help
|
||||
Feel free to join the **IW4MAdmin** [Discord](https://discord.gg/ZZFK5p3)
|
||||
If you come across an issue, bug, or feature request please post an [issue](https://github.com/RaidMax/IW4M-Admin/issues)
|
||||
___
|
||||
|
||||
### Configuration
|
||||
#### Initial Configuration
|
||||
When **IW4MAdmin** is launched for the _first time_, you will be prompted to setup your configuration.
|
||||
|
||||
`Enable webfront`
|
||||
* Enables you to monitor and control your server(s) through a web interface
|
||||
* Default — `http://0.0.0.0:1624`
|
||||
|
||||
`Enable multiple owners`
|
||||
* Enables more than one client to be promoted to level of `Owner`
|
||||
* Default — `false`
|
||||
|
||||
`Enable stepped privilege hierarchy`
|
||||
* Allows privileged clients to promote other clients to the level below their current level
|
||||
* Default — `false`
|
||||
|
||||
`Enable custom say name`
|
||||
* Shows a prefix to every message send by **IW4MAdmin** -- `[Admin] message`
|
||||
* _This feature requires you specify a custom say name_
|
||||
* _This feature only works on games that support the `sv_sayName` dvar_
|
||||
* Default — `false`
|
||||
|
||||
`Enable social link`
|
||||
* Shows a link to your community's social media/website on the webfront
|
||||
* Default — `false`
|
||||
|
||||
`Use Custom Encoding Parser`
|
||||
* Allows alternative encodings to be used for parsing game information and events
|
||||
* **Russian users should use this and then specify** `windows-1251` **as the encoding string**
|
||||
* Default — `false`
|
||||
|
||||
#### Server Configuration
|
||||
After initial configuration is finished, you will be prompted to configure your servers for **IW4MAdmin**.
|
||||
|
||||
`Enter server IP Address`
|
||||
* For almost all scenarios `127.0.0.1` is sufficient
|
||||
* Default — `n/a`
|
||||
|
||||
`Enter server port`
|
||||
* The port that your server is listening on (can be obtained via `net_port`)
|
||||
* Default — `n/a`
|
||||
|
||||
`Enter server RCon password`
|
||||
* The *\(R\)emote (Con)sole* password set in your server configuration (can be obtained via `rcon_password`)
|
||||
* Default — `n/a`
|
||||
|
||||
`Enter number of reserved slots`
|
||||
* The number of client slots reserved for privileged players (unavailable for regular users to occupy)
|
||||
* For example, if you enter **2** reserved slots on an **18** slot server, you will have **16** publicly available slots
|
||||
* Default — `0`
|
||||
|
||||
#### Advanced Configuration
|
||||
If you wish to further customize your experience of **IW4MAdmin**, the following configuration file(s) will allow you to changes core options using any text-editor.
|
||||
|
||||
#### `IW4MAdminSettings.json`-- this file is created after initial setup
|
||||
* This file uses the [JSON](https://en.wikipedia.org/wiki/JSON#JSON_sample) specification, so please validate your configuration before running **IW4MAdmin**
|
||||
|
||||
`WebfrontBindUrl`
|
||||
* Specifies the address and port the webfront will listen on.
|
||||
* The value can be an [IP Address](https://en.wikipedia.org/wiki/IP_address):port or [Domain Name](https://en.wikipedia.org/wiki/Domain_name):port
|
||||
* Example http://gameserver.com:8080
|
||||
* Default — `http://0.0.0.0:1624` (indicates that it will listen on all IP Addresses available to the default interface)
|
||||
|
||||
`CustomLocale`
|
||||
* Specifies a [locale name](https://msdn.microsoft.com/en-us/library/39cwe7zf.aspx) to use instead of system default
|
||||
* Locale must be from the `Equivalent Locale Name` column
|
||||
* Default — `windows-1252`
|
||||
|
||||
`ConnectionString`
|
||||
* Specifies the [connection string](https://www.connectionstrings.com/mysql/) to a MySQL server that is used instead of SQLite
|
||||
* Default — `null`
|
||||
|
||||
`DatabaseProvider`
|
||||
* Specifies the database provider **IW4MAdmin** should use
|
||||
* Possible values — `sqlite`, `mysql`, `postgresql`
|
||||
* Default — `sqlite`
|
||||
|
||||
`Ignore Bots`
|
||||
* Disables bots from being registered and tracked by **IW4MAdmin**
|
||||
|
||||
`RConPollRate`
|
||||
* Specifies (in milliseconds) how often to poll each server for updates
|
||||
* Default — `5000`
|
||||
|
||||
`Servers`
|
||||
* Specifies the list of servers **IW4MAdmin** will monitor
|
||||
* Default — `[]`
|
||||
* `IPAddress`
|
||||
* Specifies the IP Address of the particular server
|
||||
* Default — `n/a`
|
||||
* `Port`
|
||||
* Specifies the port of the particular server
|
||||
* Default — `n/a`
|
||||
* `Password`
|
||||
* Specifies the `rcon_password` of the particular server
|
||||
* Default — `n/a`
|
||||
* `ManualLogPath`
|
||||
* Specifies the log path to be used instead of the automatically generated one
|
||||
* To use the `GameLogServer`, this should be set to the http address that the `GameLogServer` is listening on
|
||||
* Example — http://gamelogserver.com/
|
||||
* Default — `null`
|
||||
* `AutoMessages`
|
||||
* Specifies the list of messages that are broadcasted to the particular server
|
||||
* Default — `[]`
|
||||
* `Rules`
|
||||
* Specifies the list of rules that apply to the particular server
|
||||
* Default — `[]`
|
||||
* `ReservedSlotNumber`
|
||||
* Specifies the number of client slots to reserve for privileged users
|
||||
* Default — `0`
|
||||
* `GameLogServerUrl`
|
||||
* Specifies the HTTP Url for the Game Log Server
|
||||
* Default — `null`
|
||||
|
||||
`AutoMessagePeriod`
|
||||
* Specifies (in seconds) how often messages should be broadcasted to each server
|
||||
* Default — `60`
|
||||
|
||||
`AutoMessages`
|
||||
* Specifies the list of messages that are broadcasted to **all** servers
|
||||
* Specially formatted **tokens** can be used to broadcast dynamic information
|
||||
* `{{TOTALPLAYERS}}` — displays how many players have connected
|
||||
* `{{TOPSTATS}}` — displays the top 5 players on the server based on performance
|
||||
* `{{MOSTPLAYED}}` — displays the top 5 players based on number of kills
|
||||
* `{{TOTALPLAYTIME}}` — displays the cumulative play time (in man-hours) on all monitored servers
|
||||
* `{{VERSION}}` — displays the version of **IW4MAdmin**
|
||||
* `{{ADMINS}}` — displays the currently connected and *unmasked* privileged users online
|
||||
* `{{NEXTMAP}}` — displays the next map and gametype in rotation
|
||||
|
||||
`GlobalRules`
|
||||
* Specifies the list of rules that apply to **all** servers`
|
||||
|
||||
`Maps`
|
||||
* Specifies the list of maps for each supported game
|
||||
* `Name`
|
||||
* Specifies the name of the map as returned by the game (usually the file name sans the file extension)
|
||||
* `Alias`
|
||||
* Specifies the display name of the map (as seen while loading in)
|
||||
___
|
||||
|
||||
### Commands
|
||||
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|
||||
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ---------------|
|
||||
|prune|pa|demote any trusted clients that have not connected recently (defaults to 30 days)|False|!pa \<optional inactive days\>|Owner|
|
||||
|quit|q|quit IW4MAdmin|False|!q |Owner|
|
||||
|rcon|rcon|send rcon command to server|False|!rcon \<commands\>|Owner|
|
||||
|ban|b|permanently ban a client from the server|True|!b \<player\> \<reason\>|SeniorAdmin|
|
||||
|unban|ub|unban client by client id|True|!ub \<client id\> \<reason\>|SeniorAdmin|
|
||||
|find|f|find client in database|False|!f \<player\>|Administrator|
|
||||
|killserver|kill|kill the game server|False|!kill |Administrator|
|
||||
|map|m|change to specified map|False|!m \<map\>|Administrator|
|
||||
|maprotate|mr|cycle to the next map in rotation|False|!mr |Administrator|
|
||||
|plugins|p|view all loaded plugins|False|!p |Administrator|
|
||||
|tempban|tb|temporarily ban a client for specified time (defaults to 1 hour)|True|!tb \<player\> \<duration (m\|h\|d\|w\|y)\> \<reason\>|Administrator|
|
||||
|alias|known|get past aliases and ips of a client|True|!known \<player\>|Moderator|
|
||||
|baninfo|bi|get information about a ban for a client|True|!bi \<player\>|Moderator|
|
||||
|fastrestart|fr|fast restart current map|False|!fr |Moderator|
|
||||
|flag|fp|flag a suspicious client and announce to admins on join|True|!fp \<player\> \<reason\>|Moderator|
|
||||
|kick|k|kick a client by name|True|!k \<player\> \<reason\>|Moderator|
|
||||
|list|l|list active clients|False|!l |Moderator|
|
||||
|mask|hide|hide your presence as a privileged client|False|!hide |Moderator|
|
||||
|reports|reps|get or clear recent reports|False|!reps \<optional clear\>|Moderator|
|
||||
|say|s|broadcast message to all clients|False|!s \<message\>|Moderator|
|
||||
|setlevel|sl|set client to specified privilege level|True|!sl \<player\> \<level\>|Moderator|
|
||||
|setpassword|sp|set your authentication password|False|!sp \<password\>|Moderator|
|
||||
|unflag|uf|Remove flag for client|True|!uf \<player\>|Moderator|
|
||||
|uptime|up|get current application running time|False|!up |Moderator|
|
||||
|usage|us|get application memory usage|False|!us |Moderator|
|
||||
|warn|w|warn client for infringing rules|True|!w \<player\> \<reason\>|Trusted|
|
||||
|warnclear|wc|remove all warnings for a client|True|!wc \<player\>|Trusted|
|
||||
|admins|a|list currently connected privileged clients|False|!a |User|
|
||||
|getexternalip|ip|view your external IP address|False|!ip |User|
|
||||
|help|h|list all available commands|False|!h \<optional commands\>|User|
|
||||
|nextmap|nm|view next map in rotation|False|!nm |User|
|
||||
|owner|iamgod|claim ownership of the server|False|!iamgod |User|
|
||||
|ping|pi|get client's latency|False|!pi \<optional player\>|User|
|
||||
|privatemessage|pm|send message to other client|True|!pm \<player\> \<message\>|User|
|
||||
|report|rep|report a client for suspicious behavior|True|!rep \<player\> \<reason\>|User|
|
||||
|rules|r|list server rules|False|!r |User|
|
||||
|setgravatar|sg|set gravatar for webfront profile|False|!sg \<gravatar email\>|User|
|
||||
|whoami|who|give information about yourself|False|!who |User|
|
||||
|
||||
|
||||
_These commands include all shipped plugin commands._
|
||||
|
||||
---
|
||||
|
||||
#### Player Identification
|
||||
All players are identified 5 separate ways
|
||||
1. `npID/GUID/XUID` - The ID corresponding to the player's hardware or forum account
|
||||
2. `IP` - The player's IP Address
|
||||
3. `Client ID` - The internal reference to a player, generated by **IW4MAdmin**
|
||||
4. `Name` - The visible player name as it appears in game
|
||||
5. `Client Number` - The slot the client occupies on a server. (The number ranges between 0 and the max number of clients allowed on the server)
|
||||
|
||||
For most commands players are identified by their `Name`
|
||||
However, if they are currently offline, or their name contains un-typable characters, their `Client ID` must be used
|
||||
|
||||
The `Client ID` is specified by prefixing a player's reference number with `@`.
|
||||
For example, `@123` would reference the player with a `Client ID` of 123.
|
||||
|
||||
**All commands that require a `target` look at the `first argument` for a form of player identification**
|
||||
|
||||
---
|
||||
|
||||
#### Additional Command Examples
|
||||
`setlevel`
|
||||
- _shortcut_ - `sl`
|
||||
- _Parameter 1_ - Player to modify level of
|
||||
- _Parameter 2_ - Level to set the player to ```[ User, Trusted, Moderator, Administrator, SeniorAdmin, Owner ]```
|
||||
- _Example_ - `!setlevel Player1 SeniorAdmin`, `!sl @123 Moderator`
|
||||
- **NOTE** - An `owner` cannot set another player's level to `owner` unless the configuration option is enabled during setup
|
||||
|
||||
`ban`
|
||||
- _Shortcut_ - `b`
|
||||
- _Parameter 1_ - Player to ban
|
||||
- _Parameter 2_ - Reason for ban
|
||||
- _Example_ - `!ban Player1 caught cheating`, `!b @123 GUID Spoofing`
|
||||
|
||||
`tempban`
|
||||
- _Shortcut_ - `tb`
|
||||
- _Parameter 1_ - Player to ban
|
||||
- _Parameter 2_ - Ban length (minutes|hours|days|weeks|years)
|
||||
- _Parameter 3_ - Reason for ban
|
||||
- _Example_ - `!tempban Player1 3w racism`, `!tb @123 8h Abusive behaivor`
|
||||
|
||||
`reports`
|
||||
- _Shortcut_ - `reps`
|
||||
- _Optional Parameter 1_ - `clear` (erases reports for current server)
|
||||
|
||||
___
|
||||
### Plugins
|
||||
|
||||
#### Welcome
|
||||
- This plugin uses geo-location data to welcome a player based on their country of origin
|
||||
- All privileged users ( Trusted or higher ) receive a specialized welcome message as well
|
||||
- Welcome messages can be customized in `WelcomePluginSettings.json`
|
||||
|
||||
#### Stats
|
||||
- This plugin calculates basic player performance, skill approximation, and kill/death ratio
|
||||
- Skill is an number derived from an algorithmic processing of a player's Kill Death Ratio (KDR) and Score per Minute (SPM).
|
||||
- Elo Rating is based off of the number of encounters a player wins.
|
||||
- Performance is the average of Skill + Elo Rating
|
||||
|
||||
**Commands added by this plugin**
|
||||
|
||||
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|
||||
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|
|
||||
|resetstats|rs|reset your stats to factory-new|False|!rs |User|
|
||||
|stats|xlrstats|view your stats|False|!xlrstats \<optional player\>|User|
|
||||
|topstats|ts|view the top 5 players on this server|False|!ts |User|
|
||||
|mostplayed|mp|view the top 5 dedicated players on the server|False|!mp |User|
|
||||
|
||||
- To qualify for top stats, a client must have played for at least `3 hours` and connected within the past `15 days`.
|
||||
|
||||
#### Login
|
||||
- This plugin deters GUID spoofing by requiring privileged users to login with their password before executing commands
|
||||
- A password must be set using the `setpassword` command before logging in
|
||||
|
||||
**Commands added by this plugin**
|
||||
|
||||
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|
||||
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|
|
||||
|login|l|login using password|False|!l \<password\>|Trusted|
|
||||
|
||||
#### Profanity Determent
|
||||
- This plugin warns and kicks players for using profanity
|
||||
- Profane words and warning message can be specified in `ProfanityDetermentSettings.json`
|
||||
- If a client's name contains a word listed in the settings, they will immediately be kicked
|
||||
|
||||
#### IW4 Script Commands
|
||||
- This plugin provides additional integration to IW4x
|
||||
- In order to take advantage of it, copy the `userraw` folder into your IW4x server directory
|
||||
|
||||
#### VPN Detection [Script Plugin]
|
||||
- This plugin detects if a client is using a VPN and kicks them if they are
|
||||
- To disable this plugin, delete `Plugins\VPNDetection.js`
|
||||
- Adding **Client IDs** to the `vpnExceptionIds` array will prevent a client from being kicked.
|
||||
|
||||
#### Shared GUID Kicker [Script Plugin]
|
||||
- This plugin kicks users using a specific GUID
|
||||
- GUID `F4D2C30B712AC6E3` on IW4x was packed into a torrent version of the game.
|
||||
___
|
||||
### Webfront
|
||||
`Home`
|
||||
* Shows an overview of the monitored server(s)
|
||||
|
||||
`Penalties`
|
||||
* Shows a chronological ordered list of client penalties (scrolling down loads older penalties)
|
||||
|
||||
`Admins`
|
||||
* Shows a list of privileged clients
|
||||
|
||||
`Login`
|
||||
* Allows privileged users to login using their `Client ID` and password set via `setpassword`
|
||||
* `ClientID` is a number that can be found by using `!find <client name>` or find the client on the webfront and copy the ID following `ProfileAsync/`
|
||||
|
||||
`Profile`
|
||||
* Shows a client's information and history
|
||||
|
||||
`Web Console`
|
||||
* Allows logged in privileged users to execute commands as if they are in-
|
||||
|
||||
`Search`
|
||||
* Query clients and messages
|
||||
|
||||
Advanced filters can be constructed to search for resources using the following filter table.
|
||||
| Filter | Description | Format | Example |
|
||||
|-----------|--------------------------------------------------------|-----------------------|---------------------|
|
||||
| before | include items occurring on or before the provided date | YYYY-MM-DD hh:mm:ss (UTC inferred) | 2020-05-21 23:00:00 |
|
||||
| after | include items occurring on or after the provided date | YYYY-MM-DD hh:mm:ss (UTC inferred) | 2015-01-01 |
|
||||
| server | include items matching the server id | ip:port | 127.0.0.1:28960 |
|
||||
| client | include items matching the client id | integer | 8947 |
|
||||
| contains | include items containing this substring | string | hack |
|
||||
| sort | display results in this order | ascending\|descending | descending |
|
||||
|
||||
Any number of filters can be combined in any order.
|
||||
Example — `chat|before 2020-05-21|after 2020-05-01|server 127.0.0.1:28960|client 444|contains cheating|sort descending`
|
||||
|
||||
---
|
||||
### Game Log Server
|
||||
The game log server provides a way to remotely host your server's log over a http rest-ful api.
|
||||
This feature is useful if you plan on running IW4MAdmin on a different machine than the game server.
|
||||
#### Requirements
|
||||
- [Python 3.8.x](https://www.python.org/downloads/) or newer
|
||||
|
||||
#### Installation
|
||||
1. With Python 3.x installed, open up a terminal/command prompt window in the `GameLogServer` folder and execute:
|
||||
```console
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
If this fails, you can alternatively try installing with:
|
||||
```console
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
2. Allow TCP port 1625 through firewall
|
||||
* [Windows Instructions](https://www.tomshardware.com/news/how-to-open-firewall-ports-in-windows-10,36451.html)
|
||||
* [Linux Instructions (iptables)](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-basic-iptables-firewall-on-centos-6#open-up-ports-for-selected-services)
|
||||
#### Launching
|
||||
With Python 3 installed, open a terminal/command prompt window open in the `GameServerLog` folder and execute:
|
||||
```console
|
||||
python runserver.py
|
||||
```
|
||||
The Game Log Server window will need to remain running/open as long as **IW4MAdmin** is running
|
||||
|
||||
#### Configuring
|
||||
* Update your `IW4MAdminSettings.json` by changing the value of `GameLogServerUrl` to "http://<remote_server_ip>:1625"
|
||||
* Example — `"GameLogServerUrl": "http://192.168.1.123:1625",`
|
||||
---
|
||||
### Extending Plugins
|
||||
|
||||
#### NuGet Package
|
||||
The NuGet package for **IW4MAdmin's** "Shared Library" can be obtained from the [NuGet Gallery](https://www.nuget.org/packages/RaidMax.IW4MAdmin.SharedLibraryCore)
|
||||
Referencing this package will give you the ability to write plugins against **IW4MAdmin's** core library.
|
||||
#### Code
|
||||
**IW4MAdmin's** functionality can be extended by writing additional plugins in C#.
|
||||
Each class library must implement the `IPlugin` interface.
|
||||
See the existing [plugins](https://github.com/RaidMax/IW4M-Admin/tree/master/Plugins) for examples.
|
||||
#### JavaScript
|
||||
**IW4MAdmin** functionality can also be extended using JavaScript.
|
||||
The JavaScript parser supports [ECMA 5.1](https://ecma-international.org/ecma-262/5.1/) standards.
|
||||
#### Plugin Object Template
|
||||
In order to be properly parsed by the JavaScript engine, every plugin must conform to the following template.
|
||||
```js
|
||||
var plugin = {
|
||||
author: 'YourHandle',
|
||||
version: 1.0,
|
||||
name: 'Sample JavaScript Plugin',
|
||||
|
||||
onEventAsync: function (gameEvent, server) {
|
||||
},
|
||||
|
||||
onLoadAsync: function (manager) {
|
||||
},
|
||||
|
||||
onUnloadAsync: function () {
|
||||
},
|
||||
|
||||
onTickAsync: function (server) {
|
||||
}
|
||||
};
|
||||
```
|
||||
#### Required Properties
|
||||
- `author` — [string] Author of the plugin (usually your name or online name/alias)
|
||||
- `version` — [float] Version number of your plugin (useful if you release several different versions)
|
||||
- `name` — [string] Name of your plugin (be descriptive!)
|
||||
- `onEventAsync` — [function] Handler executed when an event occurs
|
||||
- `gameEvent` — [parameter object] Object containing event type, origin, target, and other info (see the GameEvent class declaration)
|
||||
- `server` — [parameter object] Object containing information and methods about the server the event occured on (see the Server class declaration)
|
||||
- `onLoadAsync` — [function] Handler executed when the plugin is loaded by code
|
||||
- `manager` — [parameter object] Object reference to the application manager (see the IManager interface definition)
|
||||
- `onUnloadAsync` — [function] Handler executed when the plugin is unloaded by code (see live reloading)
|
||||
- `onTickAsync` — [function] Handler executed approximately once per second by code *(unimplemented as of version 2.\*)*
|
||||
- `server` — [parameter object] Object containing information and methods about the server the event occured on (see the Server class declaration)
|
||||
### Live Reloading
|
||||
Thanks to JavaScript's flexibility and parsability, the plugin importer scans the plugins folder and reloads the JavaScript plugins on demand as they're modified. This allows faster development/testing/debugging.
|
||||
|
||||
---
|
||||
### Misc
|
||||
#### Anti-cheat
|
||||
This is an [IW4x](https://iw4xcachep26muba.onion.link/) only feature (wider game support planned), that uses analytics to detect aimbots and aim-assist tools.
|
||||
To utilize anti-cheat, enable it during setup **and** copy `_customcallbacks.gsc` from `userraw` into your `IW4x Server\userraw\scripts` folder.
|
||||
|
||||
The anti-cheat feature is a work in progress and as such will be constantly tweaked and may not be 100% accurate, however the goal is to deter as many cheaters as possible from IW4x.
|
||||
#### Database Storage
|
||||
By default, all **IW4MAdmin** information is stored in `Database.db`.
|
||||
|
||||
Should you need to reset your database, this file can simply be deleted.
|
||||
Additionally, this file should be preserved during updates to retain client information.
|
||||
|
||||
Setting the `ConnectionString` and `DatabaseProvider` properties in `IW4MAdminSettings.json`
|
||||
will allow **IW4MAdmin** to use alternate methods for database storage
|
||||
#### Explore the [wiki](https://github.com/RaidMax/IW4M-Admin/wiki) to find more information.
|
||||
|
@ -57,6 +57,13 @@ namespace SharedLibraryCore
|
||||
ViewBag.Version = Manager.Version;
|
||||
ViewBag.IsFluid = false;
|
||||
ViewBag.EnableColorCodes = Manager.GetApplicationSettings().Configuration().EnableColorCodes;
|
||||
|
||||
Client ??= new EFClient()
|
||||
{
|
||||
ClientId = -1,
|
||||
Level = EFClient.Permission.User,
|
||||
CurrentAlias = new EFAlias() { Name = "Webfront Guest" }
|
||||
};
|
||||
}
|
||||
|
||||
protected async Task SignInAsync(ClaimsPrincipal claimsPrinciple)
|
||||
@ -72,13 +79,6 @@ namespace SharedLibraryCore
|
||||
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
Client = Client ?? new EFClient()
|
||||
{
|
||||
ClientId = -1,
|
||||
Level = EFClient.Permission.User,
|
||||
CurrentAlias = new EFAlias() { Name = "Webfront Guest" }
|
||||
};
|
||||
|
||||
if (!HttpContext.Connection.RemoteIpAddress.GetAddressBytes().SequenceEqual(LocalHost))
|
||||
{
|
||||
try
|
||||
@ -108,7 +108,7 @@ namespace SharedLibraryCore
|
||||
}
|
||||
|
||||
// give the local host full access
|
||||
else
|
||||
else if (!HttpContext.Request.Headers.ContainsKey("X-Forwarded-For"))
|
||||
{
|
||||
Client.ClientId = 1;
|
||||
Client.Level = EFClient.Permission.Console;
|
||||
|
@ -5,6 +5,7 @@ using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace SharedLibraryCore
|
||||
{
|
||||
@ -13,7 +14,7 @@ namespace SharedLibraryCore
|
||||
/// </summary>
|
||||
public abstract class Command : IManagerCommand
|
||||
{
|
||||
private readonly CommandConfiguration _config;
|
||||
protected readonly CommandConfiguration _config;
|
||||
protected readonly ITranslationLookup _translationLookup;
|
||||
protected ILogger logger;
|
||||
|
||||
@ -59,7 +60,7 @@ namespace SharedLibraryCore
|
||||
/// <summary>
|
||||
/// Helper property to provide the syntax of the command
|
||||
/// </summary>
|
||||
public string Syntax => $"{_translationLookup["COMMAND_HELP_SYNTAX"]} !{Alias} {string.Join(" ", Arguments.Select(a => $"<{(a.Required ? "" : _translationLookup["COMMAND_HELP_OPTIONAL"] + " ")}{a.Name}>"))}";
|
||||
public string Syntax => $"{_translationLookup["COMMAND_HELP_SYNTAX"]} {_config.CommandPrefix}{Alias} {string.Join(" ", Arguments.Select(a => $"<{(a.Required ? "" : _translationLookup["COMMAND_HELP_OPTIONAL"] + " ")}{a.Name}>"))}";
|
||||
|
||||
/// <summary>
|
||||
/// Alternate name for this command to be executed by
|
||||
@ -113,6 +114,25 @@ namespace SharedLibraryCore
|
||||
}
|
||||
private EFClient.Permission permission;
|
||||
|
||||
public Game[] SupportedGames
|
||||
{
|
||||
get => supportedGames;
|
||||
protected set
|
||||
{
|
||||
try
|
||||
{
|
||||
var savedGames = _config?.Commands[GetType().Name].SupportedGames;
|
||||
supportedGames = savedGames?.Length != 0 ? savedGames : value;
|
||||
}
|
||||
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
supportedGames = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
private Game[] supportedGames;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Argument list for the command
|
||||
@ -123,5 +143,7 @@ namespace SharedLibraryCore
|
||||
/// indicates if this command allows impersonation (run as)
|
||||
/// </summary>
|
||||
public bool AllowImpersonation { get; set; }
|
||||
|
||||
public bool IsBroadcast { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -10,12 +11,14 @@ namespace SharedLibraryCore.Commands
|
||||
{
|
||||
public class CommandProcessing
|
||||
{
|
||||
public static async Task<Command> ValidateCommand(GameEvent E)
|
||||
public static async Task<Command> ValidateCommand(GameEvent E, ApplicationConfiguration appConfig)
|
||||
{
|
||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
var Manager = E.Owner.Manager;
|
||||
bool isBroadcast = E.Data.StartsWith(appConfig.BroadcastCommandPrefix);
|
||||
int prefixLength = isBroadcast ? appConfig.BroadcastCommandPrefix.Length : appConfig.CommandPrefix.Length;
|
||||
|
||||
string CommandString = E.Data.Substring(1, E.Data.Length - 1).Split(' ')[0];
|
||||
string CommandString = E.Data.Substring(prefixLength, E.Data.Length - prefixLength).Split(' ')[0];
|
||||
E.Message = E.Data;
|
||||
|
||||
Command C = null;
|
||||
@ -34,6 +37,8 @@ namespace SharedLibraryCore.Commands
|
||||
throw new CommandException($"{E.Origin} entered unknown command \"{CommandString}\"");
|
||||
}
|
||||
|
||||
C.IsBroadcast = isBroadcast;
|
||||
|
||||
if (!C.AllowImpersonation && E.ImpersonationOrigin != null)
|
||||
{
|
||||
E.ImpersonationOrigin.Tell(loc["COMMANDS_RUN_AS_FAIL"]);
|
||||
|
@ -53,7 +53,6 @@ namespace SharedLibraryCore.Commands
|
||||
|
||||
public override Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
MetaService.Clear();
|
||||
E.Owner.Manager.Restart();
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_RESTART_SUCCESS"]);
|
||||
return Task.CompletedTask;
|
||||
@ -292,7 +291,7 @@ namespace SharedLibraryCore.Commands
|
||||
switch ((await E.Target.TempBan(tempbanReason, length, E.Origin).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason)
|
||||
{
|
||||
case GameEvent.EventFailReason.None:
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_SUCCESS"].FormatExt(E.Target, length.TimeSpanText()));
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_SUCCESS"].FormatExt(E.Target, length.HumanizeForCurrentCulture()));
|
||||
break;
|
||||
case GameEvent.EventFailReason.Exception:
|
||||
E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
|
||||
@ -798,7 +797,7 @@ namespace SharedLibraryCore.Commands
|
||||
{
|
||||
foreach (string line in OnlineAdmins(E.Owner, _translationLookup).Split(Environment.NewLine))
|
||||
{
|
||||
var _ = E.Message.IsBroadcastCommand() ? E.Owner.Broadcast(line) : E.Origin.Tell(line);
|
||||
var _ = E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix) ? E.Owner.Broadcast(line) : E.Origin.Tell(line);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
@ -886,14 +885,9 @@ namespace SharedLibraryCore.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var P in db_players)
|
||||
foreach (var client in db_players)
|
||||
{
|
||||
// they're not going by another alias
|
||||
// /*P.AliasLink.Children.FirstOrDefault(a => a.Name.ToLower().Contains(E.Data.ToLower()))?.Name*/
|
||||
string msg = P.Name.ToLower().Contains(E.Data.ToLower()) ?
|
||||
$"[^3{P.Name}^7] [^3@{P.ClientId}^7] - [{ Utilities.ConvertLevelToColor((Permission)P.LevelInt, P.Level)}^7] - {P.IPAddress} | last seen {Utilities.GetTimePassed(P.LastConnection)}" :
|
||||
$"()->[^3{P.Name}^7] [^3@{P.ClientId}^7] - [{ Utilities.ConvertLevelToColor((Permission)P.LevelInt, P.Level)}^7] - {P.IPAddress} | last seen {Utilities.GetTimePassed(P.LastConnection)}";
|
||||
E.Origin.Tell(msg);
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_FIND_FORMAT"].FormatExt(client.Name, client.ClientId, Utilities.ConvertLevelToColor((Permission)client.LevelInt, client.Level), client.IPAddress, client.LastConnectionText));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -917,7 +911,7 @@ namespace SharedLibraryCore.Commands
|
||||
if (E.Owner.Manager.GetApplicationSettings().Configuration().GlobalRules?.Length < 1 &&
|
||||
E.Owner.ServerConfig.Rules?.Length < 1)
|
||||
{
|
||||
var _ = E.Message.IsBroadcastCommand() ?
|
||||
var _ = E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix) ?
|
||||
E.Owner.Broadcast(_translationLookup["COMMANDS_RULES_NONE"]) :
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_RULES_NONE"]);
|
||||
}
|
||||
@ -933,7 +927,7 @@ namespace SharedLibraryCore.Commands
|
||||
|
||||
foreach (string r in rules)
|
||||
{
|
||||
var _ = E.Message.IsBroadcastCommand() ? E.Owner.Broadcast($"- {r}") : E.Origin.Tell($"- {r}");
|
||||
var _ = E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix) ? E.Owner.Broadcast($"- {r}") : E.Origin.Tell($"- {r}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1251,7 +1245,7 @@ namespace SharedLibraryCore.Commands
|
||||
|
||||
else
|
||||
{
|
||||
string remainingTime = (penalty.Expires.Value - DateTime.UtcNow).TimeSpanText();
|
||||
string remainingTime = (penalty.Expires.Value - DateTime.UtcNow).HumanizeForCurrentCulture();
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_BANINFO_TB_SUCCESS"].FormatExt(E.Target.Name, penalty.Offense, remainingTime));
|
||||
}
|
||||
}
|
||||
@ -1535,7 +1529,9 @@ namespace SharedLibraryCore.Commands
|
||||
/// </summary>
|
||||
public class SetGravatarCommand : Command
|
||||
{
|
||||
public SetGravatarCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
|
||||
private readonly IMetaService _metaService;
|
||||
|
||||
public SetGravatarCommand(CommandConfiguration config, ITranslationLookup translationLookup, IMetaService metaService) : base(config, translationLookup)
|
||||
{
|
||||
Name = "setgravatar";
|
||||
Description = _translationLookup["COMMANDS_GRAVATAR_DESC"];
|
||||
@ -1550,17 +1546,17 @@ namespace SharedLibraryCore.Commands
|
||||
Required = true
|
||||
}
|
||||
};
|
||||
|
||||
_metaService = metaService;
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
var metaSvc = new MetaService();
|
||||
|
||||
using (var md5 = MD5.Create())
|
||||
{
|
||||
string gravatarEmail = string.Concat(md5.ComputeHash(E.Data.ToLower().Select(d => Convert.ToByte(d)).ToArray())
|
||||
.Select(h => h.ToString("x2")));
|
||||
await metaSvc.AddPersistentMeta("GravatarEmail", gravatarEmail, E.Origin);
|
||||
await _metaService.AddPersistentMeta("GravatarEmail", gravatarEmail, E.Origin);
|
||||
}
|
||||
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_GRAVATAR_SUCCESS_NEW"]);
|
||||
|
37
SharedLibraryCore/Commands/PrivateMessageAdminsCommand.cs
Normal file
37
SharedLibraryCore/Commands/PrivateMessageAdminsCommand.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace SharedLibraryCore.Commands
|
||||
{
|
||||
public class PrivateMessageAdminsCommand : Command
|
||||
{
|
||||
public PrivateMessageAdminsCommand(CommandConfiguration config, ITranslationLookup lookup) : base(config, lookup)
|
||||
{
|
||||
Name = "privatemessageadmin";
|
||||
Description = lookup["COMMANDS_PMADMINS_DESC"];
|
||||
Alias = "pma";
|
||||
Permission = EFClient.Permission.Moderator;
|
||||
SupportedGames = new[] { Game.IW4, Game.IW5 };
|
||||
}
|
||||
|
||||
public override Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
bool isGameSupported = _config.Commands[nameof(PrivateMessageAdminsCommand)].SupportedGames.Length > 0 &&
|
||||
_config.Commands[nameof(PrivateMessageAdminsCommand)].SupportedGames.Contains(E.Owner.GameName);
|
||||
|
||||
if (!isGameSupported)
|
||||
{
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_GAME_NOT_SUPPORTED"].FormatExt(nameof(PrivateMessageAdminsCommand)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
E.Owner.ToAdmins(E.Data);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@ -66,6 +66,12 @@ namespace SharedLibraryCore.Configuration
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")]
|
||||
public string CustomLocale { get; set; }
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_COMMAND_PREFIX")]
|
||||
public string CommandPrefix { get; set; } = "!";
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_BROADCAST_COMMAND_PREFIX")]
|
||||
public string BroadcastCommandPrefix { get; set; } = "@";
|
||||
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_DB_PROVIDER")]
|
||||
public string DatabaseProvider { get; set; } = "sqlite";
|
||||
[ConfigurationOptional]
|
||||
|
@ -1,6 +1,7 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SharedLibraryCore.Configuration
|
||||
{
|
||||
@ -14,6 +15,18 @@ namespace SharedLibraryCore.Configuration
|
||||
/// </summary>
|
||||
public Dictionary<string, CommandProperties> Commands { get; set; } = new Dictionary<string, CommandProperties>();
|
||||
|
||||
/// <summary>
|
||||
/// prefix indicated the chat message is a command
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string CommandPrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// prefix indicating that the chat message is a broadcast command
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string BroadcastCommandPrefix { get; set; }
|
||||
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System.Text.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using static SharedLibraryCore.Database.Models.EFClient;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace SharedLibraryCore.Configuration
|
||||
{
|
||||
@ -29,5 +30,11 @@ namespace SharedLibraryCore.Configuration
|
||||
/// Indicates if the command can be run by another user (impersonation)
|
||||
/// </summary>
|
||||
public bool AllowImpersonation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the games supporting the functionality of the command
|
||||
/// </summary>
|
||||
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
|
||||
public Game[] SupportedGames { get; set; } = new Game[0];
|
||||
}
|
||||
}
|
||||
|
@ -64,13 +64,19 @@ namespace SharedLibraryCore.Configuration.Validation
|
||||
RuleFor(_app => _app.GlobalRules)
|
||||
.NotNull();
|
||||
|
||||
RuleForEach(_app => _app.Servers)
|
||||
.NotEmpty()
|
||||
.SetValidator(new ServerConfigurationValidator());
|
||||
|
||||
RuleFor(_app => _app.MasterUrl)
|
||||
.NotNull()
|
||||
.Must(_url => _url != null && _url.Scheme == Uri.UriSchemeHttp);
|
||||
|
||||
RuleFor(_app => _app.CommandPrefix)
|
||||
.NotEmpty();
|
||||
|
||||
RuleFor(_app => _app.BroadcastCommandPrefix)
|
||||
.NotEmpty();
|
||||
|
||||
RuleForEach(_app => _app.Servers)
|
||||
.NotEmpty()
|
||||
.SetValidator(new ServerConfigurationValidator());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ namespace SharedLibraryCore.Database
|
||||
|
||||
static string _ConnectionString;
|
||||
static string _provider;
|
||||
private static readonly string _migrationPluginDirectory = @"X:\IW4MAdmin\BUILD\Plugins";
|
||||
private static readonly ILoggerFactory _loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.AddConsole()
|
||||
@ -72,7 +71,7 @@ namespace SharedLibraryCore.Database
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
//optionsBuilder.UseLoggerFactory(_loggerFactory)
|
||||
// .EnableSensitiveDataLogging();
|
||||
// .EnableSensitiveDataLogging();
|
||||
|
||||
if (string.IsNullOrEmpty(_ConnectionString))
|
||||
{
|
||||
@ -198,11 +197,14 @@ namespace SharedLibraryCore.Database
|
||||
|
||||
// adapted from
|
||||
// https://aleemkhan.wordpress.com/2013/02/28/dynamically-adding-dbset-properties-in-dbcontext-for-entity-framework-code-first/
|
||||
#if DEBUG
|
||||
string pluginDir = _migrationPluginDirectory;
|
||||
#else
|
||||
|
||||
string pluginDir = Path.Join(Utilities.OperatingDirectory, "Plugins");
|
||||
#endif
|
||||
|
||||
if (Utilities.IsDevelopment)
|
||||
{
|
||||
pluginDir = Path.Join(Utilities.OperatingDirectory, "..", "..", "..", "..", "BUILD", "Plugins");
|
||||
}
|
||||
|
||||
IEnumerable<string> directoryFiles = Directory.GetFiles(pluginDir).Where(f => f.EndsWith(".dll"));
|
||||
|
||||
foreach (string dllPath in directoryFiles)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
@ -11,5 +12,7 @@ namespace SharedLibraryCore.Dtos
|
||||
public string Name { get; set; }
|
||||
public Game ServerGame { get; set; }
|
||||
public bool IsQuickMessage { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public string HiddenMessage => string.Concat(Enumerable.Repeat('●', Message.Length));
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
namespace SharedLibraryCore.Dtos
|
||||
{
|
||||
public class FindClientRequest : PaginationInfo
|
||||
public class FindClientRequest : PaginationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// name of client
|
||||
|
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Requests
|
||||
{
|
||||
public class BaseClientMetaRequest : PaginationRequest
|
||||
{
|
||||
public int ClientId { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Requests
|
||||
{
|
||||
public class ReceivedPenaltyRequest : BaseClientMetaRequest
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Responses
|
||||
{
|
||||
public class AdministeredPenaltyResponse : ReceivedPenaltyResponse
|
||||
{
|
||||
}
|
||||
}
|
19
SharedLibraryCore/Dtos/Meta/Responses/BaseMetaResponse.cs
Normal file
19
SharedLibraryCore/Dtos/Meta/Responses/BaseMetaResponse.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Responses
|
||||
{
|
||||
public class BaseMetaResponse : IClientMeta, IClientMetaResponse
|
||||
{
|
||||
public int MetaId { get; set; }
|
||||
public int ClientId { get; set; }
|
||||
public MetaType Type { get; set; }
|
||||
public DateTime When { get; set; }
|
||||
public bool IsSensitive { get; set; }
|
||||
public bool ShouldDisplay { get; set; }
|
||||
public int? Column { get; set; }
|
||||
public int? Order { get; set; }
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user