Compare commits

...

11 Commits

Author SHA1 Message Date
RaidMax
9f16d27630 add say all (broadcast) command 2020-10-07 08:48:12 -05:00
RaidMax
c636cc3e74 fix issue with button detection 2020-10-02 16:45:55 -05:00
RaidMax
4d9356982a hide flag status for non logged in users
remove erroneous anticheat detection reason on kick
2020-10-02 08:29:20 -05:00
RaidMax
fa75478998 fix anticheat detection type logic 2020-10-02 08:09:38 -05:00
RaidMax
e56f574af4 fix introduced bug :) 2020-10-01 19:06:12 -05:00
RaidMax
513f495304 anticheat tweaks
- reset recoil state on map change
- refactor config
- remove m21 from chest detection
- allow ignored client ids
2020-10-01 19:05:52 -05:00
RaidMax
910faf427b enhance script plugin features
(support service resolver with generic args)
(support requiresTarget for command)
2020-10-01 19:05:38 -05:00
RaidMax
9117440566 merge to pre (#172)
* update GenerateGuidFromString to resolve to a stable hash code.
fix bots not showing up on live radar

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

* implement pm admins command for issue #170

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

* add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x
2020-09-21 15:37:31 -05:00
RaidMax
eb8145a168 add 0.0.0.0 as internal "ip" even though it's not actually a valid IP but for cod4x 2020-09-04 12:58:54 -05:00
RaidMax
a560e05df8 update pipeline file for seperate builds 2020-08-31 12:33:39 -05:00
37 changed files with 621 additions and 96 deletions

View File

@ -67,13 +67,14 @@ namespace IW4MAdmin.Application
private readonly IEventHandler _eventHandler; private readonly IEventHandler _eventHandler;
private readonly IScriptCommandFactory _scriptCommandFactory; private readonly IScriptCommandFactory _scriptCommandFactory;
private readonly IMetaRegistration _metaRegistration; private readonly IMetaRegistration _metaRegistration;
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands, public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration, ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory, IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
IMetaRegistration metaRegistration) IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
@ -100,6 +101,7 @@ namespace IW4MAdmin.Application
_eventHandler = eventHandler; _eventHandler = eventHandler;
_scriptCommandFactory = scriptCommandFactory; _scriptCommandFactory = scriptCommandFactory;
_metaRegistration = metaRegistration; _metaRegistration = metaRegistration;
_scriptPluginServiceResolver = scriptPluginServiceResolver;
Plugins = plugins; Plugins = plugins;
} }
@ -277,12 +279,12 @@ namespace IW4MAdmin.Application
{ {
if (plugin is ScriptPlugin scriptPlugin) if (plugin is ScriptPlugin scriptPlugin)
{ {
await scriptPlugin.Initialize(this, _scriptCommandFactory); await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
scriptPlugin.Watcher.Changed += async (sender, e) => scriptPlugin.Watcher.Changed += async (sender, e) =>
{ {
try try
{ {
await scriptPlugin.Initialize(this, _scriptCommandFactory); await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
} }
catch (Exception ex) catch (Exception ex)
@ -449,7 +451,8 @@ namespace IW4MAdmin.Application
Name = cmd.Name, Name = cmd.Name,
Alias = cmd.Alias, Alias = cmd.Alias,
MinimumPermission = cmd.Permission, MinimumPermission = cmd.Permission,
AllowImpersonation = cmd.AllowImpersonation AllowImpersonation = cmd.AllowImpersonation,
SupportedGames = cmd.SupportedGames
}); });
} }

View File

@ -255,6 +255,7 @@ namespace IW4MAdmin.Application.EventParsers
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()), ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting, State = EFClient.ClientState.Connecting,
}, },
Extra = originIdString,
RequiredEntity = GameEvent.EventRequiredEntity.None, RequiredEntity = GameEvent.EventRequiredEntity.None,
IsBlocking = true, IsBlocking = true,
GameTime = gameTime, GameTime = gameTime,

View File

@ -25,7 +25,7 @@ namespace IW4MAdmin.Application.Factories
} }
/// <inheritdoc/> /// <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 permissionEnum = Enum.Parse<Permission>(permission);
var argsArray = args.Select(_arg => new CommandArgument var argsArray = args.Select(_arg => new CommandArgument
@ -34,7 +34,7 @@ namespace IW4MAdmin.Application.Factories
Required = _arg.Item2 Required = _arg.Item2
}).ToArray(); }).ToArray();
return new ScriptCommand(name, alias, description, permissionEnum, argsArray, executeAction, _config, _transLookup); return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction, _config, _transLookup);
} }
} }
} }

View File

@ -816,6 +816,7 @@ namespace IW4MAdmin
Origin = client, Origin = client,
Owner = this, Owner = this,
IsBlocking = true, IsBlocking = true,
Extra = client.GetAdditionalProperty<string>("BotGuid"),
Source = GameEvent.EventSource.Status Source = GameEvent.EventSource.Status
}; };

View File

@ -249,6 +249,7 @@ namespace IW4MAdmin.Application
.AddSingleton<IEntityService<EFClient>, ClientService>() .AddSingleton<IEntityService<EFClient>, ClientService>()
.AddSingleton<IMetaService, MetaService>() .AddSingleton<IMetaService, MetaService>()
.AddSingleton<IMetaRegistration, MetaRegistration>() .AddSingleton<IMetaRegistration, MetaRegistration>()
.AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>, ReceivedPenaltyResourceQueryHelper>() .AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>, ReceivedPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>, AdministeredPenaltyResourceQueryHelper>() .AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>, AdministeredPenaltyResourceQueryHelper>()
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>, UpdatedAliasResourceQueryHelper>() .AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>, UpdatedAliasResourceQueryHelper>()

View File

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

View File

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

View File

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

View File

@ -203,10 +203,11 @@ namespace IW4MAdmin.Application.RconParsers
long networkId; long networkId;
string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString;
try try
{ {
string networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]]; networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
networkId = networkIdString.IsBotGuid() ? networkId = networkIdString.IsBotGuid() ?
name.GenerateGuidFromString() : name.GenerateGuidFromString() :
@ -234,6 +235,8 @@ namespace IW4MAdmin.Application.RconParsers
State = EFClient.ClientState.Connecting State = EFClient.ClientState.Connecting
}; };
client.SetAdditionalProperty("BotGuid", networkIdString);
StatusPlayers.Add(client); StatusPlayers.Add(client);
} }
} }

View File

@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc
GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc
PostPublish.ps1 = PostPublish.ps1 PostPublish.ps1 = PostPublish.ps1
pre-release-pipeline.yml = pre-release-pipeline.yml
README.md = README.md README.md = README.md
RunPublishPre.cmd = RunPublishPre.cmd RunPublishPre.cmd = RunPublishPre.cmd
RunPublishRelease.cmd = RunPublishRelease.cmd RunPublishRelease.cmd = RunPublishRelease.cmd

View File

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

View File

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

View File

@ -3,6 +3,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -17,12 +18,14 @@ namespace LiveRadar
public string Author => "RaidMax"; public string Author => "RaidMax";
private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler; private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler;
private readonly Dictionary<string, long> _botGuidLookups;
private bool addedPage; private bool addedPage;
private readonly object lockObject = new object(); private readonly object lockObject = new object();
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory) public Plugin(IConfigurationHandlerFactory configurationHandlerFactory)
{ {
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration"); _configurationHandler = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration");
_botGuidLookups = new Dictionary<string, long>();
} }
public Task OnEventAsync(GameEvent E, Server S) 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); _botGuidLookups.Add(botKey, E.Origin.NetworkId);
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid); }
}
}
if (client != null) if (E.Type == GameEvent.EventType.Other && E.Subtype == "LiveRadar")
{ {
radarUpdate.Name = client.Name.StripColors(); try
client.SetAdditionalProperty("LiveRadar", radarUpdate); {
} 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}"); radarUpdate.Name = client.Name.StripColors();
S.Logger.WriteDebug(e.GetExceptionInfo()); 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; return Task.CompletedTask;

View File

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

View File

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

View File

@ -20,7 +20,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Offset, Offset,
Strain, Strain,
Recoil, Recoil,
Snap Snap,
Button
}; };
public ChangeTracking<EFACSnapshot> Tracker { get; private set; } public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
@ -38,11 +39,12 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
ILogger Log; ILogger Log;
Strain Strain; Strain Strain;
readonly DateTime ConnectionTime = DateTime.UtcNow; readonly DateTime ConnectionTime = DateTime.UtcNow;
private double sessionAverageRecoilAmount; private double mapAverageRecoilAmount;
private double sessionAverageSnapAmount; private double sessionAverageSnapAmount;
private int sessionSnapHits; private int sessionSnapHits;
private EFClientKill lastHit; private EFClientKill lastHit;
private int validRecoilHitCount; private int validRecoilHitCount;
private int validButtonHitCount;
private class HitInfo private class HitInfo
{ {
@ -282,18 +284,30 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
#region RECOIL #region RECOIL
float hitRecoilAverage = 0; 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++; validRecoilHitCount++;
hitRecoilAverage = (hit.AnglesList.Sum(_angle => _angle.Z) + hit.ViewAngles.Z) / (hit.AnglesList.Count + 1); 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() results.Add(new DetectionPenaltyResult()
{ {
ClientPenalty = EFPenalty.PenaltyType.Ban, ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = sessionAverageRecoilAmount, Value = mapAverageRecoilAmount,
HitCount = HitCount, HitCount = HitCount,
Type = DetectionType.Recoil Type = DetectionType.Recoil
}); });
@ -301,6 +315,37 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
} }
#endregion #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 #region SESSION_RATIOS
if (Kills >= Thresholds.LowSampleMinKills) if (Kills >= Thresholds.LowSampleMinKills)
{ {
@ -384,7 +429,19 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
#region CHEST_ABDOMEN_RATIO_SESSION #region CHEST_ABDOMEN_RATIO_SESSION
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count; 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 marginOfError = Thresholds.GetMarginOfError(chestHits);
double lerpAmount = Math.Min(1.0, (chestHits - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills)); 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; return results;
} }
public void OnMapChange()
{
mapAverageRecoilAmount = 0;
validRecoilHitCount = 0;
}
} }
} }

View File

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

View File

@ -1,6 +1,7 @@
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using Stats.Config; using Stats.Config;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using static IW4MAdmin.Plugins.Stats.Cheat.Detection; using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
@ -8,21 +9,41 @@ namespace IW4MAdmin.Plugins.Stats.Config
{ {
public class StatsConfiguration : IBaseConfiguration public class StatsConfiguration : IBaseConfiguration
{ {
public bool EnableAntiCheat { get; set; } [Obsolete]
public bool? EnableAntiCheat { get; set; }
public List<StreakMessageConfiguration> KillstreakMessages { get; set; } public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
public List<StreakMessageConfiguration> DeathstreakMessages { get; set; } public List<StreakMessageConfiguration> DeathstreakMessages { get; set; }
public List<string> RecoilessWeapons { get; set; }
public int TopPlayersMinPlayTime { get; set; } public int TopPlayersMinPlayTime { get; set; }
public bool StoreClientKills { get; set; } public bool StoreClientKills { get; set; }
public int MostKillsMaxInactivityDays { get; set; } = 30; public int MostKillsMaxInactivityDays { get; set; } = 30;
public int MostKillsClientLimit { get; set; } = 5; public int MostKillsClientLimit { get; set; } = 5;
public IDictionary<DetectionType, DistributionConfiguration> DetectionDistributions { get; set; } [Obsolete]
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; } 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 string Name() => "StatsPluginSettings";
public IBaseConfiguration Generate() 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>() KillstreakMessages = new List<StreakMessageConfiguration>()
{ {
new 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; TopPlayersMinPlayTime = 3600 * 3;
StoreClientKills = false; StoreClientKills = false;
return this; return this;
} }
} }

View File

@ -481,7 +481,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
IsKill = !isDamage, IsKill = !isDamage,
AnglesList = snapshotAngles, AnglesList = snapshotAngles,
IsAlive = isAlive == "1", IsAlive = isAlive == "1",
TimeSinceLastAttack = long.Parse(lastAttackTime) TimeSinceLastAttack = long.Parse(lastAttackTime),
GameName = attacker.CurrentServer.GameName
}; };
if (hit.HitLoc == IW4Info.HitLocation.shield) if (hit.HitLoc == IW4Info.HitLocation.shield)
@ -539,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); clientDetection.TrackedHits.Add(hit);
@ -555,10 +556,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (oldestHit.IsAlive) if (oldestHit.IsAlive)
{ {
var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker.CurrentServer.EndPoint); var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker);
#if !DEBUG
await ApplyPenalty(result, attacker); if (!Utilities.IsDevelopment)
#endif {
await ApplyPenalty(result, attacker);
}
if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any) if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any)
{ {
@ -594,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 // 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) ?? return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ??
results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ?? results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
new DetectionPenaltyResult() new DetectionPenaltyResult()
@ -617,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) async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker)
@ -1139,10 +1152,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public void ResetKillstreaks(Server sv) public void ResetKillstreaks(Server sv)
{ {
foreach (var stat in sv.GetClientsAsList() foreach (var session in sv.GetClientsAsList()
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))) .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();
} }
} }
@ -1232,6 +1250,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return 886229536; return 886229536;
} }
// todo: this is not stable and will need to be migrated again...
long id = HashCode.Combine(server.IP, server.Port); long id = HashCode.Combine(server.IP, server.Port);
id = id < 0 ? Math.Abs(id) : id; id = id < 0 ? Math.Abs(id) : id;
long? serverId; long? serverId;

View File

@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Collections.Generic; using System.Collections.Generic;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Plugins.Stats.Models namespace IW4MAdmin.Plugins.Stats.Models
{ {
@ -44,6 +45,8 @@ namespace IW4MAdmin.Plugins.Stats.Models
public float AdsPercent { get; set; } public float AdsPercent { get; set; }
[NotMapped] [NotMapped]
public List<Vector3> AnglesList { get; set; } public List<Vector3> AnglesList { get; set; }
[NotMapped]
public Game GameName { get; set; }
/// <summary> /// <summary>
/// Indicates if the attacker was alive after last captured angle /// Indicates if the attacker was alive after last captured angle

View File

@ -182,8 +182,9 @@ namespace IW4MAdmin.Plugins.Stats
if (Config.Configuration() == null) if (Config.Configuration() == null)
{ {
Config.Set((StatsConfiguration)new StatsConfiguration().Generate()); Config.Set((StatsConfiguration)new StatsConfiguration().Generate());
await Config.Save();
} }
Config.Configuration().ApplyMigration();
await Config.Save();
// register the topstats page // register the topstats page
// todo:generate the URL/Location instead of hardcoding // todo:generate the URL/Location instead of hardcoding
@ -405,7 +406,7 @@ namespace IW4MAdmin.Plugins.Stats
return (await _chatQueryHelper.QueryResource(query)).Results; return (await _chatQueryHelper.QueryResource(query)).Results;
} }
if (Config.Configuration().EnableAntiCheat) if (Config.Configuration().AnticheatConfiguration.Enable)
{ {
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, getAnticheatInfo); _metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, getAnticheatInfo);
} }
@ -496,6 +497,6 @@ namespace IW4MAdmin.Plugins.Stats
/// </summary> /// </summary>
/// <param name="s"></param> /// <param name="s"></param>
/// <returns></returns> /// <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;
} }
} }

View File

@ -5,6 +5,7 @@ using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using static SharedLibraryCore.Server;
namespace SharedLibraryCore namespace SharedLibraryCore
{ {
@ -13,7 +14,7 @@ namespace SharedLibraryCore
/// </summary> /// </summary>
public abstract class Command : IManagerCommand public abstract class Command : IManagerCommand
{ {
private readonly CommandConfiguration _config; protected readonly CommandConfiguration _config;
protected readonly ITranslationLookup _translationLookup; protected readonly ITranslationLookup _translationLookup;
protected ILogger logger; protected ILogger logger;
@ -113,6 +114,25 @@ namespace SharedLibraryCore
} }
private EFClient.Permission permission; 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> /// <summary>
/// Argument list for the command /// Argument list for the command

View File

@ -239,6 +239,42 @@ namespace SharedLibraryCore.Commands
} }
} }
/// <summary>
/// Prints out a message to all clients on all servers
/// </summary>
public class SayAllCommand : Command
{
public SayAllCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{
Name = "sayall";
Description = _translationLookup["COMMANDS_SAY_ALL_DESC"];
Alias = "sa";
Permission = Permission.Moderator;
RequiresTarget = false;
Arguments = new[]
{
new CommandArgument()
{
Name = _translationLookup["COMMANDS_ARGS_MESSAGE"],
Required = true
}
};
}
public override Task ExecuteAsync(GameEvent E)
{
string message = _translationLookup["COMMANDS_SAY_ALL_MESSAGE_FORMAT"].FormatExt(E.Origin.Name, E.Data);
foreach (var server in E.Owner.Manager.GetServers())
{
server.Broadcast(message, E.Origin);
}
E.Origin.Tell(_translationLookup["COMMANDS_SAY_SUCCESS"]);
return Task.CompletedTask;
}
}
/// <summary> /// <summary>
/// Temporarily bans a client /// Temporarily bans a client
/// </summary> /// </summary>
@ -772,8 +808,6 @@ namespace SharedLibraryCore.Commands
/// </summary> /// </summary>
public class ListAdminsCommand : Command public class ListAdminsCommand : Command
{ {
private readonly CommandConfiguration _config;
public ListAdminsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) public ListAdminsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{ {
Name = "admins"; Name = "admins";
@ -781,8 +815,6 @@ namespace SharedLibraryCore.Commands
Alias = "a"; Alias = "a";
Permission = Permission.User; Permission = Permission.User;
RequiresTarget = false; RequiresTarget = false;
_config = config;
} }
public static string OnlineAdmins(Server S, ITranslationLookup lookup) public static string OnlineAdmins(Server S, ITranslationLookup lookup)
@ -901,8 +933,6 @@ namespace SharedLibraryCore.Commands
/// </summary> /// </summary>
public class ListRulesCommands : Command public class ListRulesCommands : Command
{ {
private readonly CommandConfiguration _config;
public ListRulesCommands(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup) public ListRulesCommands(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
{ {
Name = "rules"; Name = "rules";
@ -910,8 +940,6 @@ namespace SharedLibraryCore.Commands
Alias = "r"; Alias = "r";
Permission = Permission.User; Permission = Permission.User;
RequiresTarget = false; RequiresTarget = false;
_config = config;
} }
public override Task ExecuteAsync(GameEvent E) public override Task ExecuteAsync(GameEvent E)

View 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;
}
}
}

View File

@ -1,6 +1,7 @@
using Newtonsoft.Json.Converters; using Newtonsoft.Json;
using System.Text.Json.Serialization; using Newtonsoft.Json.Converters;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.Server;
namespace SharedLibraryCore.Configuration namespace SharedLibraryCore.Configuration
{ {
@ -29,5 +30,11 @@ namespace SharedLibraryCore.Configuration
/// Indicates if the command can be run by another user (impersonation) /// Indicates if the command can be run by another user (impersonation)
/// </summary> /// </summary>
public bool AllowImpersonation { get; set; } 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];
} }
} }

View File

@ -1,6 +1,4 @@
using System; namespace SharedLibraryCore.Dtos.Meta.Responses
namespace SharedLibraryCore.Dtos.Meta.Responses
{ {
public class UpdatedAliasResponse : BaseMetaResponse public class UpdatedAliasResponse : BaseMetaResponse
{ {
@ -17,6 +15,6 @@ namespace SharedLibraryCore.Dtos.Meta.Responses
return false; return false;
} }
public override int GetHashCode() => HashCode.Combine(Name.StripColors(), IPAddress); public override int GetHashCode() => $"{Name.StripColors()}{IPAddress}".GetStableHashCode();
} }
} }

View File

@ -1,5 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.Server;
namespace SharedLibraryCore.Interfaces namespace SharedLibraryCore.Interfaces
{ {
@ -35,6 +36,11 @@ namespace SharedLibraryCore.Interfaces
/// </summary> /// </summary>
Permission Permission { get; } Permission Permission { get; }
/// <summary>
/// Games the command is supported on
/// </summary>
Game[] SupportedGames { get; }
/// <summary> /// <summary>
/// Syntax for using the command /// Syntax for using the command
/// </summary> /// </summary>

View File

@ -15,9 +15,10 @@ namespace SharedLibraryCore.Interfaces
/// <param name="alias">alias of command</param> /// <param name="alias">alias of command</param>
/// <param name="description">description of command</param> /// <param name="description">description of command</param>
/// <param name="permission">minimum required permission</param> /// <param name="permission">minimum required permission</param>
/// <param name="isTargetRequired">target required or not</param>
/// <param name="args">command arguments (name, is required)</param> /// <param name="args">command arguments (name, is required)</param>
/// <param name="executeAction">action to peform when commmand is executed</param> /// <param name="executeAction">action to peform when commmand is executed</param>
/// <returns></returns> /// <returns></returns>
IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction); IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, bool isTargetRequired, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction);
} }
} }

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace SharedLibraryCore.Interfaces
{
/// <summary>
/// interface used to dynamically resolve services by string name
/// </summary>
public interface IScriptPluginServiceResolver
{
/// <summary>
/// resolves a service with the given name
/// </summary>
/// <param name="serviceName">class name of service</param>
/// <returns></returns>
object ResolveService(string serviceName);
/// <summary>
/// resolves a service with the given name and generic params
/// </summary>
/// <param name="serviceName">class name of service</param>
/// <param name="genericParams">generic class names</param>
/// <returns></returns>
object ResolveService(string serviceName, string[] genericParameters);
}
}

View File

@ -369,7 +369,27 @@ namespace SharedLibraryCore
/// </summary> /// </summary>
/// <param name="value">value string</param> /// <param name="value">value string</param>
/// <returns></returns> /// <returns></returns>
public static long GenerateGuidFromString(this string value) => string.IsNullOrEmpty(value) ? -1 : HashCode.Combine(value.StripColors()); public static long GenerateGuidFromString(this string value) => string.IsNullOrEmpty(value) ? -1 : GetStableHashCode(value.StripColors());
/// https://stackoverflow.com/questions/36845430/persistent-hashcode-for-strings
public static int GetStableHashCode(this string str)
{
unchecked
{
int hash1 = 5381;
int hash2 = hash1;
for (int i = 0; i < str.Length && str[i] != '\0'; i += 2)
{
hash1 = ((hash1 << 5) + hash1) ^ str[i];
if (i == str.Length - 1 || str[i + 1] == '\0')
break;
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
}
return hash1 + (hash2 * 1566083941);
}
}
public static int? ConvertToIP(this string str) public static int? ConvertToIP(this string str)
{ {
@ -773,6 +793,8 @@ namespace SharedLibraryCore
byte[] bytes = toTest.GetAddressBytes(); byte[] bytes = toTest.GetAddressBytes();
switch (bytes[0]) switch (bytes[0])
{ {
case 0:
return bytes[1] == 0 && bytes[2] == 0 && bytes[3] == 0;
case 10: case 10:
return true; return true;
case 172: case 172:
@ -954,7 +976,7 @@ namespace SharedLibraryCore
/// <summary> /// <summary>
/// wrapper method for humanizee that uses current current culture /// wrapper method for humanizee that uses current current culture
/// </summary> /// </summary>
public static string HumanizeForCurrentCulture(this TimeSpan timeSpan, int precision = 1, TimeUnit maxUnit = TimeUnit.Week, public static string HumanizeForCurrentCulture(this TimeSpan timeSpan, int precision = 1, TimeUnit maxUnit = TimeUnit.Week,
TimeUnit minUnit = TimeUnit.Millisecond, string collectionSeparator = ", ", bool toWords = false) TimeUnit minUnit = TimeUnit.Millisecond, string collectionSeparator = ", ", bool toWords = false)
{ {
return timeSpan.Humanize(precision, CurrentLocalization.Culture, maxUnit, minUnit, collectionSeparator, toWords); return timeSpan.Humanize(precision, CurrentLocalization.Culture, maxUnit, minUnit, collectionSeparator, toWords);

View File

@ -537,5 +537,46 @@ namespace ApplicationTests
Assert.IsTrue(result.IsBroadcast); Assert.IsTrue(result.IsBroadcast);
} }
#endregion #endregion
#region PMADMINS
[Test]
public async Task Test_PrivateMessageAdmins_HappyPath()
{
var cmd = new PrivateMessageAdminsCommand(cmdConfig, transLookup);
var server = serviceProvider.GetRequiredService<IW4MServer>();
var origin = ClientGenerators.CreateDatabaseClient();
origin.Level = Permission.Administrator;
origin.CurrentServer = server;
var gameEvent = EventGenerators.GenerateEvent(GameEvent.EventType.Command, "", server);
cmdConfig.Commands.Add(nameof(PrivateMessageAdminsCommand), new CommandProperties { SupportedGames = new[] { server.GameName } });
server.Clients[0] = origin;
server.Clients[1] = origin;
await cmd.ExecuteAsync(gameEvent);
int expectedEvents = 2;
Assert.AreEqual(expectedEvents, mockEventHandler.Events.Count(_event => _event.Type == GameEvent.EventType.Tell));
}
[Test]
public async Task Test_PrivateMessageAdmins_GameNotSupported()
{
var cmd = new PrivateMessageAdminsCommand(cmdConfig, transLookup);
var server = serviceProvider.GetRequiredService<IW4MServer>();
var origin = ClientGenerators.CreateDatabaseClient();
origin.Level = Permission.Administrator;
origin.CurrentServer = server;
var gameEvent = EventGenerators.GenerateEvent(GameEvent.EventType.Command, "", server);
gameEvent.Origin = origin;
cmdConfig.Commands.Add(nameof(PrivateMessageAdminsCommand), new CommandProperties());
server.Clients[0] = origin;
server.Clients[1] = origin;
await cmd.ExecuteAsync(gameEvent);
int expectedEvents = 1;
Assert.AreEqual(expectedEvents, mockEventHandler.Events.Count(_event => _event.Type == GameEvent.EventType.Tell));
}
#endregion
} }
} }

View File

@ -0,0 +1,23 @@
namespace ApplicationTests.Mocks
{
public interface IScriptResolverMock
{
string Value { get; set; }
}
public class ScriptResolverMock : IScriptResolverMock
{
public string Value { get; set; }
}
public interface IScriptResolverGenericMock<T, V>
{
T Value { get; set; }
V Value2 { get; set; }
}
public class ScriptResolverGenericMock<T, V> : IScriptResolverGenericMock<T, V>
{
public T Value { get; set; }
public V Value2 { get; set; }
}
}

View File

@ -33,6 +33,7 @@ namespace ApplicationTests
serviceProvider = new ServiceCollection().BuildBase() serviceProvider = new ServiceCollection().BuildBase()
.AddSingleton(A.Fake<ClientService>()) .AddSingleton(A.Fake<ClientService>())
.AddSingleton<IScriptCommandFactory, ScriptCommandFactory>() .AddSingleton<IScriptCommandFactory, ScriptCommandFactory>()
.AddSingleton(A.Fake<IScriptPluginServiceResolver>())
.BuildServiceProvider(); .BuildServiceProvider();
fakeManager = serviceProvider.GetRequiredService<IManager>(); fakeManager = serviceProvider.GetRequiredService<IManager>();
mockEventHandler = serviceProvider.GetRequiredService<EventHandlerMock>(); mockEventHandler = serviceProvider.GetRequiredService<EventHandlerMock>();
@ -66,7 +67,7 @@ namespace ApplicationTests
A.CallTo(() => fakeManager.GetClientService()) A.CallTo(() => fakeManager.GetClientService())
.Returns(fakeClientService); .Returns(fakeClientService);
await plugin.Initialize(serviceProvider.GetRequiredService<IManager>(), serviceProvider.GetRequiredService<IScriptCommandFactory>()); await plugin.Initialize(serviceProvider.GetRequiredService<IManager>(), serviceProvider.GetRequiredService<IScriptCommandFactory>(), serviceProvider.GetRequiredService<IScriptPluginServiceResolver>());
var gameEvent = new GameEvent() var gameEvent = new GameEvent()
{ {

View File

@ -0,0 +1,66 @@
using ApplicationTests.Mocks;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using System;
namespace ApplicationTests
{
public class ScriptPluginServiceResolverTests
{
private IServiceProvider serviceProvider;
[SetUp]
public void Setup()
{
serviceProvider = new ServiceCollection()
.BuildBase()
.AddSingleton<ScriptPluginServiceResolver>()
.AddSingleton<IScriptResolverMock, ScriptResolverMock>()
.AddSingleton(new ScriptResolverMock { Value = "test" })
.AddSingleton<IScriptResolverGenericMock<int, string>, ScriptResolverGenericMock<int,string>>()
.AddSingleton(new ScriptResolverGenericMock<int, string> { Value = 123, Value2 = "test" })
.BuildServiceProvider();
}
[Test]
public void Test_ResolveType()
{
var resolver = serviceProvider.GetService<ScriptPluginServiceResolver>();
var expectedResolvedService = serviceProvider.GetService<ScriptResolverMock>();
var resolvedService = resolver.ResolveService(nameof(ScriptResolverMock));
Assert.AreEqual(expectedResolvedService, resolvedService);
}
[Test]
public void Test_ResolveType_Interface()
{
var resolver = serviceProvider.GetService<ScriptPluginServiceResolver>();
var expectedResolvedService = serviceProvider.GetService<IScriptResolverMock>();
var resolvedService = resolver.ResolveService(nameof(IScriptResolverMock));
Assert.AreEqual(expectedResolvedService, resolvedService);
}
[Test]
public void Test_ResolveGenericType()
{
var resolver = serviceProvider.GetService<ScriptPluginServiceResolver>();
var expectedResolvedService = serviceProvider.GetService<ScriptResolverGenericMock<int, string>>();
var resolvedService = resolver.ResolveService("ScriptResolverGenericMock", new[] { "Int32", "String" });
Assert.AreEqual(expectedResolvedService, resolvedService);
}
[Test]
public void Test_ResolveGenericType_Interface()
{
var resolver = serviceProvider.GetService<ScriptPluginServiceResolver>();
var expectedResolvedService = serviceProvider.GetService<IScriptResolverGenericMock<int, string>>();
var resolvedService = resolver.ResolveService("IScriptResolverGenericMock", new[] { "Int32", "String" });
Assert.AreEqual(expectedResolvedService, resolvedService);
}
}
}

View File

@ -50,7 +50,7 @@
} }
</div> </div>
} }
@if (Model.ActivePenalty != null) @if (Model.ActivePenalty != null && (Model.ActivePenalty.Type != EFPenalty.PenaltyType.Flag || ViewBag.Authorized))
{ {
<div class="font-weight-bold h4 mb-0 penalties-color-@Model.ActivePenalty.Type.ToString().ToLower()"> <div class="font-weight-bold h4 mb-0 penalties-color-@Model.ActivePenalty.Type.ToString().ToLower()">
@foreach (var result in Utilities.SplitTranslationTokens(translationKey)) @foreach (var result in Utilities.SplitTranslationTokens(translationKey))

View File

@ -28,7 +28,7 @@
else if (match.MatchValue == "reason") else if (match.MatchValue == "reason")
{ {
<span class="text-white"> <span class="text-white">
@if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Warning) @if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Warning && Model.PenaltyType != SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Kick)
{ {
<span>@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense)</span> <span>@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense)</span>
<span class="oi oi-list-rich align-top text-primary automated-penalty-info-detailed" data-penalty-id="@Model.PenaltyId" style="margin-top: 0.125rem;" title="@ViewBag.Localization["WEBFRONT_CLIENT_META_AC_METRIC"]"></span> <span class="oi oi-list-rich align-top text-primary automated-penalty-info-detailed" data-penalty-id="@Model.PenaltyId" style="margin-top: 0.125rem;" title="@ViewBag.Localization["WEBFRONT_CLIENT_META_AC_METRIC"]"></span>

View File

@ -2,12 +2,7 @@ trigger:
batch: true batch: true
branches: branches:
include: include:
- releases/* - release/pre
- 2.4-pr
paths:
exclude:
- azure-pipelines.yml
- Master/*
pr: none pr: none