Compare commits

..

62 Commits

Author SHA1 Message Date
cade2242bf Merge pull request #87 from xerxes-at/2.3
Fixed the PT6 parser
2019-12-05 16:20:10 -06:00
1d7377f975 Fixed the PT6 parser
Reworked most of the regex.
Fixed the mapping.
This hopefully fixes all issues with it.
2019-12-04 20:08:01 +01:00
fb6d20e214 fix regex pattern for PT6 2019-11-18 12:29:37 -06:00
c630f65317 update the project files even though the culprit was actually the publish file 2019-10-08 16:47:36 -05:00
76cfe30c0f update version number 2019-10-07 17:39:17 -05:00
a7872aaffd ensure that demoted clients are logged out from the webfront 2019-10-07 17:35:37 -05:00
88af032736 Update shared GUIDs 2019-09-30 13:00:44 -05:00
3af9f55bf1 Fix ordering of admins by level, then name 2019-09-27 15:49:03 -05:00
1e9a87d6fa prevent penalties from being lost in edge case alias linkage
small optimization with tasks
2019-09-26 16:08:49 -05:00
fe6fe39800 don't group admin list by alias id 2019-09-14 17:22:47 -05:00
7f7353c505 only count hits for valid recoil detection 2019-09-10 17:24:32 -05:00
198f596ab3 small updates to stat handling
various little tweaks
2019-09-09 17:37:57 -05:00
58a73e581f fix rare issues when converting encodings
add readme for AC
2019-08-30 17:24:44 -05:00
47d5df1aa1 bump application version 2019-08-30 09:29:19 -05:00
d644387091 Strip out color codes and spaces when checking for min length 2019-08-30 09:26:16 -05:00
27a05ce6db update api controller to support actually filtering events by server
fix up stats manager async semaphore wait
add new shared guids
fix regex parsing with empty name
2019-08-28 13:45:53 -05:00
11d2df1fe8 small stat changes 2019-08-24 20:15:50 -05:00
a820929582 another fix because I'm retarded
bump version up
2019-08-24 14:06:23 -05:00
6726217354 Make stats update after 10 kills so we don't wait quite as long
Gracefully disconnect clients on shutting down again
2019-08-24 10:02:53 -05:00
563c73221e actually fix it here 2019-08-23 21:27:36 -05:00
652f3fb86b Fix small issue with saving client kills multithreaded 2019-08-23 19:11:36 -05:00
91078eec0f Update to some stat stuff to fix some latent issues 2019-08-23 18:34:31 -05:00
f6857ac635 bugfix for issue #81 (linked accounts being demoted) 2019-08-18 11:18:20 -05:00
001ecc5961 prevent flagging banned players 2019-08-12 20:00:40 -05:00
5fef69d697 slight tweak to log reader to expire old keys 2019-08-10 17:58:20 -05:00
2ba0b1e7d3 prevent same level clients from demoting each other 2019-08-08 15:58:23 -05:00
1c66ac9117 fix issue with log reader
fix issue with searching names on webfront that could be parsed to hex
2019-08-01 19:42:44 -05:00
034d887abd modify how reading from file works to prevent accidental overreads 2019-07-31 20:15:29 -05:00
06af995202 prevent certain shotguns, and shotgun attachments from being used for no recoil detection 2019-07-27 17:46:48 -05:00
f8505781a0 fix issue with teknomw3 GUIDs 2019-07-27 10:02:46 -05:00
f613f0aace finish tweaks to log reader
add some more shared guids
2019-07-26 10:22:02 -05:00
ac32034910 optimize index for rating history
update log server to prevent delays or missed information
2019-07-24 19:15:07 -05:00
2542b7de12 Clean up some old files 2019-07-17 12:29:51 -05:00
68f6be23a6 require minimum kills before recoil threshold evaluated
make sure edit configuration link on webfront visible when accessing via localhost
2019-07-07 17:57:06 -05:00
042327840f fix bug with wrong locale when master is down
fix issue with reapplying penalties
show subset of penalties that are linked on client profile
2019-06-28 16:57:01 -05:00
3d468e32b9 clean up some penalty stuff
force log file to be written if none supplied
fix issue with not all meta loading
2019-06-27 20:06:30 -05:00
16d2ec82b8 make sure flags are excluded from active penalties on player profile
modify how flags "expire"
2019-06-25 18:01:47 -05:00
421e90cf70 fix old bug of auto unflag not working
fix wrong thresholds on recoil
2019-06-24 18:32:14 -05:00
8119ff9f83 adjust detection thresholds for recoil and offset
make sure we don't keep adding penalties after first
add "Other" penalty for future plugin use
2019-06-24 16:56:47 -05:00
253c7c8721 allow reports to be filed against anyone
fix rare issue with alias (maybe)
update some tests
2019-06-24 11:01:34 -05:00
cb80def122 update version
make sure ac snapshots are saved
2019-06-16 14:49:04 -05:00
e669d0be82 don't count bots on master list
don't save every ac snapshot oops..
2019-06-16 12:19:23 -05:00
495197c19d add no recoil detection 2019-06-15 17:37:43 -05:00
a5414c2c57 Merge branch '2.3' of https://github.com/RaidMax/IW4M-Admin into 2.3 2019-06-15 08:53:15 -05:00
cbfb3919fc fix GUID parsing on T6 2019-06-15 08:52:59 -05:00
d789542d0f Update README.md 2019-06-14 18:48:14 -05:00
c6c2ec7784 fix start scripts on linux (dos2unix)
fix permissions on linux (why were/are they carrying over from windows? )
2019-06-14 18:16:47 -05:00
4645bd84e8 prevent partial client updates from setting things they shouldn't be *cough* mask *cough*
setup shared library for NuGet package
fix a couple things with offset detection calc
get cod4x working again
2019-06-13 19:10:08 -05:00
10829b32ad update anti-cheat offset calculation 2019-06-12 10:27:15 -05:00
e86904b11e add a check to make sure we're not breaking EFClient entries when updating
make sure the alias is updated before banning the player as we want to link them together
update CoD4x parser to fix their breaking change
2019-06-11 08:00:14 -05:00
82390340c9 fix duplicate meta data when restarting
fix issue with parsing anticheat info in non en-US culture
fix rare issue with client spots "swapping"
don't copy referenced shared library assemeblies from plugins
2019-06-09 09:50:58 -05:00
163523d586 convert GetPort to auto property
don't force disconnect player if someone is "in" their spot
increase gamelogserver max time before purge
2019-05-31 10:17:01 -05:00
95d64df321 combined Penalty and EFPenalty
moved some classes around
2019-05-29 16:55:35 -05:00
0b0290a871 fix issue with restarting via web
replace some hard coded string in javascript with localization
break things to fix things
2019-05-17 09:02:09 -05:00
5f588bb0f7 clean up the profanity determent plugin by using the Get/Set Additional properties
cleaned up the base event parser to not need the server object to generate the event
Hopefully prevent anticheat from freaking out when database connection is lost
2019-05-13 10:36:11 -05:00
b99cc424e7 fixes for things that should have been in the previous release
console output reenabled
server start task actually runs
2019-05-09 20:00:00 -05:00
1dc0f5a240 fix aggregate issue with KDR on global top stats
refactor some of the main application code to have a cleaner code flow
add enviroment flag to opt out of .net core telemetry in start script
fixed "a moment" missing the "ago"
fixed case sensitive client searches on postgresql
clean up command code flow
Add missing map "mp_cairo" to default settings
2019-05-08 20:34:17 -05:00
43c4d4af38 force bots to all use the same profile
use C# 7.1 for projects
2019-05-04 09:17:18 -05:00
db11a5f480 upgrade packages, and delete a few unneeded ones
fix search for client resulting in invalid GUID parse
simplify output from dvar not being found
make sure to prompt if not all servers could be reached
2019-05-03 20:13:51 -05:00
b51af7ca9a fix penalty list javascript loading duplicates
make bad GUID parse throw an exception so we don't have a client connect with GUID of 0
no longer print out ac debug messages
fix small issue of trying to parse empty chat messages
fix issue with set level on accounts with multi guid, same IP
2019-05-02 22:33:38 -05:00
2cceb2f3e7 make database seed code less verbose
disable killserver command
fix issue with default parser not saving during setup
fix issue with unban reason displayed when player is rebanned
2019-04-28 20:54:11 -05:00
599a14b646 optimize the find client query 2019-04-25 21:05:35 -05:00
146 changed files with 4902 additions and 2541 deletions

View File

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

View File

@ -13,5 +13,7 @@ namespace IW4MAdmin.Application.API.GameLogServer
public int Length { get; set; }
[JsonProperty("data")]
public string Data { get; set; }
[JsonProperty("next_key")]
public string NextKey { get; set; }
}
}

View File

@ -47,7 +47,7 @@ namespace IW4MAdmin.Application.API.Master
Map = s.CurrentMap.Name,
MaxClientNum = s.MaxClients,
Id = s.EndPoint,
Port = (short)s.GetPort(),
Port = (short)s.Port,
IPAddress = s.IP
}).ToList()
};

View File

@ -3,10 +3,10 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2.2.6.5</Version>
<Version>2.2.8.4</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Product>IW4MAdmin</Product>
@ -21,18 +21,21 @@
<Configurations>Debug;Release;Prerelease</Configurations>
<Win32Resource />
<RootNamespace>IW4MAdmin.Application</RootNamespace>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RestEase" Version="1.4.7" />
<PackageReference Include="RestEase" Version="1.4.9" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" />
</ItemGroup>
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation>
<AssemblyVersion>2.2.6.5</AssemblyVersion>
<FileVersion>2.2.6.5</FileVersion>
<AssemblyVersion>2.2.8.4</AssemblyVersion>
<FileVersion>2.2.8.4</FileVersion>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
@ -51,10 +54,6 @@
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.2.2" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="call $(ProjectDir)BuildScripts\PreBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" />
</Target>

View File

@ -12,28 +12,26 @@ using SharedLibraryCore.Events;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Services;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static SharedLibraryCore.GameEvent;
namespace IW4MAdmin.Application
{
public class ApplicationManager : IManager
{
private List<Server> _servers;
private ConcurrentBag<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
public Dictionary<int, EFClient> PrivilegedClients { get; set; }
public ILogger Logger => GetLogger(0);
public bool Running { get; private set; }
public bool IsInitialized { get; private set; }
// define what the delagate function looks like
public delegate void OnServerEventEventHandler(object sender, GameEventArgs e);
// expose the event handler so we can execute the events
public OnServerEventEventHandler OnServerEvent { get; set; }
public DateTime StartTime { get; private set; }
@ -41,12 +39,11 @@ namespace IW4MAdmin.Application
public IList<IRConParser> AdditionalRConParsers { get; }
public IList<IEventParser> AdditionalEventParsers { get; }
public ITokenAuthentication TokenAuthenticator => Authenticator;
public ITokenAuthentication Authenticator => _authenticator;
public ITokenAuthentication TokenAuthenticator { get; }
public CancellationToken CancellationToken => _tokenSource.Token;
public string ExternalIPAddress { get; private set; }
public bool IsRestartRequested { get; private set; }
static ApplicationManager Instance;
readonly List<AsyncStatus> TaskStatuses;
List<Command> Commands;
readonly List<MessageToken> MessageTokens;
ClientService ClientSvc;
@ -54,33 +51,30 @@ namespace IW4MAdmin.Application
readonly PenaltyService PenaltySvc;
public BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
GameEventHandler Handler;
ManualResetEventSlim OnQuit;
readonly IPageList PageList;
readonly SemaphoreSlim ProcessingEvent = new SemaphoreSlim(1, 1);
readonly Dictionary<long, ILogger> Loggers = new Dictionary<long, ILogger>();
readonly ITokenAuthentication _authenticator;
private readonly MetaService _metaService;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource;
private ApplicationManager()
{
_servers = new List<Server>();
_servers = new ConcurrentBag<Server>();
Commands = new List<Command>();
TaskStatuses = new List<AsyncStatus>();
MessageTokens = new List<MessageToken>();
ClientSvc = new ClientService();
AliasSvc = new AliasService();
PenaltySvc = new PenaltyService();
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
StartTime = DateTime.UtcNow;
OnQuit = new ManualResetEventSlim();
PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>();
AdditionalRConParsers = new List<IRConParser>();
OnServerEvent += OnGameEvent;
OnServerEvent += EventApi.OnGameEvent;
_authenticator = new TokenAuthentication();
TokenAuthenticator = new TokenAuthentication();
_metaService = new MetaService();
_tokenSource = new CancellationTokenSource();
}
private async void OnGameEvent(object sender, GameEventArgs args)
@ -158,12 +152,12 @@ namespace IW4MAdmin.Application
return Instance ?? (Instance = new ApplicationManager());
}
public async Task UpdateServerStates(CancellationToken token)
public async Task UpdateServerStates()
{
// store the server hash code and task for it
var runningUpdateTasks = new Dictionary<long, Task>();
while (Running)
while (!_tokenSource.IsCancellationRequested)
{
// select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks
@ -194,10 +188,11 @@ namespace IW4MAdmin.Application
{
try
{
await server.ProcessUpdatesAsync(token);
await server.ProcessUpdatesAsync(_tokenSource.Token);
if (server.Throttled)
{
await Task.Delay((int)_throttleTimeout.TotalMilliseconds);
await Task.Delay((int)_throttleTimeout.TotalMilliseconds, _tokenSource.Token);
}
}
@ -221,10 +216,17 @@ namespace IW4MAdmin.Application
#endif
try
{
await Task.Delay(ConfigHandler.Configuration().RConPollRate, token);
await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
}
// if a cancellation is received, we want to return immediately after shutting down
catch
{
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
{
await server.ProcessUpdatesAsync(_tokenSource.Token);
}
break;
}
// if a cancellation is received, we want to return immediately
catch { break; }
}
}
@ -345,8 +347,6 @@ namespace IW4MAdmin.Application
{
await new ContextSeed(db).Seed();
}
PrivilegedClients = (await ClientSvc.GetPrivilegedClients()).ToDictionary(_client => _client.ClientId);
#endregion
#region COMMANDS
@ -356,6 +356,7 @@ namespace IW4MAdmin.Application
}
Commands.Add(new CQuit());
Commands.Add(new CRestart());
Commands.Add(new CKick());
Commands.Add(new CSay());
Commands.Add(new CTempBan());
@ -387,7 +388,7 @@ namespace IW4MAdmin.Application
Commands.Add(new CIP());
Commands.Add(new CMask());
Commands.Add(new CPruneAdmins());
Commands.Add(new CKillServer());
//Commands.Add(new CKillServer());
Commands.Add(new CSetPassword());
Commands.Add(new CPing());
Commands.Add(new CSetGravatar());
@ -522,15 +523,18 @@ namespace IW4MAdmin.Application
MetaService.AddRuntimeMeta(getPenaltyMeta);
#endregion
#region INIT
int failedServers = 0;
await InitializeServers();
}
private async Task InitializeServers()
{
var config = ConfigHandler.Configuration();
int successServers = 0;
Exception lastException = null;
async Task Init(ServerConfiguration Conf)
{
// setup the event handler after the class is initialized
Handler = new GameEventHandler(this);
try
@ -538,10 +542,7 @@ namespace IW4MAdmin.Application
var ServerInstance = new IW4MServer(this, Conf);
await ServerInstance.Initialize();
lock (_servers)
{
_servers.Add(ServerInstance);
}
_servers.Add(ServerInstance);
Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname));
// add the start event for this server
@ -563,48 +564,46 @@ namespace IW4MAdmin.Application
if (e.GetType() == typeof(DvarException))
{
Logger.WriteDebug($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt((e as DvarException).Data["dvar_name"])} ({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})");
Logger.WriteDebug($"{e.Message} {(e.GetType() == typeof(DvarException) ? $"({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})" : "")}");
}
else if (e.GetType() == typeof(NetworkException))
{
Logger.WriteDebug(e.Message);
}
failedServers++;
lastException = e;
}
}
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
if (successServers - failedServers <= 0)
if (successServers == 0)
{
throw lastException;
}
if (successServers != config.Servers.Count)
{
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
{
throw lastException;
}
}
#endregion
}
private async Task SendHeartbeat(object state)
private async Task SendHeartbeat()
{
var heartbeatState = (HeartbeatState)state;
bool connected = false;
while (Running)
while (!_tokenSource.IsCancellationRequested)
{
if (!heartbeatState.Connected)
if (!connected)
{
try
{
await Heartbeat.Send(this, true);
heartbeatState.Connected = true;
connected = true;
}
catch (Exception e)
{
heartbeatState.Connected = false;
connected = false;
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
}
}
@ -630,7 +629,7 @@ namespace IW4MAdmin.Application
{
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
heartbeatState.Connected = false;
connected = false;
}
}
}
@ -640,9 +639,10 @@ namespace IW4MAdmin.Application
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
heartbeatState.Connected = false;
connected = false;
}
}
catch (Exception e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
@ -652,31 +652,32 @@ namespace IW4MAdmin.Application
try
{
await Task.Delay(30000, heartbeatState.Token);
await Task.Delay(30000, _tokenSource.Token);
}
catch { break; }
}
}
public void Start()
public async Task Start()
{
var tokenSource = new CancellationTokenSource();
// this needs to be run seperately from the main thread
_ = Task.Run(() => SendHeartbeat(new HeartbeatState() { Token = tokenSource.Token }));
_ = Task.Run(() => UpdateServerStates(tokenSource.Token));
while (Running)
await Task.WhenAll(new[]
{
OnQuit.Wait();
tokenSource.Cancel();
OnQuit.Reset();
}
SendHeartbeat(),
UpdateServerStates()
});
}
public void Stop()
{
_tokenSource.Cancel();
Running = false;
OnQuit.Set();
Instance = null;
}
public void Restart()
{
IsRestartRequested = true;
Stop();
}
public ILogger GetLogger(long serverId)
@ -734,26 +735,11 @@ namespace IW4MAdmin.Application
return ConfigHandler;
}
public IDictionary<int, EFClient> GetPrivilegedClients()
{
return PrivilegedClients;
}
public bool ShutdownRequested()
{
return !Running;
}
public IEventHandler GetEventHandler()
{
return Handler;
}
public void SetHasEvent()
{
}
public IList<Assembly> GetPluginAssemblies()
{
return SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies.Union(SharedLibraryCore.Plugins.PluginImporter.Assemblies).ToList();

View File

@ -108,8 +108,14 @@ if "%CurrentConfiguration%" == "Release" (
)
echo making start scripts
@(echo @echo off && echo @title IW4MAdmin && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.cmd"
@(echo @echo off && echo @title IW4MAdmin && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%SolutionDir%Publish\Windows\StartIW4MAdmin.cmd"
@(echo @echo off && echo @title IW4MAdmin && echo set DOTNET_CLI_TELEMETRY_OPTOUT=1 && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.cmd"
@(echo @echo off && echo @title IW4MAdmin && echo set DOTNET_CLI_TELEMETRY_OPTOUT=1 && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%SolutionDir%Publish\Windows\StartIW4MAdmin.cmd"
@(echo #!/bin/bash && echo dotnet Lib/IW4MAdmin.dll) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.sh"
@(echo #!/bin/bash && echo dotnet Lib/IW4MAdmin.dll) > "%SolutionDir%Publish\Windows\StartIW4MAdmin.sh"
@(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.sh"
dos2unix "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.sh"
@(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%SolutionDir%Publish\Windows\StartIW4MAdmin.sh"
dos2unix "%SolutionDir%Publish\Windows\StartIW4MAdmin.sh"
echo setting permissions...
cacls "%SolutionDir%Publish\WindowsPrerelease" /t /e /p Everyone:F
cacls "%SolutionDir%Publish\Windows" /t /e /p Everyone:F

View File

@ -16,7 +16,7 @@
"Keep grenade launcher use to a minimum",
"Balance teams at ALL times"
],
"DisallowedClientNames": ["Unknown Soldier", "VickNet", "UnknownSoldier", "CHEATER"],
"DisallowedClientNames": [ "Unknown Soldier", "VickNet", "UnknownSoldier", "CHEATER", "Play77" ],
"QuickMessages": [
{
"Game": "IW4",
@ -517,6 +517,10 @@
"Alias": "Hanoi",
"Name": "mp_hanoi"
},
{
"Alias": "Havana",
"Name": "mp_cairo"
},
{
"Alias": "Hazard",
"Name": "mp_golfcourse"

View File

@ -36,7 +36,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world);(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world);(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world)?;(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world)?;(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -51,7 +51,7 @@ namespace IW4MAdmin.Application.EventParsers
Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world);(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world);(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world)?;(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world)?;(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
@ -75,26 +75,12 @@ namespace IW4MAdmin.Application.EventParsers
public string URLProtocolFormat { get; set; } = "CoD://{{ip}}:{{port}}";
public virtual GameEvent GetEvent(Server server, string logLine)
public virtual GameEvent GenerateGameEvent(string logLine)
{
logLine = Regex.Replace(logLine, @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim();
string[] lineSplit = logLine.Split(';');
string eventType = lineSplit[0];
if (eventType == "JoinTeam")
{
var origin = server.GetClientsAsList()
.FirstOrDefault(c => c.NetworkId == lineSplit[1].ConvertLong());
return new GameEvent()
{
Type = GameEvent.EventType.JoinTeam,
Data = logLine,
Origin = origin,
Owner = server
};
}
if (eventType == "say" || eventType == "sayteam")
{
var matchResult = Regex.Match(logLine, Configuration.Say.Pattern);
@ -107,160 +93,95 @@ namespace IW4MAdmin.Application.EventParsers
.Replace("\x15", "")
.Trim();
var origin = server.GetClientsAsList()
.First(c => c.NetworkId == matchResult.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong());
if (message[0] == '!' || message[0] == '@')
if (message.Length > 0)
{
long originId = matchResult.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong();
if (message[0] == '!' || message[0] == '@')
{
return new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = new EFClient() { NetworkId = originId },
Message = message,
RequiredEntity = GameEvent.EventRequiredEntity.Origin
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Command,
Type = GameEvent.EventType.Say,
Data = message,
Origin = origin,
Owner = server,
Message = message
Origin = new EFClient() { NetworkId = originId },
Message = message,
RequiredEntity = GameEvent.EventRequiredEntity.Origin
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = origin,
Owner = server,
Message = message
};
}
}
if (eventType == "K")
{
if (!server.CustomCallback)
var match = Regex.Match(logLine, Configuration.Kill.Pattern);
if (match.Success)
{
var match = Regex.Match(logLine, Configuration.Kill.Pattern);
long originId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].Value.ToString().ConvertGuidToLong(1);
long targetId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].Value.ToString().ConvertGuidToLong(1);
if (match.Success)
return new GameEvent()
{
string originId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].Value.ToString();
string targetId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].Value.ToString();
var origin = !string.IsNullOrEmpty(originId) ? server.GetClientsAsList()
.First(c => c.NetworkId == originId.ConvertLong()) :
Utilities.IW4MAdminClient(server);
var target = !string.IsNullOrEmpty(targetId) ? server.GetClientsAsList()
.First(c => c.NetworkId == targetId.ConvertLong()) :
Utilities.IW4MAdminClient(server);
return new GameEvent()
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = origin,
Target = target,
Owner = server
};
}
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
}
if (eventType == "ScriptKill")
{
long originId = lineSplit[1].ConvertLong();
long targetId = lineSplit[2].ConvertLong();
var origin = originId == long.MinValue ? Utilities.IW4MAdminClient(server) :
server.GetClientsAsList().First(c => c.NetworkId == originId);
var target = targetId == long.MinValue ? Utilities.IW4MAdminClient(server) :
server.GetClientsAsList().FirstOrDefault(c => c.NetworkId == targetId) ?? Utilities.IW4MAdminClient(server);
return new GameEvent()
{
Type = GameEvent.EventType.ScriptKill,
Data = logLine,
Origin = origin,
Target = target,
Owner = server
};
}
if (eventType == "ScriptDamage")
{
long originId = lineSplit[1].ConvertLong();
long targetId = lineSplit[2].ConvertLong();
var origin = originId == long.MinValue ? Utilities.IW4MAdminClient(server) :
server.GetClientsAsList().First(c => c.NetworkId == originId);
var target = targetId == long.MinValue ? Utilities.IW4MAdminClient(server) :
server.GetClientsAsList().FirstOrDefault(c => c.NetworkId == targetId) ?? Utilities.IW4MAdminClient(server);
return new GameEvent()
{
Type = GameEvent.EventType.ScriptDamage,
Data = logLine,
Origin = origin,
Target = target,
Owner = server
};
}
// damage
if (eventType == "D")
{
if (!server.CustomCallback)
var regexMatch = Regex.Match(logLine, Configuration.Damage.Pattern);
if (regexMatch.Success)
{
var regexMatch = Regex.Match(logLine, Configuration.Damage.Pattern);
long originId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(1);
long targetId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertGuidToLong(1);
if (regexMatch.Success)
return new GameEvent()
{
string originId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString();
string targetId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString();
var origin = !string.IsNullOrEmpty(originId) ? server.GetClientsAsList()
.First(c => c.NetworkId == originId.ConvertLong()) :
Utilities.IW4MAdminClient(server);
var target = !string.IsNullOrEmpty(targetId) ? server.GetClientsAsList()
.First(c => c.NetworkId == targetId.ConvertLong()) :
Utilities.IW4MAdminClient(server);
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = logLine,
Origin = origin,
Target = target,
Owner = server
};
}
Type = GameEvent.EventType.Damage,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
}
// join
if (eventType == "J")
{
var regexMatch = Regex.Match(logLine, Configuration.Join.Pattern);
if (regexMatch.Success)
{
bool isBot = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().Contains("bot");
return new GameEvent()
{
Type = GameEvent.EventType.PreConnect,
Data = logLine,
Owner = server,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().StripColors(),
},
NetworkId = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong(),
NetworkId = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(),
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting,
CurrentServer = server,
IsBot = isBot
}
},
RequiredEntity = GameEvent.EventRequiredEntity.None
};
}
}
@ -274,17 +195,17 @@ namespace IW4MAdmin.Application.EventParsers
{
Type = GameEvent.EventType.PreDisconnect,
Data = logLine,
Owner = server,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().StripColors()
},
NetworkId = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong(),
NetworkId = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(),
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Disconnecting
}
},
RequiredEntity = GameEvent.EventRequiredEntity.Origin
};
}
}
@ -295,9 +216,9 @@ namespace IW4MAdmin.Application.EventParsers
{
Type = GameEvent.EventType.MapEnd,
Data = lineSplit[0],
Origin = Utilities.IW4MAdminClient(server),
Target = Utilities.IW4MAdminClient(server),
Owner = server
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None
};
}
@ -309,19 +230,63 @@ namespace IW4MAdmin.Application.EventParsers
{
Type = GameEvent.EventType.MapChange,
Data = lineSplit[0],
Origin = Utilities.IW4MAdminClient(server),
Target = Utilities.IW4MAdminClient(server),
Owner = server,
Extra = dump.DictionaryFromKeyValue()
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
Extra = dump.DictionaryFromKeyValue(),
RequiredEntity = GameEvent.EventRequiredEntity.None
};
}
// this is a custom event printed out by _customcallbacks.gsc (used for team balance)
if (eventType == "JoinTeam")
{
return new GameEvent()
{
Type = GameEvent.EventType.JoinTeam,
Data = logLine,
Origin = new EFClient() { NetworkId = lineSplit[1].ConvertGuidToLong() },
RequiredEntity = GameEvent.EventRequiredEntity.Target
};
}
// this is a custom event printed out by _customcallbacks.gsc (used for anticheat)
if (eventType == "ScriptKill")
{
long originId = lineSplit[1].ConvertGuidToLong(1);
long targetId = lineSplit[2].ConvertGuidToLong(1);
return new GameEvent()
{
Type = GameEvent.EventType.ScriptKill,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
// this is a custom event printed out by _customcallbacks.gsc (used for anticheat)
if (eventType == "ScriptDamage")
{
long originId = lineSplit[1].ConvertGuidToLong(1);
long targetId = lineSplit[2].ConvertGuidToLong(1);
return new GameEvent()
{
Type = GameEvent.EventType.ScriptDamage,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Origin = Utilities.IW4MAdminClient(server),
Target = Utilities.IW4MAdminClient(server),
Owner = server
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None
};
}
}

View File

@ -1,23 +1,26 @@
using SharedLibraryCore;
using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace IW4MAdmin.Application
{
class GameEventHandler : IEventHandler
{
readonly IManager Manager;
readonly ApplicationManager Manager;
public GameEventHandler(IManager mgr)
{
Manager = mgr;
Manager = (ApplicationManager)mgr;
}
public void AddEvent(GameEvent gameEvent)
{
((Manager as ApplicationManager).OnServerEvent)(this, new GameEventArgs(null, false, gameEvent));
#if DEBUG
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
gameEvent.Owner.Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
Manager.OnServerEvent?.Invoke(gameEvent.Owner, new GameEventArgs(null, false, gameEvent));
}
}
}

View File

@ -8,10 +8,10 @@ namespace IW4MAdmin.Application.IO
{
class GameLogEventDetection
{
Server Server;
long PreviousFileSize;
IGameLogReader Reader;
readonly string GameLogFile;
private long previousFileSize;
private readonly Server _server;
private readonly IGameLogReader _reader;
private readonly string _gameLogFile;
class EventState
{
@ -21,16 +21,18 @@ namespace IW4MAdmin.Application.IO
public GameLogEventDetection(Server server, string gameLogPath, Uri gameLogServerUri)
{
GameLogFile = gameLogPath;
Reader = gameLogServerUri != null ? new GameLogReaderHttp(gameLogServerUri, gameLogPath, server.EventParser) : Reader = new GameLogReader(gameLogPath, server.EventParser);
Server = server;
_gameLogFile = gameLogPath;
_reader = gameLogServerUri != null ? new GameLogReaderHttp(gameLogServerUri, gameLogPath, server.EventParser) : _reader = new GameLogReader(gameLogPath, server.EventParser);
_server = server;
}
public async Task PollForChanges()
{
while (!Server.Manager.ShutdownRequested())
while (!_server.Manager.CancellationToken.IsCancellationRequested)
{
if (Server.IsInitialized)
#if !DEBUG
if (_server.IsInitialized)
#endif
{
try
{
@ -39,39 +41,40 @@ namespace IW4MAdmin.Application.IO
catch (Exception e)
{
Server.Logger.WriteWarning($"Failed to update log event for {Server.EndPoint}");
Server.Logger.WriteDebug($"Exception: {e.Message}");
Server.Logger.WriteDebug($"StackTrace: {e.StackTrace}");
_server.Logger.WriteWarning($"Failed to update log event for {_server.EndPoint}");
_server.Logger.WriteDebug(e.GetExceptionInfo());
}
}
Thread.Sleep(Reader.UpdateInterval);
await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken);
}
_server.Logger.WriteDebug("Stopped polling for changes");
}
private async Task UpdateLogEvents()
{
long fileSize = Reader.Length;
long fileSize = _reader.Length;
if (PreviousFileSize == 0)
PreviousFileSize = fileSize;
if (previousFileSize == 0)
{
previousFileSize = fileSize;
}
long fileDiff = fileSize - PreviousFileSize;
long fileDiff = fileSize - previousFileSize;
// this makes the http log get pulled
if (fileDiff < 1 && fileSize != -1)
return;
PreviousFileSize = fileSize;
var events = await Reader.ReadEventsFromLog(Server, fileDiff, 0);
var events = await _reader.ReadEventsFromLog(_server, fileDiff, previousFileSize);
foreach (var ev in events)
{
Server.Manager.GetEventHandler().AddEvent(ev);
await ev.WaitAsync();
_server.Manager.GetEventHandler().AddEvent(ev);
}
PreviousFileSize = fileSize;
previousFileSize = fileSize;
}
}
}

View File

@ -3,6 +3,7 @@ using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -12,6 +13,7 @@ namespace IW4MAdmin.Application.IO
{
IEventParser Parser;
readonly string LogFile;
private bool? ignoreBots;
public long Length => new FileInfo(LogFile).Length;
@ -25,20 +27,40 @@ namespace IW4MAdmin.Application.IO
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
{
if (!ignoreBots.HasValue)
{
ignoreBots = server.Manager.GetApplicationSettings().Configuration().IgnoreBots;
}
// allocate the bytes for the new log lines
List<string> logLines = new List<string>();
// open the file as a stream
using (var rd = new StreamReader(new FileStream(LogFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), Utilities.EncodingType))
using (FileStream fs = new FileStream(LogFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
// todo: max async
// take the old start position and go back the number of new characters
rd.BaseStream.Seek(-fileSizeDiff, SeekOrigin.End);
string newLine;
while (!string.IsNullOrEmpty(newLine = await rd.ReadLineAsync()))
byte[] buff = new byte[fileSizeDiff];
fs.Seek(startPosition, SeekOrigin.Begin);
await fs.ReadAsync(buff, 0, (int)fileSizeDiff, server.Manager.CancellationToken);
var stringBuilder = new StringBuilder();
char[] charBuff = Utilities.EncodingType.GetChars(buff);
foreach (char c in charBuff)
{
logLines.Add(newLine);
if (c == '\n')
{
logLines.Add(stringBuilder.ToString());
stringBuilder = new StringBuilder();
}
else if (c != '\r')
{
stringBuilder.Append(c);
}
}
if (stringBuilder.Length > 0)
{
logLines.Add(stringBuilder.ToString());
}
}
@ -51,10 +73,45 @@ namespace IW4MAdmin.Application.IO
{
try
{
// todo: catch elsewhere
events.Add(Parser.GetEvent(server, eventLine));
var gameEvent = Parser.GenerateGameEvent(eventLine);
// we don't want to add the event if ignoreBots is on and the event comes from a bot
if (!ignoreBots.Value || (ignoreBots.Value && !((gameEvent.Origin?.IsBot ?? false) || (gameEvent.Target?.IsBot ?? false))))
{
gameEvent.Owner = server;
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != 1)
{
gameEvent.Origin = server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId);
}
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target)
{
gameEvent.Target = server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Target?.NetworkId);
}
if (gameEvent.Origin != null)
{
gameEvent.Origin.CurrentServer = server;
}
if (gameEvent.Target != null)
{
gameEvent.Target.CurrentServer = server;
}
events.Add(gameEvent);
}
}
catch (InvalidOperationException)
{
if (!ignoreBots.Value)
{
server.Logger.WriteWarning("Could not find client in client list when parsing event line");
server.Logger.WriteDebug(eventLine);
}
}
catch (Exception e)
{
server.Logger.WriteWarning("Could not properly parse event line");

View File

@ -4,6 +4,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using static SharedLibraryCore.Utilities;
@ -18,6 +19,8 @@ namespace IW4MAdmin.Application.IO
readonly IEventParser Parser;
readonly IGameLogServer Api;
readonly string logPath;
private bool? ignoreBots;
private string lastKey = "next";
public GameLogReaderHttp(Uri gameLogServerUri, string logPath, IEventParser parser)
{
@ -28,42 +31,86 @@ namespace IW4MAdmin.Application.IO
public long Length => -1;
public int UpdateInterval => 350;
public int UpdateInterval => 500;
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
{
#if DEBUG == true
server.Logger.WriteDebug($"Begin reading from http log");
#endif
if (!ignoreBots.HasValue)
{
ignoreBots = server.Manager.GetApplicationSettings().Configuration().IgnoreBots;
}
var events = new List<GameEvent>();
string b64Path = logPath;
var response = await Api.Log(b64Path);
var response = await Api.Log(b64Path, lastKey);
lastKey = response.NextKey;
if (!response.Success)
if (!response.Success && string.IsNullOrEmpty(lastKey))
{
server.Logger.WriteError($"Could not get log server info of {logPath}/{b64Path} ({server.LogPath})");
return events;
}
// parse each line
foreach (string eventLine in response.Data.Split(Environment.NewLine))
else if (!string.IsNullOrWhiteSpace(response.Data))
{
if (eventLine.Length > 0)
{
try
{
var e = Parser.GetEvent(server, eventLine);
#if DEBUG == true
server.Logger.WriteDebug($"Parsed event with id {e.Id} from http");
#if DEBUG
server.Manager.GetLogger(0).WriteInfo(response.Data);
#endif
events.Add(e);
}
catch (Exception e)
// parse each line
foreach (string eventLine in response.Data.Split(Environment.NewLine))
{
if (eventLine.Length > 0)
{
server.Logger.WriteWarning("Could not properly parse event line");
server.Logger.WriteDebug(e.Message);
server.Logger.WriteDebug(eventLine);
try
{
var gameEvent = Parser.GenerateGameEvent(eventLine);
// we don't want to add the event if ignoreBots is on and the event comes from a bot
if (!ignoreBots.Value || (ignoreBots.Value && !((gameEvent.Origin?.IsBot ?? false) || (gameEvent.Target?.IsBot ?? false))))
{
gameEvent.Owner = server;
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != 1)
{
gameEvent.Origin = server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId);
}
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target)
{
gameEvent.Target = server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Target?.NetworkId);
}
if (gameEvent.Origin != null)
{
gameEvent.Origin.CurrentServer = server;
}
if (gameEvent.Target != null)
{
gameEvent.Target.CurrentServer = server;
}
events.Add(gameEvent);
}
#if DEBUG == true
server.Logger.WriteDebug($"Parsed event with id {gameEvent.Id} from http");
#endif
}
catch (InvalidOperationException)
{
if (!ignoreBots.Value)
{
server.Logger.WriteWarning("Could not find client in client list when parsing event line");
server.Logger.WriteDebug(eventLine);
}
}
catch (Exception e)
{
server.Logger.WriteWarning("Could not properly parse remote event line");
server.Logger.WriteDebug(e.Message);
server.Logger.WriteDebug(eventLine);
}
}
}
}

View File

@ -6,9 +6,9 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Localization;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Services;
using System;
using System.Collections.Generic;
@ -26,7 +26,6 @@ namespace IW4MAdmin
{
private static readonly Index loc = Utilities.CurrentLocalization.LocalizationIndex;
private GameLogEventDetection LogEvent;
private DateTime SessionStart;
public int Id { get; private set; }
@ -38,55 +37,45 @@ namespace IW4MAdmin
{
Logger.WriteDebug($"Client slot #{clientFromLog.ClientNumber} now reserved");
try
EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId);
// first time client is connecting to server
if (client == null)
{
EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId);
Logger.WriteDebug($"Client {clientFromLog} first time connecting");
clientFromLog.CurrentServer = this;
client = await Manager.GetClientService().Create(clientFromLog);
}
// first time client is connecting to server
if (client == null)
{
Logger.WriteDebug($"Client {clientFromLog} first time connecting");
clientFromLog.CurrentServer = this;
client = await Manager.GetClientService().Create(clientFromLog);
}
/// this is only a temporary version until the IPAddress is transmitted
client.CurrentAlias = new EFAlias()
{
Name = clientFromLog.Name,
IPAddress = clientFromLog.IPAddress
};
/// this is only a temporary version until the IPAddress is transmitted
client.CurrentAlias = new EFAlias()
{
Name = clientFromLog.Name,
IPAddress = clientFromLog.IPAddress
};
Logger.WriteInfo($"Client {client} connected...");
Logger.WriteInfo($"Client {client} connected...");
// Do the player specific stuff
client.ClientNumber = clientFromLog.ClientNumber;
client.Score = clientFromLog.Score;
client.Ping = clientFromLog.Ping;
client.CurrentServer = this;
// Do the player specific stuff
client.ClientNumber = clientFromLog.ClientNumber;
client.IsBot = clientFromLog.IsBot;
client.Score = clientFromLog.Score;
client.Ping = clientFromLog.Ping;
client.CurrentServer = this;
Clients[client.ClientNumber] = client;
Clients[client.ClientNumber] = client;
#if DEBUG == true
Logger.WriteDebug($"End PreConnect for {client}");
Logger.WriteDebug($"End PreConnect for {client}");
#endif
var e = new GameEvent()
{
Origin = client,
Owner = this,
Type = GameEvent.EventType.Connect
};
Manager.GetEventHandler().AddEvent(e);
await client.OnJoin(client.IPAddress);
client.State = ClientState.Connected;
}
catch (Exception ex)
var e = new GameEvent()
{
Logger.WriteError($"{loc["SERVER_ERROR_ADDPLAYER"]} {clientFromLog}");
Logger.WriteError(ex.GetExceptionInfo());
}
Origin = client,
Owner = this,
Type = GameEvent.EventType.Connect
};
await client.OnJoin(client.IPAddress);
client.State = ClientState.Connected;
Manager.GetEventHandler().AddEvent(e);
}
override public async Task OnClientDisconnected(EFClient client)
@ -95,18 +84,18 @@ namespace IW4MAdmin
if (client.ClientNumber >= 0)
{
#endif
Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting...");
Clients[client.ClientNumber] = null;
await client.OnDisconnect();
Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting...");
Clients[client.ClientNumber] = null;
await client.OnDisconnect();
var e = new GameEvent()
{
Origin = client,
Owner = this,
Type = GameEvent.EventType.Disconnect
};
var e = new GameEvent()
{
Origin = client,
Owner = this,
Type = GameEvent.EventType.Disconnect
};
Manager.GetEventHandler().AddEvent(e);
Manager.GetEventHandler().AddEvent(e);
#if DEBUG == true
}
#endif
@ -175,6 +164,10 @@ namespace IW4MAdmin
/// <returns></returns>
override protected async Task<bool> ProcessEvent(GameEvent E)
{
#if DEBUG
Logger.WriteDebug($"processing event of type {E.Type}");
#endif
if (E.Type == GameEvent.EventType.ConnectionLost)
{
var exception = E.Extra as Exception;
@ -200,26 +193,6 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.ChangePermission)
{
var newPermission = (Permission)E.Extra;
if (newPermission < Permission.Moderator)
{
// remove banned or demoted privileged user
Manager.GetPrivilegedClients().Remove(E.Target.ClientId);
}
else
{
if (Manager.GetPrivilegedClients().ContainsKey(E.Target.ClientId))
{
Manager.GetPrivilegedClients()[E.Target.ClientId] = E.Target;
}
else
{
Manager.GetPrivilegedClients().Add(E.Target.ClientId, E.Target);
}
}
Logger.WriteInfo($"{E.Origin} is setting {E.Target} to permission level {newPermission}");
await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin);
}
@ -235,13 +208,21 @@ namespace IW4MAdmin
var existingClient = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
// they're already connected
if (existingClient != null)
if (existingClient != null && existingClient.ClientNumber == E.Origin.ClientNumber && !E.Origin.IsBot)
{
Logger.WriteWarning($"detected preconnect for {E.Origin}, but they are already connected");
return false;
}
CONNECT:
// this happens for some reason rarely where the client spots get out of order
// possible a connect/reconnect game event before we get to process it here
else if (existingClient != null && existingClient.ClientNumber != E.Origin.ClientNumber)
{
Logger.WriteWarning($"client {E.Origin} is trying to connect in client slot {E.Origin.ClientNumber}, but they are already registed in client slot {existingClient.ClientNumber}, swapping...");
// we need to remove them so the client spots can swap
await OnClientDisconnected(Clients[existingClient.ClientNumber]);
}
if (Clients[E.Origin.ClientNumber] == null)
{
#if DEBUG == true
@ -249,7 +230,19 @@ namespace IW4MAdmin
#endif
// we can go ahead and put them in so that they don't get re added
Clients[E.Origin.ClientNumber] = E.Origin;
await OnClientConnected(E.Origin);
try
{
await OnClientConnected(E.Origin);
}
catch (Exception ex)
{
Logger.WriteError($"{loc["SERVER_ERROR_ADDPLAYER"]} {E.Origin}");
Logger.WriteDebug(ex.GetExceptionInfo());
Clients[E.Origin.ClientNumber] = null;
return false;
}
ChatHistory.Add(new ChatInfo()
{
@ -267,19 +260,24 @@ namespace IW4MAdmin
// for some reason there's still a client in the spot
else
{
Logger.WriteWarning($"{E.Origin} is connecteding but {Clients[E.Origin.ClientNumber]} is currently in that client slot");
await OnClientDisconnected(Clients[E.Origin.ClientNumber]);
goto CONNECT;
Logger.WriteWarning($"{E.Origin} is connecting but {Clients[E.Origin.ClientNumber]} is currently in that client slot");
}
}
else if (E.Type == GameEvent.EventType.Flag)
{
// todo: maybe move this to a seperate function
Penalty newPenalty = new Penalty()
DateTime? expires = null;
if (E.Extra is TimeSpan ts)
{
Type = Penalty.PenaltyType.Flag,
Expires = DateTime.UtcNow,
expires = DateTime.UtcNow + ts;
}
// todo: maybe move this to a seperate function
var newPenalty = new EFPenalty()
{
Type = EFPenalty.PenaltyType.Flag,
Expires = expires,
Offender = E.Target,
Offense = E.Data,
Punisher = E.Origin,
@ -293,9 +291,9 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.Unflag)
{
var unflagPenalty = new Penalty()
var unflagPenalty = new EFPenalty()
{
Type = Penalty.PenaltyType.Unflag,
Type = EFPenalty.PenaltyType.Unflag,
Expires = DateTime.UtcNow,
Offender = E.Target,
Offense = E.Data,
@ -304,8 +302,9 @@ namespace IW4MAdmin
Link = E.Target.AliasLink
};
await Manager.GetPenaltyService().Create(unflagPenalty);
E.Target.SetLevel(Permission.User, E.Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId);
await Manager.GetPenaltyService().Create(unflagPenalty);
}
else if (E.Type == GameEvent.EventType.Report)
@ -316,6 +315,20 @@ namespace IW4MAdmin
Target = E.Target,
Reason = E.Data
});
var newReport = new EFPenalty()
{
Type = EFPenalty.PenaltyType.Report,
Expires = DateTime.UtcNow,
Offender = E.Target,
Offense = E.Message,
Punisher = E.Origin,
Active = true,
When = DateTime.UtcNow,
Link = E.Target.AliasLink
};
await Manager.GetPenaltyService().Create(newReport);
}
else if (E.Type == GameEvent.EventType.TempBan)
@ -461,7 +474,6 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.MapEnd)
{
Logger.WriteInfo("Game ending...");
SessionStart = DateTime.UtcNow;
}
if (E.Type == GameEvent.EventType.Tell)
@ -528,8 +540,9 @@ namespace IW4MAdmin
/// <summary>
/// lists the connecting and disconnecting clients via RCon response
/// array index 0 = connecting clients
/// array index 0 = connecting clients
/// array index 1 = disconnecting clients
/// array index 2 = updated clients
/// </summary>
/// <returns></returns>
async Task<IList<EFClient>[]> PollPlayersAsync()
@ -559,6 +572,30 @@ namespace IW4MAdmin
};
}
private async Task ShutdownInternal()
{
foreach (var client in GetClientsAsList())
{
await client.OnDisconnect();
var e = new GameEvent()
{
Type = GameEvent.EventType.Disconnect,
Owner = this,
Origin = client
};
Manager.GetEventHandler().AddEvent(e);
await e.WaitAsync(Utilities.DefaultCommandTimeout, new CancellationTokenRegistration().Token);
}
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
{
await plugin.OnUnloadAsync();
}
}
DateTime start = DateTime.Now;
DateTime playerCountStart = DateTime.Now;
DateTime lastCount = DateTime.Now;
@ -566,34 +603,22 @@ namespace IW4MAdmin
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
{
try
{
#region SHUTDOWN
if (Manager.ShutdownRequested())
{
if (cts.IsCancellationRequested)
{
foreach (var client in GetClientsAsList())
{
var e = new GameEvent()
{
Type = GameEvent.EventType.PreDisconnect,
Origin = client,
Owner = this,
};
Manager.GetEventHandler().AddEvent(e);
await e.WaitAsync();
}
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
{
await plugin.OnUnloadAsync();
}
await ShutdownInternal();
return true;
}
#endregion
try
{
#if DEBUG
if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue)
{
return true;
}
#endif
var polledClients = await PollPlayersAsync();
var waiterList = new List<GameEvent>();
@ -616,15 +641,19 @@ namespace IW4MAdmin
// because we don't want to try to fill up a slot that's not empty yet
waiterList.Add(e);
}
// wait for all the disconnect tasks to finish
await Task.WhenAll(waiterList.Select(e => e.WaitAsync(10 * 1000)));
foreach (var waiter in waiterList)
{
waiter.Wait();
}
waiterList.Clear();
// this are our new connecting clients
foreach (var client in polledClients[0])
{
// note: this prevents players in ZMBI state from being registered with no name
if (string.IsNullOrEmpty(client.Name))
if (string.IsNullOrEmpty(client.Name) || (client.Ping == 999 && !client.IsBot))
{
continue;
}
@ -641,7 +670,10 @@ namespace IW4MAdmin
}
// wait for all the connect tasks to finish
await Task.WhenAll(waiterList.Select(e => e.WaitAsync(10 * 1000)));
foreach (var waiter in waiterList)
{
waiter.Wait();
}
waiterList.Clear();
// these are the clients that have updated
@ -658,7 +690,10 @@ namespace IW4MAdmin
waiterList.Add(e);
}
await Task.WhenAll(waiterList.Select(e => e.WaitAsync(10 * 1000)));
foreach (var waiter in waiterList)
{
waiter.Wait();
}
if (ConnectionErrors > 0)
{
@ -701,14 +736,14 @@ namespace IW4MAdmin
lastCount = DateTime.Now;
// update the player history
if ((lastCount - playerCountStart).TotalMinutes >= SharedLibraryCore.Helpers.PlayerHistory.UpdateInterval)
if ((lastCount - playerCountStart).TotalMinutes >= PlayerHistory.UpdateInterval)
{
while (ClientHistory.Count > ((60 / SharedLibraryCore.Helpers.PlayerHistory.UpdateInterval) * 12)) // 12 times a hour for 12 hours
while (ClientHistory.Count > ((60 / PlayerHistory.UpdateInterval) * 12)) // 12 times a hour for 12 hours
{
ClientHistory.Dequeue();
}
ClientHistory.Enqueue(new SharedLibraryCore.Helpers.PlayerHistory(ClientNum));
ClientHistory.Enqueue(new PlayerHistory(ClientNum));
playerCountStart = DateTime.Now;
}
@ -731,6 +766,12 @@ namespace IW4MAdmin
return true;
}
catch (TaskCanceledException)
{
await ShutdownInternal();
return true;
}
// this one is ok
catch (ServerException e)
{
@ -775,7 +816,8 @@ namespace IW4MAdmin
if (version?.Value?.Length != 0)
{
RconParser = Manager.AdditionalRConParsers.FirstOrDefault(_parser => _parser.Version == version.Value) ?? RconParser;
var matchedRconParser = Manager.AdditionalRConParsers.FirstOrDefault(_parser => _parser.Version == version.Value);
RconParser.Configuration = matchedRconParser != null ? matchedRconParser.Configuration : RconParser.Configuration;
EventParser = Manager.AdditionalEventParsers.FirstOrDefault(_parser => _parser.Version == version.Value) ?? EventParser;
Version = RconParser.Version;
}
@ -826,15 +868,31 @@ namespace IW4MAdmin
this.Gametype = gametype;
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
if ((logsync.Value == 0 || logfile.Value == string.Empty) && RconParser.CanGenerateLogPath)
if (RconParser.CanGenerateLogPath)
{
bool needsRestart = false;
if (logsync.Value == 0)
{
await this.SetDvarAsync("g_logsync", 2); // set to 2 for continous in other games, clamps to 1 for IW4
needsRestart = true;
}
if (string.IsNullOrWhiteSpace(logfile.Value))
{
logfile.Value = "games_mp.log";
await this.SetDvarAsync("g_log", logfile.Value);
needsRestart = true;
}
if (needsRestart)
{
Logger.WriteWarning("Game log file not properly initialized, restarting map...");
await this.ExecuteCommandAsync("map_restart");
}
// this DVAR isn't set until the a map is loaded
await this.SetDvarAsync("logfile", 2);
await this.SetDvarAsync("g_logsync", 2); // set to 2 for continous in other games, clamps to 1 for IW4
//await this.SetDvarAsync("g_log", "games_mp.log");
Logger.WriteWarning("Game log file not properly initialized, restarting map...");
await this.ExecuteCommandAsync("map_restart");
logfile = await this.GetDvarAsync<string>("g_log");
}
CustomCallback = await ScriptLoaded();
@ -902,9 +960,9 @@ namespace IW4MAdmin
Target.CurrentServer.Broadcast(message);
}
Penalty newPenalty = new Penalty()
var newPenalty = new EFPenalty()
{
Type = Penalty.PenaltyType.Warning,
Type = EFPenalty.PenaltyType.Warning,
Expires = DateTime.UtcNow,
Offender = Target,
Punisher = Origin,
@ -941,9 +999,9 @@ namespace IW4MAdmin
await Target.CurrentServer.OnClientDisconnected(Target);
#endif
var newPenalty = new Penalty()
var newPenalty = new EFPenalty()
{
Type = Penalty.PenaltyType.Kick,
Type = EFPenalty.PenaltyType.Kick,
Expires = DateTime.UtcNow,
Offender = Target,
Offense = Reason,
@ -978,9 +1036,9 @@ namespace IW4MAdmin
await Target.CurrentServer.OnClientDisconnected(Target);
#endif
Penalty newPenalty = new Penalty()
var newPenalty = new EFPenalty()
{
Type = Penalty.PenaltyType.TempBan,
Type = EFPenalty.PenaltyType.TempBan,
Expires = DateTime.UtcNow + length,
Offender = Target,
Offense = Reason,
@ -1012,18 +1070,17 @@ namespace IW4MAdmin
else
{
#if !DEBUG
string formattedString = String.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7");
string formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7");
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString);
#else
await targetClient.CurrentServer.OnClientDisconnected(targetClient);
#endif
}
Penalty newPenalty = new Penalty()
EFPenalty newPenalty = new EFPenalty()
{
Type = Penalty.PenaltyType.Ban,
Type = EFPenalty.PenaltyType.Ban,
Expires = null,
Offender = targetClient,
Offense = reason,
@ -1031,7 +1088,6 @@ namespace IW4MAdmin
Link = targetClient.AliasLink,
AutomatedOffense = originClient.AdministeredPenalties?.FirstOrDefault()?.AutomatedOffense,
IsEvadedOffense = isEvade
};
targetClient.SetLevel(Permission.Banned, originClient);
@ -1040,10 +1096,10 @@ namespace IW4MAdmin
override public async Task Unban(string reason, EFClient Target, EFClient Origin)
{
var unbanPenalty = new Penalty()
var unbanPenalty = new EFPenalty()
{
Type = Penalty.PenaltyType.Unban,
Expires = null,
Type = EFPenalty.PenaltyType.Unban,
Expires = DateTime.Now,
Offender = Target,
Offense = reason,
Punisher = Origin,
@ -1052,17 +1108,17 @@ namespace IW4MAdmin
Link = Target.AliasLink
};
await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId, Origin);
await Manager.GetPenaltyService().Create(unbanPenalty);
Target.SetLevel(Permission.User, Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId);
await Manager.GetPenaltyService().Create(unbanPenalty);
}
override public void InitializeTokens()
{
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("TOTALPLAYERS", (Server s) => Task.Run(async () => (await Manager.GetClientService().GetTotalClientsAsync()).ToString())));
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("VERSION", (Server s) => Task.FromResult(Application.Program.Version.ToString())));
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.CNextMap.GetNextMap(s)));
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("ADMINS", (Server s) => Task.FromResult(SharedLibraryCore.Commands.CListAdmins.OnlineAdmins(s))));
Manager.GetMessageTokens().Add(new MessageToken("TOTALPLAYERS", (Server s) => Task.Run(async () => (await Manager.GetClientService().GetTotalClientsAsync()).ToString())));
Manager.GetMessageTokens().Add(new MessageToken("VERSION", (Server s) => Task.FromResult(Application.Program.Version.ToString())));
Manager.GetMessageTokens().Add(new MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.CNextMap.GetNextMap(s)));
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(SharedLibraryCore.Commands.CListAdmins.OnlineAdmins(s))));
}
}
}

View File

@ -1,8 +1,6 @@
using IW4MAdmin.Application.Migration;
using SharedLibraryCore;
using SharedLibraryCore.Localization;
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -11,195 +9,226 @@ namespace IW4MAdmin.Application
{
public class Program
{
static public double Version { get; private set; }
static public ApplicationManager ServerManager;
private static ManualResetEventSlim OnShutdownComplete = new ManualResetEventSlim();
public static double Version { get; private set; } = Utilities.GetVersionAsDouble();
public static ApplicationManager ServerManager;
private static Task ApplicationTask;
public static void Main(string[] args)
/// <summary>
/// entrypoint of the application
/// </summary>
/// <returns></returns>
public static async Task Main()
{
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray;
Version = Utilities.GetVersionAsDouble();
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
Console.WriteLine("=====================================================");
Console.WriteLine(" IW4M ADMIN");
Console.WriteLine(" IW4MAdmin");
Console.WriteLine(" by RaidMax ");
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("=====================================================");
Index loc = null;
await LaunchAsync();
}
/// <summary>
/// event callback executed when the control + c combination is detected
/// gracefully stops the server manager and waits for all tasks to finish
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
{
ServerManager?.Stop();
await ApplicationTask;
}
/// <summary>
/// task that initializes application and starts the application monitoring and runtime tasks
/// </summary>
/// <returns></returns>
private static async Task LaunchAsync()
{
restart:
try
{
ServerManager = ApplicationManager.GetInstance();
var configuration = ServerManager.GetApplicationSettings().Configuration();
Localization.Configure.Initialize(configuration?.EnableCustomLocale ?? false ? (configuration.CustomLocale ?? "en-US") : "en-US");
if (configuration != null)
{
Localization.Configure.Initialize(configuration.EnableCustomLocale ? (configuration.CustomLocale ?? "windows-1252") : "windows-1252");
}
else
{
Localization.Configure.Initialize();
}
loc = Utilities.CurrentLocalization.LocalizationIndex;
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
CheckDirectories();
// do any needed migrations
// todo: move out
// do any needed housekeeping file/folder migrations
ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories();
ServerManager.Logger.WriteInfo($"Version is {Version}");
ServerManager.Logger.WriteInfo(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_VERSION"].FormatExt(Version));
var api = API.Master.Endpoint.Get();
var version = new API.Master.VersionInfo()
{
CurrentVersionStable = 99.99f
};
try
{
version = api.GetVersion().Result;
}
catch (Exception e)
{
ServerManager.Logger.WriteWarning(loc["MANAGER_VERSION_FAIL"]);
while (e.InnerException != null)
{
e = e.InnerException;
}
ServerManager.Logger.WriteDebug(e.Message);
}
if (version.CurrentVersionStable == 99.99f)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
#if !PRERELEASE
else if (version.CurrentVersionStable > Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString("0.0")}]");
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString("0.0")}]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#else
else if (version.CurrentVersionPrerelease > Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin-Prerelease {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString("0.0")}-pr]");
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString("0.0")}-pr]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#endif
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
ServerManager.Init().Wait();
var consoleTask = Task.Run(async () =>
{
string userInput;
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
do
{
userInput = Console.ReadLine();
if (userInput?.ToLower() == "quit")
{
ServerManager.Stop();
}
if (ServerManager.Servers.Count == 0)
{
Console.WriteLine(loc["MANAGER_CONSOLE_NOSERV"]);
continue;
}
if (userInput?.Length > 0)
{
GameEvent E = new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = userInput,
Origin = Origin,
Owner = ServerManager.Servers[0]
};
ServerManager.GetEventHandler().AddEvent(E);
await E.WaitAsync(30 * 1000);
}
Console.Write('>');
} while (ServerManager.Running);
});
await CheckVersion();
await ServerManager.Init();
}
catch (Exception e)
{
var loc = Utilities.CurrentLocalization.LocalizationIndex;
string failMessage = loc == null ? "Failed to initalize IW4MAdmin" : loc["MANAGER_INIT_FAIL"];
string exitMessage = loc == null ? "Press any key to exit..." : loc["MANAGER_EXIT"];
Console.WriteLine(failMessage);
while (e.InnerException != null)
{
e = e.InnerException;
}
Console.WriteLine(e.Message);
Console.WriteLine(exitMessage);
Console.ReadKey();
return;
}
if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront)
try
{
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
ApplicationTask = RunApplicationTasksAsync();
await ApplicationTask;
}
OnShutdownComplete.Reset();
ServerManager.Start();
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
OnShutdownComplete.Set();
catch { }
if (ServerManager.IsRestartRequested)
{
goto restart;
}
}
private static void OnCancelKey(object sender, ConsoleCancelEventArgs e)
/// <summary>
/// runs the core application tasks
/// </summary>
/// <returns></returns>
private static async Task RunApplicationTasksAsync()
{
ServerManager.Stop();
OnShutdownComplete.Wait();
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ?
WebfrontCore.Program.Init(ServerManager, ServerManager.CancellationToken) :
Task.CompletedTask;
// we want to run this one on a manual thread instead of letting the thread pool handle it,
// because we can't exit early from waiting on console input, and it prevents us from restarting
var inputThread = new Thread(async () => await ReadConsoleInput());
inputThread.Start();
var tasks = new[]
{
ServerManager.Start(),
webfrontTask,
};
await Task.WhenAll(tasks);
inputThread.Abort();
ServerManager.Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
}
static void CheckDirectories()
/// <summary>
/// checks for latest version of the application
/// notifies user if an update is available
/// </summary>
/// <returns></returns>
private static async Task CheckVersion()
{
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Plugins")))
var api = API.Master.Endpoint.Get();
var loc = Utilities.CurrentLocalization.LocalizationIndex;
var version = new API.Master.VersionInfo()
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Plugins"));
CurrentVersionStable = 99.99f
};
try
{
version = await api.GetVersion();
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Database")))
catch (Exception e)
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Database"));
ServerManager.Logger.WriteWarning(loc["MANAGER_VERSION_FAIL"]);
while (e.InnerException != null)
{
e = e.InnerException;
}
ServerManager.Logger.WriteDebug(e.Message);
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Log")))
if (version.CurrentVersionStable == 99.99f)
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log"));
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
#if !PRERELEASE
else if (version.CurrentVersionStable > Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString("0.0")}]");
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString("0.0")}]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#else
else if (version.CurrentVersionPrerelease > Version)
{
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"IW4MAdmin-Prerelease {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString("0.0")}-pr]");
Console.WriteLine(loc["MANAGER_VERSION_CURRENT"].FormatExt($"[v{Version.ToString("0.0")}-pr]"));
Console.ForegroundColor = ConsoleColor.Gray;
}
#endif
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
}
/// <summary>
/// reads input from the console and executes entered commands on the default server
/// </summary>
/// <returns></returns>
private static async Task ReadConsoleInput()
{
string lastCommand;
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
try
{
while (!ServerManager.CancellationToken.IsCancellationRequested)
{
lastCommand = Console.ReadLine();
if (lastCommand?.Length > 0)
{
if (lastCommand?.Length > 0)
{
GameEvent E = new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = lastCommand,
Origin = Origin,
Owner = ServerManager.Servers[0]
};
ServerManager.GetEventHandler().AddEvent(E);
await E.WaitAsync(Utilities.DefaultCommandTimeout, ServerManager.CancellationToken);
Console.Write('>');
}
}
}
}
catch (OperationCanceledException)
{ }
}
}
}

View File

@ -15,6 +15,27 @@ namespace IW4MAdmin.Application.Migration
/// </summary>
class ConfigurationMigration
{
/// <summary>
/// ensures required directories are created
/// </summary>
public static void CheckDirectories()
{
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Plugins")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Plugins"));
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Database")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Database"));
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Log")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log"));
}
}
/// <summary>
/// moves existing configs from the root folder into a configs folder
/// </summary>

View File

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

View File

@ -12,7 +12,11 @@ using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.RconParsers
{
#if DEBUG
public class BaseRConParser : IRConParser
#else
class BaseRConParser : IRConParser
#endif
{
public BaseRConParser()
{
@ -34,7 +38,7 @@ namespace IW4MAdmin.Application.RconParsers
},
};
Configuration.Status.Pattern = @"^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) +(.{0,32}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$";
Configuration.Status.Pattern = @"^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$";
Configuration.Status.AddMapping(ParserRegex.GroupType.RConClientNumber, 1);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConScore, 2);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConPing, 3);
@ -52,7 +56,7 @@ namespace IW4MAdmin.Application.RconParsers
public IRConParserConfiguration Configuration { get; set; }
public string Version { get; set; } = "CoD";
public virtual string Version { get; set; } = "CoD";
public Game GameName { get; set; } = Game.COD;
public bool CanGenerateLogPath { get; set; } = true;
@ -66,22 +70,13 @@ namespace IW4MAdmin.Application.RconParsers
{
string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName);
string response = string.Join('\n', lineSplit.Skip(1));
if (!lineSplit[0].Contains(Configuration.CommandPrefixes.RConResponse))
{
throw new DvarException($"Could not retrieve DVAR \"{dvarName}\"");
}
if (response.Contains("Unknown command"))
{
throw new DvarException($"DVAR \"{dvarName}\" does not exist");
}
var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (!match.Success)
if (!lineSplit[0].Contains(Configuration.CommandPrefixes.RConResponse) ||
response.Contains("Unknown command") ||
!match.Success)
{
throw new DvarException($"Could not retrieve DVAR \"{dvarName}\"");
throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName));
}
string value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value.StripColors();
@ -91,14 +86,14 @@ namespace IW4MAdmin.Application.RconParsers
return new Dvar<T>()
{
Name = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarName]].Value.StripColors(),
Value = string.IsNullOrEmpty(value) ? default(T) : (T)Convert.ChangeType(value, typeof(T)),
DefaultValue = string.IsNullOrEmpty(defaultValue) ? default(T) : (T)Convert.ChangeType(defaultValue, typeof(T)),
LatchedValue = string.IsNullOrEmpty(latchedValue) ? default(T) : (T)Convert.ChangeType(latchedValue, typeof(T)),
Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)),
DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)),
LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)),
Domain = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDomain]].Value.StripColors()
};
}
public async Task<List<EFClient>> GetStatusAsync(Connection connection)
public virtual async Task<List<EFClient>> GetStatusAsync(Connection connection)
{
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
return ClientsFromStatus(response);
@ -139,7 +134,17 @@ namespace IW4MAdmin.Application.RconParsers
ping = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value);
}
long networkId = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].Value.ConvertLong();
long networkId;
try
{
networkId = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].Value.ConvertGuidToLong();
}
catch (FormatException)
{
continue;
}
string name = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].Value.StripColors().Trim();
int? ip = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Value.Split(':')[0].ConvertToIP();
@ -147,22 +152,23 @@ namespace IW4MAdmin.Application.RconParsers
{
CurrentAlias = new EFAlias()
{
Name = name
Name = name,
IPAddress = ip
},
NetworkId = networkId,
ClientNumber = clientNumber,
IPAddress = ip,
Ping = ping,
Score = score,
IsBot = ip == null,
State = EFClient.ClientState.Connecting
};
//// they've not fully connected yet
//if (!client.IsBot && ping == 999)
//{
// continue;
//}
#if DEBUG
if (client.NetworkId < 1000 && client.NetworkId > 0)
{
client.IPAddress = 2147483646;
client.Ping = 0;
}
#endif
StatusPlayers.Add(client);
}
@ -176,5 +182,4 @@ namespace IW4MAdmin.Application.RconParsers
return StatusPlayers;
}
}
}
}}

View File

@ -1,6 +1,8 @@
import re
import os
import time
import random
import string
class LogReader(object):
def __init__(self):
@ -8,7 +10,7 @@ class LogReader(object):
# (if the time between checks is greater, ignore ) - in seconds
self.max_file_time_change = 30
def read_file(self, path):
def read_file(self, path, retrieval_key):
# this removes old entries that are no longer valid
try:
self._clear_old_logs()
@ -22,57 +24,88 @@ class LogReader(object):
# prevent traversing directories
if re.search('r^.+\.\.\\.+$', path):
return False
return self._generate_bad_response()
# must be a valid log path and log file
if not re.search(r'^.+[\\|\/](.+)[\\|\/].+.log$', path):
return False
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 False
return self._generate_bad_response()
# this is the first time the log has been requested
if path not in self.log_file_sizes:
self.log_file_sizes[path] = {
'length' : new_file_size,
'read': time.time()
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
}
return ''
else:
last_log_info = self.log_file_sizes[retrieval_key]
# grab the previous values
last_length = self.log_file_sizes[path]['length']
file_size_difference = new_file_size - last_length
print('next key is %s' % next_retrieval_key)
expired_key = last_log_info['previous_key']
print('expired key is %s' % expired_key)
# update the new size and actually read the data
self.log_file_sizes[path] = {
'length': new_file_size,
'read': time.time()
# 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
}
new_log_info = self.get_file_lines(path, file_size_difference)
return new_log_info
if expired_key in self.log_file_sizes:
print('deleting expired key %s' % expired_key)
del self.log_file_sizes[expired_key]
def get_file_lines(self, path, length):
#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(-length, 2)
file_data = file_handle.read(length)
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))
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 log in expired_logs:
print('removing expired log {0}'.format(log))
del self.log_file_sizes[log]
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:

View File

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

View File

@ -10,5 +10,5 @@ def init():
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
api = Api(app)
api.add_resource(LogResource, '/log/<string:path>')
api.add_resource(LogResource, '/log/<string:path>/<string:retrieval_key>')
#api.add_resource(RestartResource, '/restart')

View File

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.16
# Visual Studio Version 16
VisualStudioVersion = 16.0.28803.352
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8B310-269E-46D4-A612-24601F16065F}"
EndProject
@ -34,8 +34,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "DiscordWebhook", "DiscordWebhook\DiscordWebhook.pyproj", "{15A81D6E-7502-46CE-8530-0647A380B5F4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
ProjectSection(SolutionItems) = preProject
Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js
@ -309,18 +307,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
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|x64.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|x86.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|Mixed Platforms.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|x64.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|x86.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|x64.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|x86.ActiveCfg = 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

View File

@ -3,19 +3,23 @@
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<TargetLatestRuntimePatch >true</TargetLatestRuntimePatch>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2"/>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;"/>
<Exec Command="copy &quot;$(TargetDir)Microsoft.SyndicationFeed.ReaderWriter.dll&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;"/>
<Exec Command="copy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
<Exec Command="copy &quot;$(TargetDir)Microsoft.SyndicationFeed.ReaderWriter.dll&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
</Target>
</Project>

View File

@ -79,7 +79,7 @@ namespace AutomessageFeed
public Task OnUnloadAsync()
{
throw new NotImplementedException();
return Task.CompletedTask;
}
}
}

View File

@ -1,6 +1,5 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Linq;

View File

@ -1,7 +1,6 @@
using IW4ScriptCommands.Commands;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
@ -17,7 +16,7 @@ namespace WebfrontCore.Controllers.API
public IActionResult ClientInfo(string networkId)
{
var clientInfo = Manager.GetActiveClients()
.FirstOrDefault(c => c.NetworkId == networkId.ConvertLong());
.FirstOrDefault(c => c.NetworkId == networkId.ConvertGuidToLong());
if (clientInfo != null)
{
@ -40,7 +39,7 @@ namespace WebfrontCore.Controllers.API
return Unauthorized();
var client = Manager.GetActiveClients()
.FirstOrDefault(c => c.NetworkId == networkId.ConvertLong());
.FirstOrDefault(c => c.NetworkId == networkId.ConvertGuidToLong());
var server = Manager.GetServers().First(c => c.EndPoint == serverId);

View File

@ -3,10 +3,11 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<ApplicationIcon />
<StartupObject />
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
@ -14,12 +15,11 @@
</Target>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
<ProjectReference Include="..\Stats\Stats.csproj" />
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\Stats\Stats.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.2.2" />
</ItemGroup>
</Project>

View File

@ -18,13 +18,12 @@ namespace IW4MAdmin.Plugins.Login.Commands
public override async Task ExecuteAsync(GameEvent E)
{
var client = E.Owner.Manager.GetPrivilegedClients()[E.Origin.ClientId];
bool success = E.Owner.Manager.TokenAuthenticator.AuthorizeToken(E.Origin.NetworkId, E.Data);
if (!success)
{
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, client.PasswordSalt));
success = hashedPassword[0] == client.Password;
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, E.Origin.PasswordSalt));
success = hashedPassword[0] == E.Origin.Password;
}
if (success)

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.Login</PackageId>
@ -11,6 +11,7 @@
<Company>Forever None</Company>
<Product>Login Plugin for IW4MAdmin</Product>
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -18,13 +19,11 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.2.2" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
</Target>

View File

@ -42,10 +42,8 @@ namespace IW4MAdmin.Plugins.Login
E.Origin.Level == EFClient.Permission.Console)
return Task.CompletedTask;
E.Owner.Manager.GetPrivilegedClients().TryGetValue(E.Origin.ClientId, out EFClient client);
if (((Command)E.Extra).Name == new SharedLibraryCore.Commands.CSetPassword().Name &&
client?.Password == null)
E.Origin?.Password == null)
return Task.CompletedTask;
if (((Command)E.Extra).Name == new Commands.CLogin().Name)

View File

@ -1,11 +1,9 @@
using System.Collections.Concurrent;
using System.Linq;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Plugins.ProfanityDeterment
@ -19,8 +17,6 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
public string Author => "RaidMax";
BaseConfigurationHandler<Configuration> Settings;
ConcurrentDictionary<int, Tracking> ProfanityCounts;
IManager Manager;
public Task OnEventAsync(GameEvent E, Server S)
{
@ -29,10 +25,7 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
if (E.Type == GameEvent.EventType.Connect)
{
if (!ProfanityCounts.TryAdd(E.Origin.ClientId, new Tracking(E.Origin)))
{
S.Logger.WriteWarning("Could not add client to profanity tracking");
}
E.Origin.SetAdditionalProperty("_profanityInfringements", 0);
var objectionalWords = Settings.Configuration().OffensiveWords;
bool containsObjectionalWord = objectionalWords.FirstOrDefault(w => E.Origin.Name.ToLower().Contains(w)) != null;
@ -54,10 +47,7 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
if (E.Type == GameEvent.EventType.Disconnect)
{
if (!ProfanityCounts.TryRemove(E.Origin.ClientId, out Tracking old))
{
S.Logger.WriteWarning("Could not remove client from profanity tracking");
}
E.Origin.SetAdditionalProperty("_profanityInfringements", 0);
}
if (E.Type == GameEvent.EventType.Say)
@ -78,17 +68,17 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
if (containsObjectionalWord)
{
var clientProfanity = ProfanityCounts[E.Origin.ClientId];
if (clientProfanity.Infringements >= Settings.Configuration().KickAfterInfringementCount)
int profanityInfringments = E.Origin.GetAdditionalProperty<int>("_profanityInfringements");
if (profanityInfringments >= Settings.Configuration().KickAfterInfringementCount)
{
clientProfanity.Client.Kick(Settings.Configuration().ProfanityKickMessage, Utilities.IW4MAdminClient(E.Owner));
E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, Utilities.IW4MAdminClient(E.Owner));
}
else if (clientProfanity.Infringements < Settings.Configuration().KickAfterInfringementCount)
else if (profanityInfringments < Settings.Configuration().KickAfterInfringementCount)
{
clientProfanity.Infringements++;
clientProfanity.Client.Warn(Settings.Configuration().ProfanityWarningMessage, Utilities.IW4MAdminClient(E.Owner));
E.Origin.SetAdditionalProperty("_profanityInfringements", profanityInfringments + 1);
E.Origin.Warn(Settings.Configuration().ProfanityWarningMessage, Utilities.IW4MAdminClient(E.Owner));
}
}
}
@ -104,9 +94,6 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
Settings.Set((Configuration)new Configuration().Generate());
await Settings.Save();
}
ProfanityCounts = new ConcurrentDictionary<int, Tracking>();
Manager = manager;
}
public Task OnTickAsync(Server S) => Task.CompletedTask;

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.ProfanityDeterment</PackageId>
@ -13,14 +13,13 @@
<Description>Warns and kicks players for using profanity</Description>
<Copyright>2018</Copyright>
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.2.2" />
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Plugins.ProfanityDeterment
{
class Tracking
{
public EFClient Client { get; private set; }
public int Infringements { get; set; }
public Tracking(EFClient client)
{
Client = client;
Infringements = 0;
}
}
}

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'FrenchFry, RaidMax',
version: 0.3,
version: 0.5,
name: 'CoD4x Parser',
isParser: true,
@ -14,18 +14,18 @@ var plugin = {
rconParser = manager.GenerateDynamicRConParser();
eventParser = manager.GenerateDynamicEventParser();
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){16}|(?:[a-z]|[0-9]){32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$'
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){16,32}|(?:[a-z]|[0-9]){32}|bot[0-9]+) ([0-9+]) *(.{0,32}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$'
rconParser.Configuration.Status.AddMapping(104, 6); // RConName
rconParser.Configuration.Status.AddMapping(105, 8); // RConIPAddress
rconParser.Configuration.Dvar.Pattern = '^"(.+)" is: "(.+)?" default: "(.+)?" info: "(.+)?"$';
rconParser.Configuration.Dvar.AddMapping(109, 2); // DVAR latched value
rconParser.Configuration.Dvar.AddMapping(110, 4); // dvar info
rconParser.Version = 'CoD4 X 1.8 win_mingw-x86 build 2055 May 2 2017';
rconParser.Version = 'CoD4 X - win_mingw-x86 build 963 Mar 12 2019';
rconParser.GameName = 1; // IW3
eventParser.Configuration.GameDirectory = 'main';
eventParser.Version = 'CoD4 X 1.8 win_mingw-x86 build 2055 May 2 2017';
eventParser.Version = 'CoD4 X - win_mingw-x86 build 963 Mar 12 2019';
eventParser.GameName = 1; // IW3
eventParser.URLProtocolFormat = 'cod4://{{ip}}:{{port}}';
},

View File

@ -19,7 +19,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Kick = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'tempbanclient {0} "{1}"';
eventParser.Configuration.GameDirectory = 'userraw';
eventParser.Configuration.GameDirectory = 'userraw';
rconParser.Version = 'IW4x (v0.6.0)';
rconParser.GameName = 2; // IW4x

View File

@ -2,8 +2,8 @@ var rconParser;
var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.2,
author: 'RaidMax, Xerxes',
version: 0.4,
name: 'Plutonium T6 Parser',
isParser: true,
@ -26,13 +26,13 @@ var plugin = {
rconParser.Configuration.Dvar.AddMapping(107, 2);
rconParser.Configuration.WaitForResponse = false;
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(.+) +((?:[A-Z]+|[0-9]+)) +((?:[A-Z]|[0-9]){8,16}) +(.{0,16}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(-?[0-9]+) +([0-9]+) *$'
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$';
rconParser.Configuration.Status.AddMapping(100, 1);
rconParser.Configuration.Status.AddMapping(101, 2);
rconParser.Configuration.Status.AddMapping(102, 4);
rconParser.Configuration.Status.AddMapping(103, 5);
rconParser.Configuration.Status.AddMapping(104, 6);
rconParser.Configuration.Status.AddMapping(105, 8);
rconParser.Configuration.Status.AddMapping(102, 3);
rconParser.Configuration.Status.AddMapping(103, 4);
rconParser.Configuration.Status.AddMapping(104, 5);
rconParser.Configuration.Status.AddMapping(105, 6);
eventParser.Configuration.GameDirectory = 't6r\\data';

View File

@ -11,13 +11,22 @@ var plugin = {
// connect or join event
if (gameEvent.Type === 3) {
// this GUID seems to have been packed in a IW4 torrent and results in an unreasonable amount of people using the same GUID
if (gameEvent.Origin.NetworkId === -805366929435212061) {
// this GUID seems to have been packed in a IW4 torrent and results in an unreasonable amount of people using the same GUID
if (gameEvent.Origin.NetworkId === -805366929435212061 ||
gameEvent.Origin.NetworkId === 3150799945255696069 ||
gameEvent.Origin.NetworkId === 5859032128210324569 ||
gameEvent.Origin.NetworkId === 2908745942105435771 ||
gameEvent.Origin.NetworkId === -6492697076432899192 ||
gameEvent.Origin.NetworkId === 1145760003260769995 ||
gameEvent.Origin.NetworkId === -7102887284306116957 ||
gameEvent.Origin.NetworkId === 3474936520447289592 ||
gameEvent.Origin.NetworkId === -1168897558496584395 ||
gameEvent.Origin.NetworkId === 8348020621355817691 ||
gameEvent.Origin.NetworkId === 3259219574061214058) {
gameEvent.Origin.Kick('Your GUID is generic. Delete players/guids.dat and rejoin', _IW4MAdminClient);
}
}
},
onLoadAsync: function (manager) {
},

View File

@ -1,6 +1,6 @@
var plugin = {
author: 'RaidMax',
version: 1.1,
version: 1.2,
name: 'VPN Detection Plugin',
manager: null,
@ -43,8 +43,8 @@ var plugin = {
},
onEventAsync: function (gameEvent, server) {
// connect event
if (gameEvent.Type === 3) {
// join event
if (gameEvent.Type === 4) {
this.checkForVpn(gameEvent.Origin);
}
},

View File

@ -1,11 +1,13 @@
using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace IW4MAdmin.Plugins.Stats.Cheat
{
@ -16,16 +18,18 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Bone,
Chest,
Offset,
Strain
Strain,
Recoil
};
public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
public const int QUEUE_COUNT = 10;
public const int MIN_HITS_TO_RUN_DETECTION = 5;
private const int MIN_ANGLE_COUNT = 5;
public List<EFClientKill> QueuedHits { get; set; }
public List<EFClientKill> TrackedHits { get; set; }
int Kills;
int HitCount;
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
Dictionary<IW4Info.HitLocation, HitInfo> HitLocationCount;
double AngleDifferenceAverage;
EFClientStatistics ClientStats;
long LastOffset;
@ -33,20 +37,29 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
ILogger Log;
Strain Strain;
readonly DateTime ConnectionTime = DateTime.UtcNow;
private double sessionAverageRecoilAmount;
private EFClientKill lastHit;
private int validRecoilHitCount;
private class HitInfo
{
public int Count { get; set; }
public double Offset { get; set; }
};
public Detection(ILogger log, EFClientStatistics clientStats)
{
Log = log;
HitLocationCount = new Dictionary<IW4Info.HitLocation, int>();
HitLocationCount = new Dictionary<IW4Info.HitLocation, HitInfo>();
foreach (var loc in Enum.GetValues(typeof(IW4Info.HitLocation)))
{
HitLocationCount.Add((IW4Info.HitLocation)loc, 0);
HitLocationCount.Add((IW4Info.HitLocation)loc, new HitInfo());
}
ClientStats = clientStats;
Strain = new Strain();
Tracker = new ChangeTracking<EFACSnapshot>();
QueuedHits = new List<EFClientKill>();
TrackedHits = new List<EFClientKill>();
}
/// <summary>
@ -56,6 +69,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
/// <returns>true if detection reached thresholds, false otherwise</returns>
public DetectionPenaltyResult ProcessHit(EFClientKill hit, bool isDamage)
{
var results = new List<DetectionPenaltyResult>();
if ((hit.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
hit.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
hit.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
@ -65,16 +80,16 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
{
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Any,
ClientPenalty = EFPenalty.PenaltyType.Any,
};
}
DetectionPenaltyResult result = null;
LastWeapon = hit.Weapon;
HitLocationCount[hit.HitLoc]++;
HitLocationCount[hit.HitLoc].Count++;
HitCount++;
if (!isDamage)
{
Kills++;
@ -93,52 +108,64 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + realAgainstPredict) / hitLoc.HitCount;
hitLoc.HitOffsetAverage = (float)newAverage;
if (hitLoc.HitOffsetAverage > Thresholds.MaxOffset(hitLoc.HitCount) &&
int totalHits = ClientStats.HitLocations.Sum(_hit => _hit.HitCount);
var weightedLifetimeAverage = ClientStats.HitLocations.Where(_hit => _hit.HitCount > 0)
.Sum(_hit => _hit.HitOffsetAverage * _hit.HitCount) / totalHits;
if (weightedLifetimeAverage > Thresholds.MaxOffset(totalHits) &&
hitLoc.HitCount > 100)
{
//Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***");
//Log.WriteDebug($"Lifetime Average = {newAverage}");
//Log.WriteDebug($"Bone = {hitLoc.Location}");
//Log.WriteDebug($"HitCount = {hitLoc.HitCount}");
//Log.WriteDebug($"ID = {hit.AttackerId}");
Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***");
Log.WriteDebug($"Lifetime Average = {newAverage}");
Log.WriteDebug($"Bone = {hitLoc.Location}");
Log.WriteDebug($"HitCount = {hitLoc.HitCount}");
Log.WriteDebug($"ID = {hit.AttackerId}");
result = new DetectionPenaltyResult()
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = hitLoc.HitOffsetAverage,
HitCount = hitLoc.HitCount,
Type = DetectionType.Offset
};
});
}
// SESSION
double sessAverage = (AngleDifferenceAverage * (HitCount - 1) + realAgainstPredict) / HitCount;
AngleDifferenceAverage = sessAverage;
var sessionHitLoc = HitLocationCount[hit.HitLoc];
sessionHitLoc.Offset = (sessionHitLoc.Offset * (sessionHitLoc.Count - 1) + realAgainstPredict) / sessionHitLoc.Count;
if (sessAverage > Thresholds.MaxOffset(HitCount) &&
HitCount > 30)
int totalSessionHits = HitLocationCount.Sum(_hit => _hit.Value.Count);
var weightedSessionAverage = HitLocationCount.Where(_hit => _hit.Value.Count > 0)
.Sum(_hit => _hit.Value.Offset * _hit.Value.Count) / totalSessionHits;
AngleDifferenceAverage = weightedSessionAverage;
if (weightedSessionAverage > Thresholds.MaxOffset(totalSessionHits) &&
totalSessionHits >= (Thresholds.MediumSampleMinKills * 2))
{
//Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***");
//Log.WriteDebug($"Session Average = {sessAverage}");
//Log.WriteDebug($"HitCount = {HitCount}");
//Log.WriteDebug($"ID = {hit.AttackerId}");
Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***");
Log.WriteDebug($"Session Average = {weightedSessionAverage}");
Log.WriteDebug($"HitCount = {HitCount}");
Log.WriteDebug($"ID = {hit.AttackerId}");
result = new DetectionPenaltyResult()
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
Value = sessAverage,
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = weightedSessionAverage,
HitCount = HitCount,
Type = DetectionType.Offset,
Location = hitLoc.Location
};
});
}
#if DEBUG
Log.WriteDebug($"PredictVsReal={realAgainstPredict}");
#endif
}
#endregion
double currentStrain = Strain.GetStrain(isDamage, hit.Damage, hit.Distance / 0.0254, hit.ViewAngles, Math.Max(50, hit.TimeOffset - LastOffset));
#region STRAIN
double currentStrain = Strain.GetStrain(hit.Distance / 0.0254, hit.ViewAngles, Math.Max(50, hit.TimeOffset - LastOffset));
#if DEBUG == true
Log.WriteDebug($"Current Strain: {currentStrain}");
#endif
@ -152,26 +179,47 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
// flag
if (currentStrain > Thresholds.MaxStrainFlag)
{
result = new DetectionPenaltyResult()
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Flag,
ClientPenalty = EFPenalty.PenaltyType.Flag,
Value = currentStrain,
HitCount = HitCount,
Type = DetectionType.Strain
};
});
}
// ban
if (currentStrain > Thresholds.MaxStrainBan &&
HitCount >= 5)
{
result = new DetectionPenaltyResult()
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = currentStrain,
HitCount = HitCount,
Type = DetectionType.Strain
};
});
}
#endregion
#region RECOIL
float hitRecoilAverage = 0;
if (!Plugin.Config.Configuration().RecoilessWeapons.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex)))
{
validRecoilHitCount++;
hitRecoilAverage = (hit.AnglesList.Sum(_angle => _angle.Z) + hit.ViewAngles.Z) / (hit.AnglesList.Count + 1);
sessionAverageRecoilAmount = (sessionAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount;
if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && sessionAverageRecoilAmount == 0)
{
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = sessionAverageRecoilAmount,
HitCount = HitCount,
Type = DetectionType.Recoil
});
}
}
#endregion
@ -188,11 +236,11 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
double maxBoneRatioLerpValueForBan = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(3.25), Thresholds.BoneRatioThresholdHighSample(3.25), lerpAmount) + marginOfError;
// calculate headshot ratio
double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet] + HitLocationCount[IW4Info.HitLocation.neck]) / (double)HitCount);
double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head].Count + HitLocationCount[IW4Info.HitLocation.helmet].Count + HitLocationCount[IW4Info.HitLocation.neck].Count) / (double)HitCount);
// calculate maximum bone
double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v / (double)HitCount).Max());
var bone = HitLocationCount.FirstOrDefault(b => b.Value == HitLocationCount.Values.Max()).Key;
double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v.Count / (double)HitCount).Max());
var bone = HitLocationCount.FirstOrDefault(b => b.Value.Count == HitLocationCount.Values.Max(_hit => _hit.Count)).Key;
#region HEADSHOT_RATIO
// flag on headshot
@ -201,51 +249,25 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
// ban on headshot
if (currentHeadshotRatio > maxHeadshotLerpValueForBan)
{
Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**");
Log.WriteDebug($"ClientId: {hit.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
Log.WriteDebug($"**Ratio {currentHeadshotRatio}");
Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
results.Add(new DetectionPenaltyResult()
{
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
}
Log.WriteDebug(sb.ToString());
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = currentHeadshotRatio,
Location = IW4Info.HitLocation.head,
HitCount = HitCount,
Type = DetectionType.Bone
};
});
}
else
{
Log.WriteDebug("**Maximum Headshot Ratio Reached For Flag**");
Log.WriteDebug($"ClientId: {hit.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
Log.WriteDebug($"**Ratio {currentHeadshotRatio}");
Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
results.Add(new DetectionPenaltyResult()
{
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
}
Log.WriteDebug(sb.ToString());
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Flag,
ClientPenalty = EFPenalty.PenaltyType.Flag,
Value = currentHeadshotRatio,
Location = IW4Info.HitLocation.head,
HitCount = HitCount,
Type = DetectionType.Bone
};
});
}
}
#endregion
@ -257,58 +279,32 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
// ban on bone ratio
if (currentMaxBoneRatio > maxBoneRatioLerpValueForBan)
{
Log.WriteDebug("**Maximum Bone Ratio Reached For Ban**");
Log.WriteDebug($"ClientId: {hit.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
Log.WriteDebug($"**Ratio {currentMaxBoneRatio}");
Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForBan}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
results.Add(new DetectionPenaltyResult()
{
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
}
Log.WriteDebug(sb.ToString());
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = currentMaxBoneRatio,
Location = bone,
HitCount = HitCount,
Type = DetectionType.Bone
};
});
}
else
{
Log.WriteDebug("**Maximum Bone Ratio Reached For Flag**");
Log.WriteDebug($"ClientId: {hit.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
Log.WriteDebug($"**Ratio {currentMaxBoneRatio}");
Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForFlag}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
results.Add(new DetectionPenaltyResult()
{
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
}
Log.WriteDebug(sb.ToString());
result = new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Flag,
ClientPenalty = EFPenalty.PenaltyType.Flag,
Value = currentMaxBoneRatio,
Location = bone,
HitCount = HitCount,
Type = DetectionType.Bone
};
});
}
}
#endregion
}
#region CHEST_ABDOMEN_RATIO_SESSION
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper];
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count;
if (chestHits >= Thresholds.MediumSampleMinKills)
{
@ -318,63 +314,44 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(3), Thresholds.ChestAbdomenRatioThresholdHighSample(3), lerpAmount) + marginOfError;
double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(4), Thresholds.ChestAbdomenRatioThresholdHighSample(4), lerpAmount) + marginOfError;
double currentChestAbdomenRatio = HitLocationCount[IW4Info.HitLocation.torso_upper] / (double)HitLocationCount[IW4Info.HitLocation.torso_lower];
double currentChestAbdomenRatio = HitLocationCount[IW4Info.HitLocation.torso_upper].Count / (double)HitLocationCount[IW4Info.HitLocation.torso_lower].Count;
if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag)
{
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan && chestHits >= Thresholds.MediumSampleMinKills + 30)
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan && chestHits >= Thresholds.MediumSampleMinKills * 2)
{
//Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Ban**");
//Log.WriteDebug($"ClientId: {hit.AttackerId}");
//Log.WriteDebug($"**Chest Hits: {chestHits}");
//Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
//Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}");
//var sb = new StringBuilder();
//foreach (var kvp in HitLocationCount)
// sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
//Log.WriteDebug(sb.ToString());
result = new DetectionPenaltyResult()
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
Type = DetectionType.Chest,
HitCount = chestHits
};
});
}
else
{
//Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Flag**");
//Log.WriteDebug($"ClientId: {hit.AttackerId}");
//Log.WriteDebug($"**Chest Hits: {chestHits}");
//Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
//Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}");
//var sb = new StringBuilder();
//foreach (var kvp in HitLocationCount)
// sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
//Log.WriteDebug(sb.ToString());
result = new DetectionPenaltyResult()
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Flag,
ClientPenalty = EFPenalty.PenaltyType.Flag,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
Type = DetectionType.Chest,
HitCount = chestHits
};
});
}
}
}
#endregion
#endregion
Tracker.OnChange(new EFACSnapshot()
var snapshot = new EFACSnapshot()
{
When = hit.When,
ClientId = ClientStats.ClientId,
SessionAngleOffset = AngleDifferenceAverage,
RecoilOffset = hitRecoilAverage,
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds,
CurrentStrain = currentStrain,
CurrentViewAngle = hit.ViewAngles,
@ -397,81 +374,16 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
StrainAngleBetween = Strain.LastDistance,
TimeSinceLastEvent = (int)Strain.LastDeltaTime,
WeaponId = hit.Weapon
});
return result ?? new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Any,
};
}
public DetectionPenaltyResult ProcessTotalRatio(EFClientStatistics stats)
{
int totalChestHits = stats.HitLocations.Single(c => c.Location == IW4Info.HitLocation.torso_upper).HitCount;
Tracker.OnChange(snapshot);
if (totalChestHits >= 60)
{
double marginOfError = Thresholds.GetMarginOfError(totalChestHits);
double lerpAmount = Math.Min(1.0, (totalChestHits - 60) / 250.0);
// determine max acceptable ratio of chest to abdomen kills
double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(3.0), Thresholds.ChestAbdomenRatioThresholdHighSample(2.0), lerpAmount) + marginOfError;
double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(4.0), Thresholds.ChestAbdomenRatioThresholdHighSample(3.0), lerpAmount) + marginOfError;
double currentChestAbdomenRatio = totalChestHits /
stats.HitLocations.Single(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount;
if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag)
return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ??
results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
new DetectionPenaltyResult()
{
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan)
{
//Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Ban**");
//Log.WriteDebug($"ClientId: {stats.ClientId}");
//Log.WriteDebug($"**Total Chest Hits: {totalChestHits}");
//Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
//Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}");
//var sb = new StringBuilder();
//foreach (var location in stats.HitLocations)
// sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
//Log.WriteDebug(sb.ToString());
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
HitCount = totalChestHits,
Type = DetectionType.Chest
};
}
else
{
//Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Flag**");
//Log.WriteDebug($"ClientId: {stats.ClientId}");
//Log.WriteDebug($"**Total Chest Hits: {totalChestHits}");
//Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
//Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}");
//var sb = new StringBuilder();
//foreach (var location in stats.HitLocations)
// sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
//Log.WriteDebug(sb.ToString());
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Flag,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
HitCount = totalChestHits,
Type = DetectionType.Chest
};
}
}
}
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Any
};
ClientPenalty = EFPenalty.PenaltyType.Any,
};
}
}
}

View File

@ -1,11 +1,11 @@
using SharedLibraryCore.Objects;
using SharedLibraryCore.Database.Models;
namespace IW4MAdmin.Plugins.Stats.Cheat
{
class DetectionPenaltyResult
{
public Detection.DetectionType Type { get; set; }
public Penalty.PenaltyType ClientPenalty { get; set; }
public EFPenalty.PenaltyType ClientPenalty { get; set; }
public double Value { get; set; }
public IW4Info.HitLocation Location { get; set; }
public int HitCount { get; set; }

View File

@ -0,0 +1,79 @@
# IW4MAdmin Anticheat
**Initial document draft | 8.30.19**
**IW4MAdmin** anticheat for IW4x uses data from in-game logs to track player locations, hit locations, and view angles
to validate against a known set of play styles.
Every hit event ocurring in the game (Damage or Kill from a gun) is captured by IW4MAdmin and analyzed.
**Session analysis** occurs against all hit events since the player connected to the server.
**Lifetime analysis** occurs against all hit events ever occuring for a given player.
## Detection Types
### Bone
Compares the number of times a particular bone is hit against the number of hits on all other bones.
Many rudimentary aimbots lock onto a particular player bone position such as _Head_, or _Upper Torso_.
This detection method has the highest chance of a false positive, as non-cheaters can play abnormally
(e.g. going for headshots, or using a weapon that unconciously changes their playstyle)
#### Hit Location Reference
| Number | Hit Location |
|--------|-----------------|
| 2 | Head |
| 3 | Neck |
| 4 | Upper Torso |
| 5 | Lower Torso |
| 6 | Upper Right Arm |
| 7 | Upper Left Arm |
| 8 | Lower Right Arm |
| 9 | Lower Left Arm |
| 10 | Right Hand |
| 11 | Left Hand |
| 12 | Upper Right Leg |
| 13 | Upper Left Leg |
| 14 | Lower Right Leg |
| 15 | Right Foot |
| 16 | Left Foot |
### Chest
Identical to *Bone* detection, except it specifically compares the ratio of Upper Torso / Lower Torso hit counts.
It can be thought of as a focused bone detection type, as most aimbots don't aim to unusual bones such as a foot.
As with *Bone* detection, this is prone to false positives.
### Offset
Compares the player angles from three snapshots. The first snapshot being the snapshot immediately before the server registered the hit for a player.
The second snapshot is the "frame" the server registered the kill. The final snapshot is the snapshot immediately after the server registers the hit.
The algorithm is:
```
let a = first snapshot angles
let b = second snapshot angles
let c = third snapshot angles
offset = ((a - b) + (c - b)) - (a-c)
```
This detection method is very effective at detecting silent aimbots.
Silent aimbots "fake" a shot by changing a player's view angles for a single snapshot. From a spectator's position, the single snapshot view angle altering is not visible.
The larger "FOV" (field of view) from a silent aimbot, the more absolute difference between the three snapshots.
Over time if the average distance between these two viewangles is higher than expected, a detection is triggered.
False positives are very rare with this detection. However, extreme client lag can trigger a false positive in special situations.
### Strain
Analyzes the frequency, viewangle distance, and player distance between hits.
The algorithm is:
```
let v = view angle distance between two hits
let t = delta time (time between hits)
let d = distance between the attacker and victim
let s = decay over time
strain = ((v / t) * d)^s
```
A high value indicates fast target switching at large distance intervals, which if done naturally, requires an extreme level of mechanical effort.
"Rage" aimbots commonly switch to targets as fast as possible in any direction.
### Recoil
Compares the average of the last few snapshots of player angles to 0. Client sided no recoil prevents the view angle "Z" axis from changing.
If the average view angle "Z" axis value remains 0, the detection is triggered.
As of 8.30.19, there are no known false positives for this detection.
Several weapons which do not have any recoil from the game are excluded in this detection.
### Format
Detection penalty reasons are as follow:
<_detectionType_>-<_location/value_>@<_hitCount_>
Example:
- detectionType = Strain
- value = 1.39
- hitCount = 136
Result: **Strain-1.39@136**
This reason is only visible to logged in privileged users.

View File

@ -14,7 +14,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
public Vector3 LastAngle { get; private set; }
public double LastDeltaTime { get; private set; }
public double GetStrain(bool isDamage, int damage, double killDistance, Vector3 newAngle, double deltaTime)
public double GetStrain(double killDistance, Vector3 newAngle, double deltaTime)
{
if (LastAngle == null)
LastAngle = newAngle;

View File

@ -26,10 +26,14 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
public const int MediumSampleMinKills = 30;
public const int HighSampleMinKills = 100;
public const double KillTimeThreshold = 0.2;
public const int LowSampleMinKillsRecoil = 5;
public const double MaxStrainBan = 0.9;
public static double MaxOffset(int sampleSize) => Math.Exp(Math.Max(-3.07 + (-3.07 / Math.Sqrt(sampleSize)), -3.07 - (-3.07 / Math.Sqrt(sampleSize))) + 4 * (0.869));
private const double _offsetMeanLog = -2.727273;
private const double _offsetSdLog = 0.458325;
public static double MaxOffset(int sampleSize) => Math.Exp(Math.Max(_offsetMeanLog + (_offsetMeanLog / Math.Sqrt(sampleSize)), _offsetMeanLog - (_offsetMeanLog / Math.Sqrt(sampleSize))) + 4 * (_offsetSdLog));
public const double MaxStrainFlag = 0.36;
public static double GetMarginOfError(int numKills) => 1.6455 / Math.Sqrt(numKills);

View File

@ -4,7 +4,6 @@ using System.Linq;
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Objects;
using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore.Database;
using System.Collections.Generic;
@ -17,14 +16,14 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{
public static async Task<List<string>> GetMostPlayed(Server s)
{
long serverId = await StatManager.GetIdForServer(s);
long serverId = StatManager.GetIdForServer(s);
List<string> mostPlayed = new List<string>()
{
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--"
};
using (var db = new DatabaseContext())
using (var db = new DatabaseContext(true))
{
db.ChangeTracker.AutoDetectChangesEnabled = false;
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

View File

@ -17,7 +17,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
if (E.Origin.ClientNumber >= 0)
{
long serverId = await Helpers.StatManager.GetIdForServer(E.Owner);
long serverId = Helpers.StatManager.GetIdForServer(E.Owner);
EFClientStatistics clientStats;
using (var ctx = new DatabaseContext(disableTracking: true))

View File

@ -4,7 +4,6 @@ using System.Linq;
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Services;
using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore.Database;
@ -18,7 +17,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{
public static async Task<List<string>> GetTopStats(Server s)
{
long serverId = await StatManager.GetIdForServer(s);
long serverId = StatManager.GetIdForServer(s);
List<string> topStatsText = new List<string>()
{
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"

View File

@ -1,5 +1,4 @@
using SharedLibraryCore;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Services;
using IW4MAdmin.Plugins.Stats.Models;
using System;
@ -43,27 +42,47 @@ namespace IW4MAdmin.Plugins.Stats.Commands
}
}
long serverId = await StatManager.GetIdForServer(E.Owner);
long serverId = StatManager.GetIdForServer(E.Owner);
using (var ctx = new DatabaseContext(disableTracking: true))
if (E.Target != null)
{
if (E.Target != null)
{
int performanceRanking = await StatManager.GetClientOverallRanking(E.Target.ClientId);
string performanceRankingString = performanceRanking == 0 ? loc["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{loc["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}";
int performanceRanking = await StatManager.GetClientOverallRanking(E.Target.ClientId);
string performanceRankingString = performanceRanking == 0 ? loc["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{loc["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}";
pStats = (await ctx.Set<EFClientStatistics>().FirstAsync(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId));
statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}";
if (E.Owner.GetClientsAsList().Any(_client => _client.Equals(E.Target)))
{
pStats = Plugin.Manager.GetClientStats(E.Target.ClientId, serverId);
}
else
{
int performanceRanking = await StatManager.GetClientOverallRanking(E.Origin.ClientId);
string performanceRankingString = performanceRanking == 0 ? loc["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{loc["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}";
pStats = (await ctx.Set<EFClientStatistics>().FirstAsync((c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId)));
statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}";
using (var ctx = new DatabaseContext(true))
{
pStats = (await ctx.Set<EFClientStatistics>().FirstAsync(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId));
}
}
statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}";
}
else
{
int performanceRanking = await StatManager.GetClientOverallRanking(E.Origin.ClientId);
string performanceRankingString = performanceRanking == 0 ? loc["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{loc["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}";
if (E.Owner.GetClientsAsList().Any(_client => _client.Equals(E.Origin)))
{
pStats = Plugin.Manager.GetClientStats(E.Origin.ClientId, serverId);
}
else
{
using (var ctx = new DatabaseContext(true))
{
pStats = (await ctx.Set<EFClientStatistics>().FirstAsync(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId));
}
}
statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}";
}
if (E.Message.IsBroadcastCommand())

View File

@ -9,6 +9,7 @@ namespace IW4MAdmin.Plugins.Stats.Config
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 string Name() => "Stats";
@ -49,6 +50,13 @@ namespace IW4MAdmin.Plugins.Stats.Config
},
};
RecoilessWeapons = new List<string>()
{
"ranger.*_mp",
"model1887.*_mp",
".+shotgun.*_mp"
};
TopPlayersMinPlayTime = 3600 * 3;
StoreClientKills = false;

View File

@ -36,7 +36,7 @@ namespace IW4MAdmin.Plugins.Stats.Web.Controllers
if (server != null)
{
serverId = await StatManager.GetIdForServer(server);
serverId = StatManager.GetIdForServer(server);
}
var results = await Plugin.Manager.GetTopStats(offset, count, serverId);

View File

@ -14,9 +14,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
float X = vector.X >= 0 ? vector.X : 360.0f + vector.X;
float Y = vector.Y >= 0 ? vector.Y : 360.0f + vector.Y;
float Z = vector.Z >= 0 ? vector.Z : 360.0f + vector.Z;
return new Vector3(Y, X, Z);
return new Vector3(Y, X, vector.Z);
}
public static float ToRadians(this float value) => (float)Math.PI * value / 180.0f;

View File

@ -2,6 +2,7 @@
using IW4MAdmin.Plugins.Stats.Models;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace IW4MAdmin.Plugins.Stats.Helpers
@ -9,6 +10,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
class ServerStats {
public ConcurrentDictionary<int, EFClientStatistics> PlayerStats { get; set; }
public ConcurrentDictionary<int, Detection> PlayerDetections { get; set; }
public IList<EFClientKill> HitCache { get; private set; }
public EFServerStatistics ServerStatistics { get; private set; }
public EFServer Server { get; private set; }
public bool IsTeamBased { get; set; }
@ -17,6 +19,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
PlayerStats = new ConcurrentDictionary<int, EFClientStatistics>();
PlayerDetections = new ConcurrentDictionary<int, Detection>();
HitCache = new List<EFClientKill>();
ServerStatistics = st;
Server = sv;
}

View File

@ -7,7 +7,6 @@ using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -20,25 +19,28 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
public class StatManager
{
private ConcurrentDictionary<long, ServerStats> Servers;
private ILogger Log;
private readonly IManager Manager;
private readonly SemaphoreSlim OnProcessingPenalty;
private readonly SemaphoreSlim OnProcessingSensitive;
private const int MAX_CACHED_HITS = 100;
private readonly ConcurrentDictionary<long, ServerStats> _servers;
private readonly ILogger _log;
private static List<EFServer> serverModels;
public StatManager(IManager mgr)
{
Servers = new ConcurrentDictionary<long, ServerStats>();
Log = mgr.GetLogger(0);
Manager = mgr;
OnProcessingPenalty = new SemaphoreSlim(1, 1);
OnProcessingSensitive = new SemaphoreSlim(1, 1);
_servers = new ConcurrentDictionary<long, ServerStats>();
_log = mgr.GetLogger(0);
}
private void SetupServerIds()
{
using (var ctx = new DatabaseContext(disableTracking: true))
{
serverModels = ctx.Set<EFServer>().ToList();
}
}
public EFClientStatistics GetClientStats(int clientId, long serverId)
{
return Servers[serverId].PlayerStats[clientId];
return _servers[serverId].PlayerStats[clientId];
}
public static Expression<Func<EFRating, bool>> GetRankingFunc(long? serverId = null)
@ -135,6 +137,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
where clientIds.Contains(stat.ClientId)
where stat.Kills > 0 || stat.Deaths > 0
where serverId == null ? true : stat.ServerId == serverId
group stat by stat.ClientId into s
select new
{
@ -142,7 +146,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Kills = s.Sum(c => c.Kills),
Deaths = s.Sum(c => c.Deaths),
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) / s.Sum(c => c.TimePlayed),
TotalTimePlayed = s.Sum(c => c.TimePlayed)
TotalTimePlayed = s.Sum(c => c.TimePlayed),
});
#if DEBUG == true
@ -191,7 +195,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// insert the server if it does not exist
try
{
long serverId = GetIdForServer(sv).Result;
if (serverModels == null)
{
SetupServerIds();
}
long serverId = GetIdForServer(sv);
EFServer server;
using (var ctx = new DatabaseContext(disableTracking: true))
@ -219,7 +228,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
server = new EFServer()
{
Port = sv.GetPort(),
Port = sv.Port,
EndPoint = sv.ToString(),
ServerId = serverId,
GameName = sv.GameName
@ -242,7 +251,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// check to see if the stats have ever been initialized
var serverStats = InitializeServerStats(server.ServerId);
Servers.TryAdd(serverId, new ServerStats(server, serverStats)
_servers.TryAdd(serverId, new ServerStats(server, serverStats)
{
IsTeamBased = sv.Gametype != "dm"
});
@ -250,8 +259,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (Exception e)
{
Log.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]} - {e.Message}");
Log.WriteDebug(e.GetExceptionInfo());
_log.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]} - {e.Message}");
_log.WriteDebug(e.GetExceptionInfo());
}
}
@ -262,24 +271,22 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// <returns>EFClientStatistic of specified player</returns>
public async Task<EFClientStatistics> AddPlayer(EFClient pl)
{
await OnProcessingSensitive.WaitAsync();
try
{
long serverId = await GetIdForServer(pl.CurrentServer);
long serverId = GetIdForServer(pl.CurrentServer);
if (!Servers.ContainsKey(serverId))
if (!_servers.ContainsKey(serverId))
{
Log.WriteError($"[Stats::AddPlayer] Server with id {serverId} could not be found");
_log.WriteError($"[Stats::AddPlayer] Server with id {serverId} could not be found");
return null;
}
var playerStats = Servers[serverId].PlayerStats;
var detectionStats = Servers[serverId].PlayerDetections;
var playerStats = _servers[serverId].PlayerStats;
var detectionStats = _servers[serverId].PlayerDetections;
if (playerStats.ContainsKey(pl.ClientId))
{
Log.WriteWarning($"Duplicate ClientId in stats {pl.ClientId}");
_log.WriteWarning($"Duplicate ClientId in stats {pl.ClientId}");
return playerStats[pl.ClientId];
}
@ -321,7 +328,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (!playerStats.TryAdd(clientStats.ClientId, clientStats))
{
Log.WriteWarning("Adding new client to stats failed");
_log.WriteWarning("Adding new client to stats failed");
}
}
@ -329,7 +336,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
if (!playerStats.TryAdd(clientStats.ClientId, clientStats))
{
Log.WriteWarning("Adding pre-existing client to stats failed");
_log.WriteWarning("Adding pre-existing client to stats failed");
}
}
@ -366,9 +373,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
clientStats.SessionScore = pl.Score;
clientStats.LastScore = pl.Score;
if (!detectionStats.TryAdd(pl.ClientId, new Cheat.Detection(Log, clientStats)))
if (!detectionStats.TryAdd(pl.ClientId, new Detection(_log, clientStats)))
{
Log.WriteWarning("Could not add client to detection");
_log.WriteWarning("Could not add client to detection");
}
pl.CurrentServer.Logger.WriteInfo($"Adding {pl} to stats");
@ -379,13 +386,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (Exception ex)
{
Log.WriteWarning("Could not add client to stats");
Log.WriteDebug(ex.GetExceptionInfo());
}
finally
{
OnProcessingSensitive.Release(1);
_log.WriteWarning("Could not add client to stats");
_log.WriteDebug(ex.GetExceptionInfo());
}
return null;
@ -400,56 +402,46 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
pl.CurrentServer.Logger.WriteInfo($"Removing {pl} from stats");
long serverId = await GetIdForServer(pl.CurrentServer);
var playerStats = Servers[serverId].PlayerStats;
var detectionStats = Servers[serverId].PlayerDetections;
var serverStats = Servers[serverId].ServerStatistics;
long serverId = GetIdForServer(pl.CurrentServer);
var playerStats = _servers[serverId].PlayerStats;
var detectionStats = _servers[serverId].PlayerDetections;
var serverStats = _servers[serverId].ServerStatistics;
if (!playerStats.ContainsKey(pl.ClientId))
{
pl.CurrentServer.Logger.WriteWarning($"Client disconnecting not in stats {pl}");
// remove the client from the stats dictionary as they're leaving
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue1);
detectionStats.TryRemove(pl.ClientId, out Detection removedValue2);
playerStats.TryRemove(pl.ClientId, out _);
detectionStats.TryRemove(pl.ClientId, out _);
return;
}
// get individual client's stats
var clientStats = playerStats[pl.ClientId];
// remove the client from the stats dictionary as they're leaving
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue3);
detectionStats.TryRemove(pl.ClientId, out Detection removedValue4);
// sync their stats before they leave
clientStats = UpdateStats(clientStats);
await SaveClientStats(clientStats);
using (var ctx = new DatabaseContext(disableTracking: true))
{
ctx.Update(clientStats);
await ctx.SaveChangesAsync();
}
// remove the client from the stats dictionary as they're leaving
playerStats.TryRemove(pl.ClientId, out _);
detectionStats.TryRemove(pl.ClientId, out _);
// increment the total play time
serverStats.TotalPlayTime += pl.ConnectionLength;
}
private static async Task SaveClientStats(EFClientStatistics clientStats)
{
using (var ctx = new DatabaseContext())
{
ctx.Update(clientStats);
await ctx.SaveChangesAsync();
}
}
public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, long serverId)
{
// todo: maybe do something with this
//string regex = @"^(D);(.+);([0-9]+);(allies|axis);(.+);([0-9]+);(allies|axis);(.+);(.+);([0-9]+);(.+);(.+)$";
//var match = Regex.Match(eventLine, regex, RegexOptions.IgnoreCase);
//if (match.Success)
//{
// // this gives us what team the player is on
// var attackerStats = Servers[serverId].PlayerStats[attackerClientId];
// var victimStats = Servers[serverId].PlayerStats[victimClientId];
// IW4Info.Team victimTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[4].ToString(), true);
// IW4Info.Team attackerTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[7].ToString(), true);
// attackerStats.Team = attackerTeam;
// victimStats.Team = victimTeam;
//}
}
/// <summary>
@ -463,155 +455,176 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Vector3 vDeathOrigin = null;
Vector3 vKillOrigin = null;
Vector3 vViewAngles = null;
SemaphoreSlim waiter = null;
try
{
vDeathOrigin = Vector3.Parse(deathOrigin);
vKillOrigin = Vector3.Parse(killOrigin);
vViewAngles = Vector3.Parse(viewAngles).FixIW4Angles();
}
catch (FormatException)
{
Log.WriteWarning("Could not parse kill or death origin or viewangle vectors");
Log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAngle - {viewAngles}");
await AddStandardKill(attacker, victim);
return;
}
var snapshotAngles = new List<Vector3>();
try
{
foreach (string angle in snapAngles.Split(':', StringSplitOptions.RemoveEmptyEntries))
try
{
snapshotAngles.Add(Vector3.Parse(angle).FixIW4Angles());
vDeathOrigin = Vector3.Parse(deathOrigin);
vKillOrigin = Vector3.Parse(killOrigin);
vViewAngles = Vector3.Parse(viewAngles).FixIW4Angles();
}
}
catch (FormatException)
{
Log.WriteWarning("Could not parse snapshot angles");
return;
}
catch (FormatException)
{
_log.WriteWarning("Could not parse kill or death origin or viewangle vectors");
_log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAngle - {viewAngles}");
await AddStandardKill(attacker, victim);
return;
}
var hit = new EFClientKill()
{
Active = true,
AttackerId = attacker.ClientId,
VictimId = victim.ClientId,
ServerId = serverId,
Map = ParseEnum<IW4Info.MapName>.Get(map, typeof(IW4Info.MapName)),
DeathOrigin = vDeathOrigin,
KillOrigin = vKillOrigin,
DeathType = ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
Damage = Int32.Parse(damage),
HitLoc = ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
Weapon = ParseEnum<IW4Info.WeaponName>.Get(weapon, typeof(IW4Info.WeaponName)),
ViewAngles = vViewAngles,
TimeOffset = Int64.Parse(offset),
When = time,
IsKillstreakKill = isKillstreakKill[0] != '0',
AdsPercent = float.Parse(Ads),
Fraction = double.Parse(fraction),
VisibilityPercentage = double.Parse(visibilityPercentage),
IsKill = !isDamage,
AnglesList = snapshotAngles
};
if ((hit.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE &&
hit.Damage == 100000) || hit.HitLoc == IW4Info.HitLocation.shield)
{
// suicide by switching teams so let's not count it against them
return;
}
if (!isDamage)
{
await AddStandardKill(attacker, victim);
}
if (hit.IsKillstreakKill)
{
return;
}
// incase the add player event get delayed
if (!Servers[serverId].PlayerStats.ContainsKey(attacker.ClientId))
{
await AddPlayer(attacker);
}
var clientDetection = Servers[serverId].PlayerDetections[attacker.ClientId];
var clientStats = Servers[serverId].PlayerStats[attacker.ClientId];
using (var ctx = new DatabaseContext(disableTracking: true))
{
ctx.Set<EFClientStatistics>().Update(clientStats);
await ctx.SaveChangesAsync();
}
// increment their hit count
if (hit.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
hit.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
hit.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
{
clientStats.HitLocations.Single(hl => hl.Location == hit.HitLoc).HitCount += 1;
}
using (var ctx = new DatabaseContext())
{
await OnProcessingPenalty.WaitAsync();
var snapshotAngles = new List<Vector3>();
try
{
if (Plugin.Config.Configuration().StoreClientKills)
foreach (string angle in snapAngles.Split(':', StringSplitOptions.RemoveEmptyEntries))
{
ctx.Set<EFClientKill>().Add(hit);
snapshotAngles.Add(Vector3.Parse(angle).FixIW4Angles());
}
}
if (Plugin.Config.Configuration().EnableAntiCheat && !attacker.IsBot)
catch (FormatException)
{
_log.WriteWarning("Could not parse snapshot angles");
return;
}
var hit = new EFClientKill()
{
Active = true,
AttackerId = attacker.ClientId,
VictimId = victim.ClientId,
ServerId = serverId,
DeathOrigin = vDeathOrigin,
KillOrigin = vKillOrigin,
DeathType = ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
Damage = int.Parse(damage),
HitLoc = ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
Weapon = ParseEnum<IW4Info.WeaponName>.Get(weapon, typeof(IW4Info.WeaponName)),
ViewAngles = vViewAngles,
TimeOffset = long.Parse(offset),
When = time,
IsKillstreakKill = isKillstreakKill[0] != '0',
AdsPercent = float.Parse(Ads, System.Globalization.CultureInfo.InvariantCulture),
Fraction = double.Parse(fraction, System.Globalization.CultureInfo.InvariantCulture),
VisibilityPercentage = double.Parse(visibilityPercentage, System.Globalization.CultureInfo.InvariantCulture),
IsKill = !isDamage,
AnglesList = snapshotAngles
};
if (hit.HitLoc == IW4Info.HitLocation.shield)
{
// we don't care about shield hits
return;
}
// incase the add player event get delayed
if (!_servers[serverId].PlayerStats.ContainsKey(attacker.ClientId))
{
await AddPlayer(attacker);
}
if (!isDamage)
{
await AddStandardKill(attacker, victim);
}
var clientDetection = _servers[serverId].PlayerDetections[attacker.ClientId];
var clientStats = _servers[serverId].PlayerStats[attacker.ClientId];
waiter = clientStats.ProcessingHit;
await waiter.WaitAsync();
// increment their hit count
if (hit.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
hit.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
hit.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
{
clientStats.HitLocations.First(hl => hl.Location == hit.HitLoc).HitCount += 1;
}
if (hit.IsKillstreakKill)
{
return;
}
if (Plugin.Config.Configuration().StoreClientKills)
{
var cache = _servers[serverId].HitCache;
cache.Add(hit);
if (cache.Count > MAX_CACHED_HITS)
{
if (clientDetection.QueuedHits.Count > Detection.QUEUE_COUNT)
await SaveHitCache(serverId);
}
}
if (Plugin.Config.Configuration().EnableAntiCheat && !attacker.IsBot && attacker.ClientId != victim.ClientId)
{
DetectionPenaltyResult result = new DetectionPenaltyResult() { ClientPenalty = EFPenalty.PenaltyType.Any };
clientDetection.TrackedHits.Add(hit);
if (clientDetection.TrackedHits.Count >= Detection.MIN_HITS_TO_RUN_DETECTION)
{
while (clientDetection.TrackedHits.Count > 0)
{
while (clientDetection.QueuedHits.Count > 0)
var oldestHit = clientDetection.TrackedHits
.OrderBy(_hits => _hits.TimeOffset)
.First();
clientDetection.TrackedHits.Remove(oldestHit);
result = clientDetection.ProcessHit(oldestHit, isDamage);
#if !DEBUG
await ApplyPenalty(result, attacker);
#endif
if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any)
{
clientDetection.QueuedHits = clientDetection.QueuedHits.OrderBy(_hits => _hits.TimeOffset).ToList();
var oldestHit = clientDetection.QueuedHits.First();
clientDetection.QueuedHits.RemoveAt(0);
await ApplyPenalty(clientDetection.ProcessHit(oldestHit, isDamage), clientDetection, attacker, ctx);
await SaveTrackedSnapshots(clientDetection);
if (result.ClientPenalty == EFPenalty.PenaltyType.Ban)
{
// we don't care about any additional hits now that they're banned
clientDetection.TrackedHits.Clear();
break;
}
}
}
else
{
clientDetection.QueuedHits.Add(hit);
}
await ApplyPenalty(clientDetection.ProcessTotalRatio(clientStats), clientDetection, attacker, ctx);
}
ctx.Set<EFHitLocationCount>().UpdateRange(clientStats.HitLocations);
await ctx.SaveChangesAsync();
}
}
catch (Exception ex)
{
Log.WriteError("Could not save hit or AC info");
Log.WriteDebug(ex.GetExceptionInfo());
}
catch (Exception ex)
{
_log.WriteError("Could not save hit or AC info");
_log.WriteDebug(ex.GetExceptionInfo());
}
OnProcessingPenalty.Release(1);
finally
{
waiter?.Release(1);
}
}
async Task ApplyPenalty(DetectionPenaltyResult penalty, Detection clientDetection, EFClient attacker, DatabaseContext ctx)
public async Task SaveHitCache(long serverId)
{
using (var ctx = new DatabaseContext(true))
{
var server = _servers[serverId];
ctx.AddRange(server.HitCache);
await ctx.SaveChangesAsync();
server.HitCache.Clear();
}
}
async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker)
{
var penaltyClient = Utilities.IW4MAdminClient(attacker.CurrentServer);
switch (penalty.ClientPenalty)
{
case Penalty.PenaltyType.Ban:
case EFPenalty.PenaltyType.Ban:
if (attacker.Level == EFClient.Permission.Banned)
{
break;
@ -627,14 +640,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
};
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], penaltyClient, false).WaitAsync();
if (clientDetection.Tracker.HasChanges)
{
SaveTrackedSnapshots(clientDetection, ctx);
}
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], penaltyClient, false).WaitAsync(Utilities.DefaultCommandTimeout, attacker.CurrentServer.Manager.CancellationToken);
break;
case Penalty.PenaltyType.Flag:
case EFPenalty.PenaltyType.Flag:
if (attacker.Level != EFClient.Permission.User)
{
break;
@ -644,104 +652,57 @@ 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}";
await attacker.Flag(flagReason, penaltyClient).WaitAsync();
if (clientDetection.Tracker.HasChanges)
{
SaveTrackedSnapshots(clientDetection, ctx);
}
await attacker.Flag(flagReason, penaltyClient, new TimeSpan(168, 0, 0)).WaitAsync(Utilities.DefaultCommandTimeout, attacker.CurrentServer.Manager.CancellationToken);
break;
}
}
void SaveTrackedSnapshots(Detection clientDetection, DatabaseContext ctx)
async Task SaveTrackedSnapshots(Detection clientDetection)
{
// todo: why does this cause duplicate primary key
var change = clientDetection.Tracker.GetNextChange();
while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot))
EFACSnapshot change;
using (var ctx = new DatabaseContext(true))
{
if (change.HitOrigin.Vector3Id > 0)
while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot))
{
change.HitOriginId = change.HitOrigin.Vector3Id;
ctx.Attach(change.HitOrigin);
ctx.Add(change);
}
else if (change.HitOrigin.Vector3Id == 0)
{
ctx.Add(change.HitOrigin);
}
if (change.HitDestination.Vector3Id > 0)
{
change.HitDestinationId = change.HitDestination.Vector3Id;
ctx.Attach(change.HitDestination);
}
else if (change.HitDestination.Vector3Id == 0)
{
ctx.Add(change.HitOrigin);
}
if (change.CurrentViewAngle.Vector3Id > 0)
{
change.CurrentViewAngleId = change.CurrentViewAngle.Vector3Id;
ctx.Attach(change.CurrentViewAngle);
}
else if (change.CurrentViewAngle.Vector3Id == 0)
{
ctx.Add(change.HitOrigin);
}
if (change.LastStrainAngle.Vector3Id > 0)
{
change.LastStrainAngleId = change.LastStrainAngle.Vector3Id;
ctx.Attach(change.LastStrainAngle);
}
else if (change.LastStrainAngle.Vector3Id == 0)
{
ctx.Add(change.HitOrigin);
}
ctx.Add(change);
await ctx.SaveChangesAsync();
}
}
public async Task AddStandardKill(EFClient attacker, EFClient victim)
{
long serverId = await GetIdForServer(attacker.CurrentServer);
long serverId = GetIdForServer(attacker.CurrentServer);
EFClientStatistics attackerStats = null;
if (!Servers[serverId].PlayerStats.ContainsKey(attacker.ClientId))
if (!_servers[serverId].PlayerStats.ContainsKey(attacker.ClientId))
{
attackerStats = await AddPlayer(attacker);
}
else
{
attackerStats = Servers[serverId].PlayerStats[attacker.ClientId];
attackerStats = _servers[serverId].PlayerStats[attacker.ClientId];
}
EFClientStatistics victimStats = null;
if (!Servers[serverId].PlayerStats.ContainsKey(victim.ClientId))
if (!_servers[serverId].PlayerStats.ContainsKey(victim.ClientId))
{
victimStats = await AddPlayer(victim);
}
else
{
victimStats = Servers[serverId].PlayerStats[victim.ClientId];
victimStats = _servers[serverId].PlayerStats[victim.ClientId];
}
#if DEBUG
Log.WriteDebug("Calculating standard kill");
_log.WriteDebug("Calculating standard kill");
#endif
// update the total stats
Servers[serverId].ServerStatistics.TotalKills += 1;
await Sync(attacker.CurrentServer);
_servers[serverId].ServerStatistics.TotalKills += 1;
// this happens when the round has changed
if (attackerStats.SessionScore == 0)
@ -777,38 +738,24 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// fixme: why?
if (double.IsNaN(victimStats.SPM) || double.IsNaN(victimStats.Skill))
{
Log.WriteDebug($"[StatManager::AddStandardKill] victim SPM/SKILL {victimStats.SPM} {victimStats.Skill}");
_log.WriteDebug($"[StatManager::AddStandardKill] victim SPM/SKILL {victimStats.SPM} {victimStats.Skill}");
victimStats.SPM = 0.0;
victimStats.Skill = 0.0;
}
if (double.IsNaN(attackerStats.SPM) || double.IsNaN(attackerStats.Skill))
{
Log.WriteDebug($"[StatManager::AddStandardKill] attacker SPM/SKILL {victimStats.SPM} {victimStats.Skill}");
_log.WriteDebug($"[StatManager::AddStandardKill] attacker SPM/SKILL {victimStats.SPM} {victimStats.Skill}");
attackerStats.SPM = 0.0;
attackerStats.Skill = 0.0;
}
// update their performance
#if !DEBUG
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 2.5)
#else
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 0.1)
#endif
{
attackerStats.LastStatHistoryUpdate = DateTime.UtcNow;
await UpdateStatHistory(attacker, attackerStats);
}
// todo: do we want to save this immediately?
using (var ctx = new DatabaseContext(disableTracking: true))
{
var clientStatsSet = ctx.Set<EFClientStatistics>();
clientStatsSet.Attach(attackerStats).State = EntityState.Modified;
clientStatsSet.Attach(victimStats).State = EntityState.Modified;
await ctx.SaveChangesAsync();
}
}
/// <summary>
@ -822,12 +769,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds;
// don't update their stat history if they haven't played long
#if DEBUG == false
if (currentSessionTime < 60)
{
return;
}
#endif
int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime;
@ -857,11 +802,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
#region INDIVIDUAL_SERVER_PERFORMANCE
// get the client ranking for the current server
int individualClientRanking = await ctx.Set<EFRating>()
.Where(GetRankingFunc(clientStats.ServerId))
// ignore themselves in the query
.Where(c => c.RatingHistory.ClientId != client.ClientId)
.Where(c => c.Performance > clientStats.Performance)
.CountAsync() + 1;
.Where(GetRankingFunc(clientStats.ServerId))
// ignore themselves in the query
.Where(c => c.RatingHistory.ClientId != client.ClientId)
.Where(c => c.Performance > clientStats.Performance)
.CountAsync() + 1;
// limit max history per server to 40
if (clientHistory.Ratings.Count(r => r.ServerId == clientStats.ServerId) >= 40)
@ -1014,7 +959,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
attackerStats = UpdateStats(attackerStats);
// calulate elo
if (Servers[attackerStats.ServerId].PlayerStats.Count > 1)
if (_servers[attackerStats.ServerId].PlayerStats.Count > 1)
{
#region DEPRECATED
/* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats
@ -1095,7 +1040,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
double killSPM = scoreDifference / timeSinceLastCalc;
double spmMultiplier = 2.934 * Math.Pow(Servers[clientStats.ServerId].TeamCount(clientStats.Team == IW4Info.Team.Allies ? IW4Info.Team.Axis : IW4Info.Team.Allies), -0.454);
double spmMultiplier = 2.934 * Math.Pow(_servers[clientStats.ServerId].TeamCount(clientStats.Team == IW4Info.Team.Allies ? IW4Info.Team.Axis : IW4Info.Team.Allies), -0.454);
killSPM *= Math.Max(1, spmMultiplier);
// update this for ac tracking
@ -1120,8 +1065,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (clientStats.SPM < 0)
{
Log.WriteWarning("[StatManager:UpdateStats] clientStats SPM < 0");
Log.WriteDebug($"{scoreDifference}-{clientStats.RoundScore} - {clientStats.LastScore} - {clientStats.SessionScore}");
_log.WriteWarning("[StatManager:UpdateStats] clientStats SPM < 0");
_log.WriteDebug($"{scoreDifference}-{clientStats.RoundScore} - {clientStats.LastScore} - {clientStats.SessionScore}");
clientStats.SPM = 0;
}
@ -1131,8 +1076,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// fixme: how does this happen?
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
{
Log.WriteWarning("[StatManager::UpdateStats] clientStats SPM/Skill NaN");
Log.WriteDebug($"{killSPM}-{KDRWeight}-{totalPlayTime}-{SPMAgainstPlayWeight}-{clientStats.SPM}-{clientStats.Skill}-{scoreDifference}");
_log.WriteWarning("[StatManager::UpdateStats] clientStats SPM/Skill NaN");
_log.WriteDebug($"{killSPM}-{KDRWeight}-{totalPlayTime}-{SPMAgainstPlayWeight}-{clientStats.SPM}-{clientStats.Skill}-{scoreDifference}");
clientStats.SPM = 0;
clientStats.Skill = 0;
}
@ -1154,7 +1099,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (serverStats == null)
{
Log.WriteDebug($"Initializing server stats for {serverId}");
_log.WriteDebug($"Initializing server stats for {serverId}");
// server stats have never been generated before
serverStats = new EFServerStatistics()
{
@ -1173,7 +1118,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public void ResetKillstreaks(long serverId)
{
var serverStats = Servers[serverId];
var serverStats = _servers[serverId];
foreach (var stat in serverStats.PlayerStats.Values)
{
@ -1183,7 +1128,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public void ResetStats(int clientId, long serverId)
{
var stats = Servers[serverId].PlayerStats[clientId];
var stats = _servers[serverId].PlayerStats[clientId];
stats.Kills = 0;
stats.Deaths = 0;
stats.SPM = 0;
@ -1216,64 +1161,55 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public async Task Sync(Server sv)
{
long serverId = await GetIdForServer(sv);
long serverId = GetIdForServer(sv);
using (var ctx = new DatabaseContext(disableTracking: true))
{
var serverSet = ctx.Set<EFServer>();
serverSet.Update(Servers[serverId].Server);
serverSet.Update(_servers[serverId].Server);
var serverStatsSet = ctx.Set<EFServerStatistics>();
serverStatsSet.Update(Servers[serverId].ServerStatistics);
await ctx.SaveChangesAsync();
serverStatsSet.Update(_servers[serverId].ServerStatistics);
}
foreach (var client in sv.GetClientsAsList())
{
var stats = GetClientStats(client.ClientId, serverId);
if (stats != null)
{
await SaveClientStats(stats);
}
}
await SaveHitCache(serverId);
}
public void SetTeamBased(long serverId, bool isTeamBased)
{
Servers[serverId].IsTeamBased = isTeamBased;
_servers[serverId].IsTeamBased = isTeamBased;
}
public static async Task<long> GetIdForServer(Server server)
public static long GetIdForServer(Server server)
{
// hack: my laziness
if ($"{server.IP}:{server.GetPort().ToString()}" == "66.150.121.184:28965")
if ($"{server.IP}:{server.Port.ToString()}" == "66.150.121.184:28965")
{
return 886229536;
}
else if ($"{server.IP}:{server.GetPort().ToString()}" == "66.150.121.184:28960")
long id = HashCode.Combine(server.IP, server.Port);
id = id < 0 ? Math.Abs(id) : id;
long? serverId;
serverId = serverModels.FirstOrDefault(_server => _server.ServerId == server.EndPoint ||
_server.EndPoint == server.ToString() ||
_server.ServerId == id)?.ServerId;
if (!serverId.HasValue)
{
return 1645744423;
return id;
}
else if ($"{server.IP}:{server.GetPort().ToString()}" == "66.150.121.184:28970")
{
return 1645809959;
}
else
{
long id = HashCode.Combine(server.IP, server.GetPort());
id = id < 0 ? Math.Abs(id) : id;
long? serverId;
// todo: cache this eventually, as it shouldn't change
using (var ctx = new DatabaseContext(disableTracking: true))
{
serverId = (await ctx.Set<EFServer>().FirstOrDefaultAsync(_server => _server.ServerId == server.EndPoint ||
_server.EndPoint == server.ToString() ||
_server.ServerId == id))?.ServerId;
}
if (!serverId.HasValue)
{
return id;
}
return serverId.Value;
}
return serverId.Value;
}
}
}

View File

@ -1,70 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Plugins.Stats
{
public class MinimapInfo
{
public string MapName { get; set; }
// distance from the edge of the minimap image
// to the "playable" area
public int Top { get; set; }
public int Bottom { get; set; }
public int Left { get; set; }
public int Right { get; set; }
// maximum coordinate values for the map
public int MaxTop { get; set; }
public int MaxBottom { get; set; }
public int MaxLeft { get; set; }
public int MaxRight { get; set; }
public int Width => MaxLeft - MaxRight;
public int Height => MaxTop - MaxBottom;
}
public class MinimapConfig : Serialize<MinimapConfig>
{
public List<MinimapInfo> MapInfo;
public static MinimapConfig IW4Minimaps()
{
return new MinimapConfig()
{
MapInfo = new List<MinimapInfo>()
{
new MinimapInfo()
{
MapName = "mp_terminal",
Top = 85,
Bottom = 89,
Left = 7,
Right = 6,
MaxTop = 2929,
MaxBottom = -513,
MaxLeft = 7520,
MaxRight = 2447
},
new MinimapInfo()
{
MapName = "mp_rust",
Top = 122,
Bottom = 104,
Left = 155,
Right = 82,
MaxRight = -225,
MaxLeft = 1809,
MaxTop = 1641,
MaxBottom = -469
}
}
};
}
}
}

View File

@ -30,6 +30,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
public double CurrentStrain { get; set; }
public double StrainAngleBetween { get; set; }
public double SessionAngleOffset { get; set; }
public double RecoilOffset { get; set; }
public int LastStrainAngleId { get; set; }
[ForeignKey("LastStrainAngleId")]
public Vector3 LastStrainAngle { get; set; }

View File

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharedLibraryCore.Database.Models;
@ -12,6 +13,16 @@ namespace IW4MAdmin.Plugins.Stats.Models
{
public class EFClientStatistics : SharedEntity
{
public EFClientStatistics()
{
ProcessingHit = new SemaphoreSlim(1, 1);
}
~EFClientStatistics()
{
ProcessingHit.Dispose();
}
public int ClientId { get; set; }
[ForeignKey("ClientId")]
public virtual EFClient Client { get; set; }
@ -26,6 +37,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
public virtual ICollection<EFHitLocationCount> HitLocations { get; set; }
public double RollingWeightedKDR { get; set; }
public double VisionAverage { get; set; }
public double AverageRecoilOffset { get; set; }
[NotMapped]
public double Performance
{
@ -95,12 +107,14 @@ namespace IW4MAdmin.Plugins.Stats.Models
}
}
[NotMapped]
private List<int> SessionScores = new List<int>() { 0 };
private readonly List<int> SessionScores = new List<int>() { 0 };
[NotMapped]
public IW4Info.Team Team { get; set; }
[NotMapped]
public DateTime LastStatHistoryUpdate { get; set; } = DateTime.UtcNow;
[NotMapped]
public double SessionSPM { get; set; }
[NotMapped]
public SemaphoreSlim ProcessingHit { get; private set; }
}
}

View File

@ -21,13 +21,7 @@ namespace Stats.Models
.HasColumnName("EFClientStatistics_ServerId");
builder.Entity<EFRating>()
.HasIndex(p => p.Performance);
builder.Entity<EFRating>()
.HasIndex(p => p.Ranking);
builder.Entity<EFRating>()
.HasIndex(p => p.When);
.HasIndex(p => new { p.Performance, p.Ranking, p.When });
builder.Entity<EFClientMessage>()
.HasIndex(p => p.TimeSent);

View File

@ -29,6 +29,10 @@ namespace IW4MAdmin.Plugins.Stats
public static StatManager Manager { get; private set; }
public static IManager ServerManager;
public static BaseConfigurationHandler<StatsConfiguration> Config { get; private set; }
#if DEBUG
int scriptDamageCount;
int scriptKillCount;
#endif
public async Task OnEventAsync(GameEvent E, Server S)
{
@ -44,20 +48,21 @@ namespace IW4MAdmin.Plugins.Stats
break;
case GameEvent.EventType.Disconnect:
await Manager.RemovePlayer(E.Origin);
await Manager.Sync(S);
break;
case GameEvent.EventType.Say:
if (!string.IsNullOrEmpty(E.Data) &&
E.Origin.ClientId > 1)
{
await Manager.AddMessageAsync(E.Origin.ClientId, await StatManager.GetIdForServer(E.Owner), E.Data);
await Manager.AddMessageAsync(E.Origin.ClientId, StatManager.GetIdForServer(E.Owner), E.Data);
}
break;
case GameEvent.EventType.MapChange:
Manager.SetTeamBased(await StatManager.GetIdForServer(E.Owner), E.Owner.Gametype != "dm");
Manager.ResetKillstreaks(await StatManager.GetIdForServer(E.Owner));
Manager.SetTeamBased(StatManager.GetIdForServer(E.Owner), E.Owner.Gametype != "dm");
Manager.ResetKillstreaks(StatManager.GetIdForServer(E.Owner));
await Manager.Sync(E.Owner);
break;
case GameEvent.EventType.MapEnd:
await Manager.Sync(E.Owner);
break;
case GameEvent.EventType.JoinTeam:
break;
@ -77,37 +82,36 @@ namespace IW4MAdmin.Plugins.Stats
break;
case GameEvent.EventType.ScriptKill:
string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
if (killInfo.Length >= 14 && !ShouldIgnoreEvent(E.Origin, E.Target))
if (E.Owner.CustomCallback && killInfo.Length >= 14 && !ShouldIgnoreEvent(E.Origin, E.Target))
{
// this treats "world" damage as self damage
if (E.Origin.ClientId <= 1)
if (IsWorldDamage(E.Origin))
{
E.Origin = E.Target;
}
if (E.Target.ClientId <= 1)
{
E.Target = E.Origin;
}
#if DEBUG
scriptKillCount++;
S.Logger.WriteInfo($"Start ScriptKill {scriptKillCount}");
#endif
await Manager.AddScriptHit(false, E.Time, E.Origin, E.Target, await StatManager.GetIdForServer(E.Owner), S.CurrentMap.Name, killInfo[7], killInfo[8],
await Manager.AddScriptHit(false, E.Time, E.Origin, E.Target, StatManager.GetIdForServer(E.Owner), S.CurrentMap.Name, killInfo[7], killInfo[8],
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15]);
#if DEBUG
S.Logger.WriteInfo($"End ScriptKill {scriptKillCount}");
#endif
}
break;
case GameEvent.EventType.Kill:
if (!E.Owner.CustomCallback && !ShouldIgnoreEvent(E.Origin, E.Target))
{
// this treats "world" damage as self damage
if (E.Origin.ClientId <= 1)
if (IsWorldDamage(E.Origin))
{
E.Origin = E.Target;
}
if (E.Target.ClientId <= 1)
{
E.Target = E.Origin;
}
await Manager.AddStandardKill(E.Origin, E.Target);
}
break;
@ -115,36 +119,39 @@ namespace IW4MAdmin.Plugins.Stats
if (!E.Owner.CustomCallback && !ShouldIgnoreEvent(E.Origin, E.Target))
{
// this treats "world" damage as self damage
if (E.Origin.ClientId <= 1)
if (IsWorldDamage(E.Origin))
{
E.Origin = E.Target;
}
if (E.Target.ClientId <= 1)
{
E.Target = E.Origin;
}
Manager.AddDamageEvent(E.Data, E.Origin.ClientId, E.Target.ClientId, await StatManager.GetIdForServer(E.Owner));
Manager.AddDamageEvent(E.Data, E.Origin.ClientId, E.Target.ClientId, StatManager.GetIdForServer(E.Owner));
}
break;
case GameEvent.EventType.ScriptDamage:
killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
if (killInfo.Length >= 14 && !ShouldIgnoreEvent(E.Origin, E.Target))
if (E.Owner.CustomCallback && killInfo.Length >= 14 && !ShouldIgnoreEvent(E.Origin, E.Target))
{
// this treats "world" damage as self damage
if (E.Origin.ClientId <= 1)
if (IsWorldDamage(E.Origin))
{
E.Origin = E.Target;
}
if (E.Target.ClientId <= 1)
{
E.Target = E.Origin;
}
#if DEBUG
scriptDamageCount++;
S.Logger.WriteInfo($"Start ScriptDamage {scriptDamageCount}");
#endif
await Manager.AddScriptHit(true, E.Time, E.Origin, E.Target, await StatManager.GetIdForServer(E.Owner), S.CurrentMap.Name, killInfo[7], killInfo[8],
await Manager.AddScriptHit(true, E.Time, E.Origin, E.Target, StatManager.GetIdForServer(E.Owner), S.CurrentMap.Name, killInfo[7], killInfo[8],
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15]);
#if DEBUG
S.Logger.WriteInfo($"End ScriptDamage {scriptDamageCount}");
#endif
}
else
{
break;
}
break;
}
@ -264,6 +271,7 @@ namespace IW4MAdmin.Plugins.Stats
double abdomenRatio = 0;
double chestAbdomenRatio = 0;
double hitOffsetAverage = 0;
double averageRecoilAmount = 0;
double maxStrain = clientStats.Count(c => c.MaxStrain > 0) == 0 ? 0 : clientStats.Max(cs => cs.MaxStrain);
if (clientStats.Where(cs => cs.HitLocations.Count > 0).FirstOrDefault() != null)
@ -286,6 +294,7 @@ namespace IW4MAdmin.Plugins.Stats
var validOffsets = clientStats.Where(c => c.HitLocations.Count(hl => hl.HitCount > 0) > 0).SelectMany(hl => hl.HitLocations);
hitOffsetAverage = validOffsets.Sum(o => o.HitCount * o.HitOffsetAverage) / (double)validOffsets.Sum(o => o.HitCount);
averageRecoilAmount = clientStats.Average(_stat => _stat.AverageRecoilOffset);
}
return new List<ProfileMeta>()
@ -351,6 +360,16 @@ namespace IW4MAdmin.Plugins.Stats
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM6"],
Sensitive = true
},
new ProfileMeta()
{
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 7",
Value = Math.Round(averageRecoilAmount, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Type = ProfileMeta.MetaType.Information,
Column = 2,
Order = 5,
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM7"],
Sensitive = true
}
};
}
@ -491,7 +510,14 @@ namespace IW4MAdmin.Plugins.Stats
/// <returns></returns>
private bool ShouldIgnoreEvent(EFClient origin, EFClient target)
{
return ((origin.ClientId <= 1 && target.ClientId <= 1) || (target.IsBot || origin.IsBot) && ServerManager.GetApplicationSettings().Configuration().IgnoreBots);
return ((origin?.NetworkId <= 1 && target?.NetworkId <= 1) || (origin?.ClientId <= 1 && target?.ClientId <= 1));
}
/// <summary>
/// Indicates if the damage occurs from world (fall damage/certain killstreaks)
/// </summary>
/// <param name="origin"></param>
/// <returns></returns>
private bool IsWorldDamage(EFClient origin) => origin?.NetworkId == 1;
}
}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.Stats</PackageId>
@ -13,17 +13,18 @@
<Description>Client Statistics Plugin for IW4MAdmin</Description>
<Copyright>2018</Copyright>
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
<ProjectReference Include="..\..\WebfrontCore\WebfrontCore.csproj" />
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\..\WebfrontCore\WebfrontCore.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.2.2" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
</Target>

View File

@ -19,7 +19,7 @@ namespace Stats.ViewComponents
if (server != null)
{
serverId = await StatManager.GetIdForServer(server);
serverId = StatManager.GetIdForServer(server);
}
return View("_List", await Plugin.Manager.GetTopStats(offset, count, serverId));

View File

@ -2,12 +2,13 @@
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Objects;
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
@ -15,12 +16,12 @@ namespace Tests
[Collection("ManagerCollection")]
public class ClientTests
{
readonly ApplicationManager Manager;
private readonly ApplicationManager _manager;
const int TestTimeout = 10000;
public ClientTests(ManagerFixture fixture)
{
Manager = fixture.Manager;
_manager = fixture.Manager;
}
[Fact]
@ -42,38 +43,122 @@ namespace Tests
}
[Fact]
public void WarnClientShouldSucceed()
public void BanEvasionShouldLink()
{
while (!Manager.IsInitialized)
var server = _manager.Servers[0];
var waiter = new ManualResetEventSlim();
_manager.GetApplicationSettings().Configuration().RConPollRate = 5000;
while (!server.IsInitialized)
{
Thread.Sleep(100);
}
var client = Manager.Servers.First().GetClientsAsList().FirstOrDefault();
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
}
}
};
Assert.False(client == null, "no client found to warn");
_manager.GetEventHandler().AddEvent(e);
e.OnProcessed.Wait();
var warnEvent = client.Warn("test warn", new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
warnEvent.OnProcessed.Wait();
e = new GameEvent()
{
Type = GameEvent.EventType.PreConnect,
Owner = server,
Origin = new EFClient()
{
NetworkId = 1338,
ClientNumber = 1,
CurrentAlias = new EFAlias()
{
Name = "Ban Me",
IPAddress = null
}
}
};
//Assert.True((client.Warnings == 1 ||
// warnEvent.Failed) &&
// Manager.GetPenaltyService().GetClientPenaltiesAsync(client.ClientId).Result.Count(p => p.Type == Penalty.PenaltyType.Warning) == 1,
// "warning did not get applied");
_manager.GetEventHandler().AddEvent(e);
e.OnProcessed.Wait();
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.GetEventHandler().AddEvent(e);
e.OnProcessed.Wait();
}
[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.OnProcessed.Wait(5000);
Assert.False(warnEvent.Failed);
warnEvent = client.Warn("test warn", new EFClient() { ClientId = 1, Level = EFClient.Permission.Banned, CurrentServer = client.CurrentServer });
warnEvent.OnProcessed.Wait();
warnEvent.OnProcessed.Wait(5000);
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.OnProcessed.Wait(5000);
Assert.True(warnClearEvent.FailReason == GameEvent.EventFailReason.Permission &&
client.Warnings == 1, "warning was removed without proper permissions");
warnClearEvent = client.WarnClear(new EFClient { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
warnClearEvent = client.WarnClear(Utilities.IW4MAdminClient(server));
warnClearEvent.OnProcessed.Wait(5000);
Assert.True(!warnClearEvent.Failed && client.Warnings == 0, "warning was not cleared");
}
@ -81,12 +166,12 @@ namespace Tests
[Fact]
public void ReportClientShouldSucceed()
{
while (!Manager.IsInitialized)
while (!_manager.IsInitialized)
{
Thread.Sleep(100);
}
var client = Manager.Servers.First().GetClientsAsList().FirstOrDefault();
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
Assert.False(client == null, "no client found to report");
// fail
@ -128,12 +213,12 @@ namespace Tests
[Fact]
public void FlagClientShouldSucceed()
{
while (!Manager.IsInitialized)
while (!_manager.IsInitialized)
{
Thread.Sleep(100);
}
var client = Manager.Servers.First().GetClientsAsList().FirstOrDefault();
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 });
@ -178,12 +263,12 @@ namespace Tests
[Fact]
void KickClientShouldSucceed()
{
while (!Manager.IsInitialized)
while (!_manager.IsInitialized)
{
Thread.Sleep(100);
}
var client = Manager.Servers.First().GetClientsAsList().FirstOrDefault();
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 });
@ -194,18 +279,18 @@ namespace Tests
kickEvent = client.Kick("test kick", new EFClient() { ClientId = 1, Level = EFClient.Permission.Console, CurrentServer = client.CurrentServer });
kickEvent.OnProcessed.Wait();
Assert.True(Manager.Servers.First().GetClientsAsList().FirstOrDefault(c => c.NetworkId == client.NetworkId) == null, "client was not kicked");
Assert.True(_manager.Servers.First().GetClientsAsList().FirstOrDefault(c => c.NetworkId == client.NetworkId) == null, "client was not kicked");
}
[Fact]
void TempBanClientShouldSucceed()
{
while (!Manager.IsInitialized)
while (!_manager.IsInitialized)
{
Thread.Sleep(100);
}
var client = Manager.Servers.First().GetClientsAsList().FirstOrDefault();
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
Assert.False(client == null, "no client found to tempban");
var tbCommand = new CTempBan();
@ -218,19 +303,19 @@ namespace Tests
Owner = client.CurrentServer
}).Wait();
Assert.True(Manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId).Result.Count(p => p.Type == Penalty.PenaltyType.TempBan) == 1,
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)
while (!_manager.IsInitialized)
{
Thread.Sleep(100);
}
var client = Manager.Servers.First().GetClientsAsList().FirstOrDefault();
var client = _manager.Servers.First().GetClientsAsList().FirstOrDefault();
Assert.False(client == null, "no client found to ban");
var banCommand = new CBan();
@ -243,20 +328,20 @@ namespace Tests
Owner = client.CurrentServer
}).Wait();
Assert.True(Manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId).Result.Count(p => p.Type == Penalty.PenaltyType.Ban) == 1,
Assert.True(_manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId).Result.Count(p => p.Type == EFPenalty.PenaltyType.Ban) == 1,
"ban was not added");
var unbanCommand = new CUnban();
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(),
//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 == Penalty.PenaltyType.Ban) == 0,
Assert.True(_manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId).Result.Count(p => p.Type == EFPenalty.PenaltyType.Ban) == 0,
"ban was not removed");
}

View File

@ -16,9 +16,9 @@ namespace Tests
public ManagerFixture()
{
File.WriteAllText("test_mp.log", "test_log_file");
string logFile = @"X:\IW4MAdmin\Plugins\Tests\bin\Debug\netcoreapp2.2\test_mp.log";
//IW4MAdmin.Application.Localization.Configure.Initialize("en-US");
File.WriteAllText(logFile, Environment.NewLine);
Manager = ApplicationManager.GetInstance();
@ -31,18 +31,22 @@ namespace Tests
AutoMessages = new List<string>(),
IPAddress = "127.0.0.1",
Password = "test",
Port = 28963,
Port = 28960,
Rules = new List<string>(),
ManualLogPath = "http://google.com"
RConParserVersion = "test",
EventParserVersion = "IW4x (v0.6.0)",
ManualLogPath = logFile
}
},
AutoMessages = new List<string>(),
GlobalRules = new List<string>(),
Maps = new List<MapConfiguration>(),
RConPollRate = 10000
RConPollRate = int.MaxValue
};
Manager.ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("test");
Manager.ConfigHandler.Set(config);
Manager.AdditionalRConParsers.Add(new TestRconParser());
Manager.Init().Wait();

View File

@ -1,8 +1,6 @@
using IW4MAdmin.Application;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -34,162 +32,17 @@ namespace Tests
[Fact]
public void AreCommandAliasesUnique()
{
var mgr = Program.ServerManager;
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 AddAndRemoveClientsViaJoinShouldSucceed()
{
var server = Manager.GetServers().First();
var waiters = new Queue<GameEvent>();
int clientStartIndex = 4;
int clientNum = 10;
for (int i = clientStartIndex; i < clientStartIndex + clientNum; i++)
{
var e = new GameEvent()
{
Type = GameEvent.EventType.Join,
Origin = new EFClient()
{
Name = $"Player{i}",
NetworkId = i,
ClientNumber = i - 1
},
Owner = server
};
server.Manager.GetEventHandler().AddEvent(e);
waiters.Enqueue(e);
}
while (waiters.Count > 0)
{
waiters.Dequeue().OnProcessed.Wait();
}
Assert.True(server.ClientNum == clientNum, $"client num does not match added client num [{server.ClientNum}:{clientNum}]");
for (int i = clientStartIndex; i < clientStartIndex + clientNum; i++)
{
var e = new GameEvent()
{
Type = GameEvent.EventType.Disconnect,
Origin = new EFClient()
{
Name = $"Player{i}",
NetworkId = i,
ClientNumber = i - 1
},
Owner = server
};
server.Manager.GetEventHandler().AddEvent(e);
waiters.Enqueue(e);
}
while (waiters.Count > 0)
{
waiters.Dequeue().OnProcessed.Wait();
}
Assert.True(server.ClientNum == 0, "there are still clients connected");
}
[Fact]
public void AddAndRemoveClientsViaRconShouldSucceed()
{
var server = Manager.GetServers().First();
var waiters = new Queue<GameEvent>();
int clientIndexStart = 1;
int clientNum = 8;
for (int i = clientIndexStart; i < clientNum + clientIndexStart; i++)
{
var e = new GameEvent()
{
Type = GameEvent.EventType.Connect,
Origin = new EFClient()
{
Name = $"Player{i}",
NetworkId = i,
ClientNumber = i - 1,
IPAddress = i,
Ping = 50,
CurrentServer = server
},
Owner = server,
};
Manager.GetEventHandler().AddEvent(e);
waiters.Enqueue(e);
}
while (waiters.Count > 0)
{
waiters.Dequeue().OnProcessed.Wait();
}
int actualClientNum = server.GetClientsAsList().Count(p => p.State == EFClient.ClientState.Connected);
Assert.True(actualClientNum == clientNum, $"client connected states don't match [{actualClientNum}:{clientNum}");
for (int i = clientIndexStart; i < clientNum + clientIndexStart; i++)
{
var e = new GameEvent()
{
Type = GameEvent.EventType.Disconnect,
Origin = new EFClient()
{
Name = $"Player{i}",
NetworkId = i,
ClientNumber = i - 1,
IPAddress = i,
Ping = 50,
CurrentServer = server
},
Owner = server,
};
Manager.GetEventHandler().AddEvent(e);
waiters.Enqueue(e);
}
while (waiters.Count > 0)
{
waiters.Dequeue().OnProcessed.Wait();
}
actualClientNum = server.ClientNum;
Assert.True(actualClientNum == 0, "there are clients still connected");
}
[Fact]
public void AddClientViaLog()
{
var resetEvent = new ManualResetEventSlim();
resetEvent.Reset();
Manager.OnServerEvent += (sender, eventArgs) =>
{
if (eventArgs.Event.Type == GameEvent.EventType.Join)
{
eventArgs.Event.OnProcessed.Wait();
Assert.True(false);
}
};
File.AppendAllText("test_mp.log", " 2:33 J;224b3d0bc64ab4f9;0;goober");
resetEvent.Wait(5000);
}
[Fact]
public void PrintCommands()
{

View File

@ -1,7 +1,6 @@
using IW4MAdmin.Application;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Objects;
using Xunit;
namespace Tests

View File

@ -1,10 +1,113 @@
using System;
using IW4MAdmin.Application;
using SharedLibraryCore;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using Xunit;
namespace Tests
{
class ServerTests
[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.OnProcessed.Wait();
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.OnProcessed.Wait();
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.OnProcessed.Wait();
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.OnProcessed.Wait();
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;
}
}
}

View File

@ -0,0 +1,24 @@
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");
}
}
}

View File

@ -0,0 +1,38 @@
using SharedLibraryCore.Database.Models;
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 int FakeClientCount { get; set; }
public List<EFClient> FakeClients { get; set; } = new List<EFClient>();
public override string Version => "test";
public override async Task<List<EFClient>> GetStatusAsync(Connection 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 : FakeClients;
}
}
}

View File

@ -3,9 +3,10 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<TargetLatestRuntimePatch >true</TargetLatestRuntimePatch>
<ApplicationIcon />
<StartupObject />
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -13,7 +14,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
@ -25,9 +25,4 @@
<ProjectReference Include="..\..\Application\Application.csproj" />
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.2.2" />
</ItemGroup>
</Project>

View File

@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetLatestRuntimePatch >true</TargetLatestRuntimePatch>
<RazorCompileOnBuild Condition="'$(CONFIG)'!='Debug'">true</RazorCompileOnBuild>
<RazorCompiledOnPublish Condition="'$(CONFIG)'!='Debug'">true</RazorCompiledOnPublish>
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
@ -11,7 +13,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Stats\Stats.csproj" />
<ProjectReference Include="..\..\Stats\Stats.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>

View File

@ -3,7 +3,6 @@ using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Services;
using SharedLibraryCore.Database.Models;
@ -99,7 +98,7 @@ namespace IW4MAdmin.Plugins.Welcome
using (var ctx = new DatabaseContext(disableTracking: true))
{
penaltyReason = await ctx.Penalties
.Where(p => p.OffenderId == newPlayer.ClientId && p.Type == Penalty.PenaltyType.Flag)
.Where(p => p.OffenderId == newPlayer.ClientId && p.Type == EFPenalty.PenaltyType.Flag)
.OrderByDescending(p => p.When)
.Select(p => p.AutomatedOffense ?? p.Offense)
.FirstOrDefaultAsync();

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.Welcome</PackageId>
@ -13,14 +13,13 @@
<Description>Welcome plugin for IW4MAdmin welcomes clients to the server</Description>
<Copyright>2018</Copyright>
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.2.2" />
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

100
README.md
View File

@ -1,9 +1,10 @@
# IW4MAdmin
### Quick Start Guide
### Version 2.2
### Version 2.3
_______
### About
**IW4MAdmin** is an administration tool for [IW4x](https://iw4xcachep26muba.onion.link/), [Pluto T6](https://forum.plutonium.pw/category/33/plutonium-t6), ~~[Pluto IW5](https://forum.plutonium.pw/category/5/plutonium-iw5)~~, and most Call of Duty<EFBFBD> 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** is an administration tool for [IW4x](https://iw4xcachep26muba.onion.link/), [Pluto T6](https://forum.plutonium.pw/category/33/plutonium-t6), [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.
### Download
Latest binary builds are always available at:
- [RaidMax](https://raidmax.org/IW4MAdmin)
@ -13,17 +14,26 @@ Latest binary builds are always available at:
### Setup
**IW4MAdmin** requires minimal effort to get up and running.
#### Prerequisites
* [.NET Core 2.1.5 Runtime](https://www.microsoft.com/net/download) *or newer*
* [.NET Core 2.2.2 Runtime](https://www.microsoft.com/net/download) *or newer*
#### Installation
1. Install .NET Core Runtime
2. Extract `IW4MAdmin-<version>.zip`
#### Launching
1. Run `StartIW4MAdmin.cmd` (Windows)
2. Run `StartIW4MAdmin.sh` (Linux)
Windows
1. Run `StartIW4MAdmin.cmd`
2. Configure **IW4MAdmin**
Linux
1. Execute `chmod +x StartIW4MAdmin.sh`
2. Run `StartIW4MAdmin.sh`
3. Configure **IW4MAdmin**
### Updating
1. Extract newer version of **IW4MAdmin** into pre-existing **IW4MAdmin** folder and overwrite existing files
- _Your configuration and database will be saved_
1. Download the latest version of **IW4MAdmin**
2. Extract the newer version of **IW4MAdmin** into pre-existing **IW4MAdmin** folder and overwrite existing files
_Your configuration and database will be saved_
---
### Help
Feel free to join the **IW4MAdmin** [Discord](https://discord.gg/ZZFK5p3)
@ -75,29 +85,22 @@ After initial configuration is finished, you will be prompted to configure your
* The *\(R\)emote (Con)sole* password set in your server configuration (can be obtained via `rcon_password`)
* Default &mdash; `n/a`
`Use Pluto T6 parser`
* Used if setting up a server for Plutonium T6 (BO2)
* Default &mdash; `false`
`Use Pluto IW5 parser`
* Used if setting a server for Plutonium IW5 (MW3)
* Default &mdash; `false`
`Enter number of reserved slots`
* The number of client slots reserver for privileged players (unavailable for regular users to occupy)
* 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 &mdash; `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 it before running **IW4MAdmin**
#### `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 &mdash; `http://0.0.0.0:1624`
* Default &mdash; `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
@ -114,7 +117,7 @@ If you wish to further customize your experience of **IW4MAdmin**, the following
* Default &mdash; `sqlite`
`Ignore Bots`
* Disables bots from being registered by **IW4MAdmin**
* Disables bots from being registered and tracked by **IW4MAdmin**
`RConPollRate`
* Specifies (in milliseconds) how often to poll each server for updates
@ -136,15 +139,19 @@ If you wish to further customize your experience of **IW4MAdmin**, the following
* 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 &mdash; http://gamelogserver.com/
* Default &mdash; `null`
* `AutoMessages`
* Specifies the list of messages that are broadcasted to the particular server
* Default &mdash; `null`
* Default &mdash; `[]`
* `Rules`
* Specifies the list of rules that apply to the particular server
* Default &mdash; `null`
* Default &mdash; `[]`
* `ReservedSlotNumber`
* Specifies the number of client slots to reserve for privileged users
* Default &mdash; `0`
* `GameLogServerUrl`
* Specifies the HTTP Url for the Game Log Server
* Default &mdash; `null`
`AutoMessagePeriod`
* Specifies (in seconds) how often messages should be broadcasted to each server
@ -309,7 +316,7 @@ ___
#### 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 ClientIds to `vpnExceptionIds` will prevent a client from being kicked.
- 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
@ -359,6 +366,10 @@ Werkzeug>=0.14.1
```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)
@ -367,14 +378,20 @@ With Python 3 installed, open a terminal/command prompt window open in the `Game
```console
python runserver.py
```
The Game Log Server window will need to remain running/open as long as **IW4MAdmin** is running
---
### 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 functionality can be extended by writing additional plugins in C#.
**IW4MAdmin's** functionality can be extended by writing additional plugins in C#.
Each class library must implement the `IPlugin` interface.
See the existing plugins for examples.
See the existing [plugins](https://github.com/RaidMax/IW4M-Admin/tree/master/Plugins) for examples.
#### JavaScript
IW4MAdmin functionality can be extended using 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.
@ -407,38 +424,11 @@ var plugin = {
- `onLoadAsync` &mdash; [function] Handler executed when the plugin is loaded by code
- `manager` &mdash; [parameter object] Object reference to the application manager (see the IManager interface definition)
- `onUnloadAsync` &mdash; [function] Handler executed when the plugin is unloaded by code (see live reloading)
- `onTickAsync` &mdash; [function] Handler executed approximately once per second by code *(unimplemented as of version 2.1)*
- `onTickAsync` &mdash; [function] Handler executed approximately once per second by code *(unimplemented as of version 2.\*)*
- `server` &mdash; [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.
---
### Discord Webhook
If you'd like to receive notifications on your Discord guild, configure and start `DiscordWebhook.py`
#### Requirements
- [Python 3.6](https://www.python.org/downloads/) or newer
- The following [PIP](https://pypi.org/project/pip/) packages (provided in `requirements.txt`)
```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
```
#### Configuration Options
- `IW4MAdminUrl` &mdash; Base url corresponding to your IW4MAdmin `WebfrontBindUrl`.
Example http://127.0.0.1
- `DiscordWebhookNotificationUrl` &mdash; [required] Discord generated URL to send notifications/alerts to; this includes **Reports** and **Bans**
Example https://discordapp.com/api/webhooks/id/token
- `DiscordWebhookInformationUrl` &mdash; [optional] Discord generated URL to send information to; this includes information such as player messages
- `NotifyRoleIds` &mdash; [optional] List of [discord role ids](https://discordhelp.net/role-id) to mention when notification hook is sent
#### Launching
With Python installed, open a terminal/command prompt window open in the `Webhook` folder and execute:
```console
python DiscordWebhook.py
```
---
### Misc
#### Anti-cheat
@ -453,4 +443,4 @@ 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.
will allow **IW4MAdmin** to use alternate methods for database storage

View File

@ -1,9 +1,7 @@
dotnet publish WebfrontCore/WebfrontCore.csproj -c Prerelease -o X:\IW4MAdmin\Publish\WindowsPrerelease /p:PublishProfile=Prerelease
dotnet publish Application/Application.csproj -c Prerelease -o X:\IW4MAdmin\Publish\WindowsPrerelease /p:PublishProfile=Prerelease
dotnet publish GameLogServer/GameLogServer.pyproj -c Release -o X:\IW4MAdmin\Publish\WindowsPrerelease\GameLogServer
::dotnet publish GameLogServer/DiscordWebhook.pyproj -c Release -o X:\IW4MAdmin\Publish\WindowsPrerelease\DiscordWebhook
dotnet publish WebfrontCore/WebfrontCore.csproj -c Prerelease -f netcoreapp2.2 --force -o X:\IW4MAdmin\Publish\WindowsPrerelease /p:PublishProfile=Prerelease
dotnet publish Application/Application.csproj -c Prerelease -f netcoreapp2.2 --force -o X:\IW4MAdmin\Publish\WindowsPrerelease /p:PublishProfile=Prerelease
dotnet publish GameLogServer/GameLogServer.pyproj -c Release -f netcoreapp2.2 --force -o X:\IW4MAdmin\Publish\WindowsPrerelease\GameLogServer
call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\Tools\VsDevCmd.bat"
msbuild GameLogServer/GameLogServer.pyproj /p:PublishProfile=PreRelease /p:DeployOnBuild=true /p:PublishProfileRootFolder=X:\IW4MAdmin\GameLogServer\
msbuild DiscordWebhook/DiscordWebhook.pyproj /p:PublishProfile=PreRelease /p:DeployOnBuild=true /p:PublishProfileRootFolder=X:\IW4MAdmin\DiscordWebhook\
cd "X:\IW4MAdmin\DEPLOY\"
PowerShell ".\upload_prerelease.ps1"

View File

@ -3,6 +3,5 @@ dotnet publish Application/Application.csproj -c Release -o X:\IW4MAdmin\Publish
dotnet publish GameLogServer/GameLogServer.pyproj -c Release -o X:\IW4MAdmin\Publish\Windows\GameLogServer
call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\Tools\VsDevCmd.bat"
msbuild GameLogServer/GameLogServer.pyproj /p:PublishProfile=Stable /p:DeployOnBuild=true /p:PublishProfileRootFolder=X:\IW4MAdmin\GameLogServer\
msbuild DiscordWebhook/DiscordWebhook.pyproj /p:PublishProfile=Stable /p:DeployOnBuild=true /p:PublishProfileRootFolder=X:\IW4MAdmin\DiscordWebhook\
cd "X:\IW4MAdmin\DEPLOY\"
PowerShell ".\upload_release.ps1"

View File

@ -2,7 +2,6 @@
using System.Linq;
using System.Threading.Tasks;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Objects;
namespace SharedLibraryCore
{

View File

@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Services;
using System;
using System.Collections.Generic;
@ -11,6 +11,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using static SharedLibraryCore.Database.Models.EFClient;
namespace SharedLibraryCore.Commands
{
@ -22,7 +23,23 @@ namespace SharedLibraryCore.Commands
public override Task ExecuteAsync(GameEvent E)
{
return Task.Run(() => { E.Owner.Manager.Stop(); });
E.Owner.Manager.Stop();
return Task.CompletedTask;
}
}
public class CRestart : Command
{
public CRestart() :
base("restart", Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_DESC"], "res", EFClient.Permission.Owner, false)
{ }
public override Task ExecuteAsync(GameEvent E)
{
MetaService.Clear();
E.Owner.Manager.Restart();
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_SUCCESS"]);
return Task.CompletedTask;
}
}
@ -119,9 +136,18 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E)
{
var _ = !(await E.Target.Kick(E.Data, E.Origin).WaitAsync()).Failed ?
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_KICK_SUCCESS"].FormatExt(E.Target.Name)) :
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_KICK_FAIL"].FormatExt(E.Target.Name));
switch ((await E.Target.Kick(E.Data, E.Origin).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason)
{
case GameEvent.EventFailReason.None:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_KICK_SUCCESS"].FormatExt(E.Target.Name));
break;
case GameEvent.EventFailReason.Exception:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_INGAME"]);
break;
default:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_KICK_FAIL"].FormatExt(E.Target.Name));
break;
}
}
}
@ -185,9 +211,18 @@ namespace SharedLibraryCore.Commands
else
{
var _ = !(await E.Target.TempBan(tempbanReason, length, E.Origin).WaitAsync()).Failed ?
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_TEMPBAN_SUCCESS"].FormatExt(E.Target, length.TimeSpanText())) :
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_TEMPBAN_FAIL"].FormatExt(E.Target.Name));
switch ((await E.Target.TempBan(tempbanReason, length, E.Origin).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason)
{
case GameEvent.EventFailReason.None:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_TEMPBAN_SUCCESS"].FormatExt(E.Target, length.TimeSpanText()));
break;
case GameEvent.EventFailReason.Exception:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_INGAME"]);
break;
default:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_TEMPBAN_FAIL"].FormatExt(E.Target.Name));
break;
}
}
}
}
@ -213,9 +248,18 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E)
{
var _ = !(await E.Target.Ban(E.Data, E.Origin, false).WaitAsync()).Failed ?
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BAN_SUCCESS"].FormatExt(E.Target.Name)) :
switch ((await E.Target.Ban(E.Data, E.Origin, false).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason)
{
case GameEvent.EventFailReason.None:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BAN_SUCCESS"].FormatExt(E.Target.Name));
break;
case GameEvent.EventFailReason.Exception:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_INGAME"]);
break;
default:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BAN_FAIL"].FormatExt(E.Target.Name));
break;
}
}
}
@ -239,12 +283,21 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E)
{
// todo: don't do the lookup here
var penalties = await E.Owner.Manager.GetPenaltyService().GetActivePenaltiesAsync(E.Target.AliasLinkId);
if (penalties.Where(p => p.Type == Penalty.PenaltyType.Ban || p.Type == Penalty.PenaltyType.TempBan).FirstOrDefault() != null)
if (penalties.Where(p => p.Type == EFPenalty.PenaltyType.Ban || p.Type == EFPenalty.PenaltyType.TempBan).FirstOrDefault() != null)
{
await E.Target.Unban(E.Data, E.Origin).WaitAsync();
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_UNBAN_SUCCESS"].FormatExt(E.Target));
switch ((await E.Target.Unban(E.Data, E.Origin).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason)
{
case GameEvent.EventFailReason.None:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_UNBAN_SUCCESS"].FormatExt(E.Target));
break;
default:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_INGAME"]);
break;
}
}
else
{
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_UNBAN_FAIL"].FormatExt(E.Target));
@ -434,44 +487,45 @@ namespace SharedLibraryCore.Commands
})
{ }
public override Task ExecuteAsync(GameEvent E)
public override async Task ExecuteAsync(GameEvent E)
{
EFClient.Permission oldPerm = E.Target.Level;
EFClient.Permission newPerm = Utilities.MatchPermission(E.Data);
Permission oldPerm = E.Target.Level;
Permission newPerm = Utilities.MatchPermission(E.Data);
if (E.Target == E.Origin)
{
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_SETLEVEL_SELF"]);
return Task.CompletedTask;
return;
}
else if (newPerm == EFClient.Permission.Owner &&
!E.Owner.Manager.GetApplicationSettings().Configuration().EnableMultipleOwners)
else if (newPerm == Permission.Owner &&
!E.Owner.Manager.GetApplicationSettings().Configuration().EnableMultipleOwners &&
await E.Owner.Manager.GetClientService().GetOwnerCount() > 0)
{
// only one owner is allowed
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_SETLEVEL_OWNER"]);
return Task.CompletedTask;
return;
}
else if (E.Origin.Level < EFClient.Permission.Owner &&
else if (E.Origin.Level < Permission.Owner &&
!E.Owner.Manager.GetApplicationSettings().Configuration().EnableSteppedHierarchy)
{
// only the owner is allowed to set levels
E.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_SETLEVEL_STEPPEDDISABLED"]} ^5{E.Target.Name}");
return Task.CompletedTask;
return;
}
else if (E.Origin.Level <= newPerm &&
E.Origin.Level < EFClient.Permission.Owner)
else if ((E.Origin.Level <= newPerm &&
E.Origin.Level < Permission.Owner) ||
E.Origin.Level == newPerm)
{
// can't promote a client to higher than your current perms
// or your peer
E.Origin.Tell(string.Format(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_SETLEVEL_LEVELTOOHIGH"], E.Target.Name, (E.Origin.Level - 1).ToString()));
return Task.CompletedTask;
return;
}
else if (newPerm > EFClient.Permission.Banned)
else if (newPerm > Permission.Banned)
{
var ActiveClient = E.Owner.Manager.GetActiveClients()
.FirstOrDefault(p => p.NetworkId == E.Target.NetworkId);
@ -503,8 +557,6 @@ namespace SharedLibraryCore.Commands
{
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_SETLEVEL_FAIL"]);
}
return Task.CompletedTask;
}
}
@ -586,12 +638,13 @@ namespace SharedLibraryCore.Commands
if (m.Name.ToLower() == newMap || m.Alias.ToLower() == newMap)
{
E.Owner.Broadcast(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_MAP_SUCCESS"].FormatExt(m.Alias));
await Task.Delay(5000);
await Task.Delay((int)(Utilities.DefaultCommandTimeout.TotalMilliseconds / 2.0));
await E.Owner.LoadMap(m.Name);
return;
}
}
// todo: this can be moved into a single statement
E.Owner.Broadcast(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_MAP_UKN"].FormatExt(newMap));
await Task.Delay(5000);
await E.Owner.LoadMap(newMap);
@ -619,10 +672,7 @@ namespace SharedLibraryCore.Commands
return;
}
IList<EFClient> db_players = (await (E.Owner.Manager.GetClientService() as ClientService)
.FindClientsByIdentifier(E.Data))
.OrderByDescending(p => p.LastConnection)
.ToList();
var db_players = (await (E.Owner.Manager.GetClientService() as ClientService).FindClientsByIdentifier(E.Data));
if (db_players.Count == 0)
{
@ -632,11 +682,11 @@ namespace SharedLibraryCore.Commands
foreach (var P in db_players)
{
string localizedLevel = Utilities.CurrentLocalization.LocalizationIndex[$"GLOBAL_PERMISSION_{P.Level.ToString().ToUpper()}"];
// 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(P.Level, localizedLevel)}^7] - {P.IPAddressString} | last seen {Utilities.GetTimePassed(P.LastConnection)}" :
$"({P.AliasLink.Children.FirstOrDefault(a => a.Name.ToLower().Contains(E.Data.ToLower()))?.Name})->[^3{P.Name}^7] [^3@{P.ClientId}^7] - [{ Utilities.ConvertLevelToColor(P.Level, localizedLevel)}^7] - {P.IPAddressString} | 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)}" :
$"()->[^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);
}
}
@ -722,27 +772,23 @@ namespace SharedLibraryCore.Commands
})
{ }
public override Task ExecuteAsync(GameEvent E)
public override async Task ExecuteAsync(GameEvent E)
{
var flagEvent = E.Target.Flag(E.Data, E.Origin);
if (E.FailReason == GameEvent.EventFailReason.Permission)
switch ((await E.Target.Flag(E.Data, E.Origin).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason)
{
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_FLAG_FAIL"].FormatExt(E.Target.Name));
case GameEvent.EventFailReason.Permission:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_FLAG_FAIL"].FormatExt(E.Target.Name));
break;
case GameEvent.EventFailReason.Invalid:
E.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_FLAG_ALREADYFLAGGED"]}");
break;
case GameEvent.EventFailReason.None:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_FLAG_SUCCESS"].FormatExt(E.Target.Name));
break;
default:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_INGAME"]);
break;
}
else if (E.FailReason == GameEvent.EventFailReason.Invalid)
{
E.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_FLAG_ALREADYFLAGGED"]}");
}
else
{
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_FLAG_SUCCESS"].FormatExt(E.Target.Name));
}
return Task.CompletedTask;
}
}
@ -759,27 +805,23 @@ namespace SharedLibraryCore.Commands
})
{ }
public override Task ExecuteAsync(GameEvent E)
public override async Task ExecuteAsync(GameEvent E)
{
var unflagEvent = E.Target.Unflag(E.Data, E.Origin);
if (unflagEvent.FailReason == GameEvent.EventFailReason.Permission)
switch ((await E.Target.Unflag(E.Data, E.Origin).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason)
{
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_UNFLAG_FAIL"].FormatExt(E.Target.Name));
case GameEvent.EventFailReason.None:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_FLAG_UNFLAG"].FormatExt(E.Target.Name));
break;
case GameEvent.EventFailReason.Permission:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_UNFLAG_FAIL"].FormatExt(E.Target.Name));
break;
case GameEvent.EventFailReason.Invalid:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_UNFLAG_NOTFLAGGED"]);
break;
default:
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_INGAME"]);
break;
}
else if (unflagEvent.FailReason == GameEvent.EventFailReason.Invalid)
{
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_UNFLAG_NOTFLAGGED"]);
}
else
{
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_FLAG_UNFLAG"].FormatExt(E.Target.Name));
}
return Task.CompletedTask;
// todo: update immediately?
}
}
@ -810,46 +852,30 @@ namespace SharedLibraryCore.Commands
return;
}
var reportEvent = commandEvent.Target.Report(commandEvent.Data, commandEvent.Origin);
bool success = false;
if (reportEvent.FailReason == GameEvent.EventFailReason.Permission)
switch ((await commandEvent.Target.Report(commandEvent.Data, commandEvent.Origin).WaitAsync(Utilities.DefaultCommandTimeout, commandEvent.Owner.Manager.CancellationToken)).FailReason)
{
commandEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_REPORT_FAIL"].FormatExt(commandEvent.Target.Name));
case GameEvent.EventFailReason.None:
commandEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_REPORT_SUCCESS"]);
success = true;
break;
case GameEvent.EventFailReason.Exception:
commandEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_REPORT_FAIL_DUPLICATE"]);
break;
case GameEvent.EventFailReason.Permission:
commandEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_REPORT_FAIL"].FormatExt(commandEvent.Target.Name));
break;
case GameEvent.EventFailReason.Invalid:
commandEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_REPORT_FAIL_SELF"]);
break;
case GameEvent.EventFailReason.Throttle:
commandEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_REPORT_FAIL_TOOMANY"]);
break;
}
else if (reportEvent.FailReason == GameEvent.EventFailReason.Invalid)
if (success)
{
commandEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_REPORT_FAIL_SELF"]);
}
else if (reportEvent.FailReason == GameEvent.EventFailReason.Throttle)
{
commandEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_REPORT_FAIL_TOOMANY"]);
}
else if (reportEvent.Failed)
{
commandEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_REPORT_FAIL_DUPLICATE"]);
}
else
{
// todo: move into server
Penalty newReport = new Penalty()
{
Type = Penalty.PenaltyType.Report,
Expires = DateTime.UtcNow,
Offender = commandEvent.Target,
Offense = commandEvent.Data,
Punisher = commandEvent.Origin,
Active = true,
When = DateTime.UtcNow,
Link = commandEvent.Target.AliasLink
};
await commandEvent.Owner.Manager.GetPenaltyService().Create(newReport);
commandEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_REPORT_SUCCESS"]);
commandEvent.Owner.ToAdmins(String.Format("^5{0}^7->^1{1}^7: {2}", commandEvent.Origin.Name, commandEvent.Target.Name, commandEvent.Data));
}
}
@ -931,7 +957,7 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E)
{
var existingPenalties = await E.Owner.Manager.GetPenaltyService().GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.IPAddress);
var penalty = existingPenalties.FirstOrDefault(b => b.Type > Penalty.PenaltyType.Kick);
var penalty = existingPenalties.FirstOrDefault(b => b.Type > EFPenalty.PenaltyType.Kick);
if (penalty == null)
{
@ -939,7 +965,7 @@ namespace SharedLibraryCore.Commands
return;
}
if (penalty.Type == Penalty.PenaltyType.Ban)
if (penalty.Type == EFPenalty.PenaltyType.Ban)
{
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BANINFO_SUCCESS"].FormatExt(E.Target.Name, penalty.Offense));
}
@ -1088,7 +1114,7 @@ namespace SharedLibraryCore.Commands
.Where(c => c.Level > EFClient.Permission.Flagged && c.Level <= EFClient.Permission.Moderator)
.Where(c => c.LastConnection < lastActive)
.ToListAsync();
inactiveUsers.ForEach(c => c.SetLevel(EFClient.Permission.User, E.Origin));
inactiveUsers.ForEach(c => c.SetLevel(Permission.User, E.Origin));
await context.SaveChangesAsync();
}
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_PRUNE_SUCCESS"].FormatExt(inactiveUsers.Count));
@ -1120,10 +1146,6 @@ namespace SharedLibraryCore.Commands
E.Origin.Password = hashedPassword[0];
E.Origin.PasswordSalt = hashedPassword[1];
// update the password for the client in privileged
E.Owner.Manager.GetPrivilegedClients()[E.Origin.ClientId].Password = hashedPassword[0];
E.Owner.Manager.GetPrivilegedClients()[E.Origin.ClientId].PasswordSalt = hashedPassword[1];
await E.Owner.Manager.GetClientService().Update(E.Origin);
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_PASSWORD_SUCCESS"]);
}
@ -1137,12 +1159,12 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E)
{
if (E.Owner.ServerConfig.ManualLogPath != null)
if (E.Owner.ServerConfig.GameLogServerUrl != null)
{
using (var wc = new WebClient())
{
E.Owner.RestartRequested = true;
var response = await wc.DownloadStringTaskAsync(new Uri($"{E.Owner.ServerConfig.ManualLogPath}/restart"));
var response = await wc.DownloadStringTaskAsync(new Uri($"{E.Owner.ServerConfig.GameLogServerUrl}/restart"));
}
}
@ -1158,7 +1180,7 @@ namespace SharedLibraryCore.Commands
var regex = Regex.Match(cmdLine, @".*((?:\+set|\+) net_port) +([0-9]+).*");
if (regex.Success && Int32.Parse(regex.Groups[2].Value) == E.Owner.GetPort())
if (regex.Success && Int32.Parse(regex.Groups[2].Value) == E.Owner.Port)
{
currentProcess = p;
}

View File

@ -10,7 +10,7 @@ namespace SharedLibraryCore.Configuration
{
[LocalizedDisplayName("SETUP_ENABLE_WEBFRONT")]
[ConfiguratinLinked("WebfrontBindUrl", "ManualWebfrontUrl")]
[ConfigurationLinked("WebfrontBindUrl", "ManualWebfrontUrl")]
public bool EnableWebFront { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_BIND_URL")]
public string WebfrontBindUrl { get; set; }
@ -27,14 +27,14 @@ namespace SharedLibraryCore.Configuration
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_IGNORE_BOTS")]
public bool IgnoreBots { get; set; }
[ConfiguratinLinked("CustomSayName")]
[ConfigurationLinked("CustomSayName")]
[LocalizedDisplayName("SETUP_ENABLE_CUSTOMSAY")]
public bool EnableCustomSayName { get; set; }
[LocalizedDisplayName("SETUP_SAY_NAME")]
public string CustomSayName { get; set; }
[LocalizedDisplayName("SETUP_DISPLAY_SOCIAL")]
[ConfiguratinLinked("SocialLinkAddress", "SocialLinkTitle")]
[ConfigurationLinked("SocialLinkAddress", "SocialLinkTitle")]
public bool EnableSocialLink { get; set; }
[LocalizedDisplayName("SETUP_SOCIAL_LINK")]
public string SocialLinkAddress { get; set; }
@ -42,19 +42,19 @@ namespace SharedLibraryCore.Configuration
public string SocialLinkTitle { get; set; }
[LocalizedDisplayName("SETUP_USE_CUSTOMENCODING")]
[ConfiguratinLinked("CustomParserEncoding")]
[ConfigurationLinked("CustomParserEncoding")]
public bool EnableCustomParserEncoding { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_ENCODING")]
public string CustomParserEncoding { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_ENABLE_WHITELIST")]
[ConfiguratinLinked("WebfrontConnectionWhitelist")]
[ConfigurationLinked("WebfrontConnectionWhitelist")]
public bool EnableWebfrontConnectionWhitelist { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_WHITELIST_LIST")]
public List<string> WebfrontConnectionWhitelist { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")]
[ConfiguratinLinked("CustomLocale")]
[ConfigurationLinked("CustomLocale")]
public bool EnableCustomLocale { get; set; }
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_CUSTOM_LOCALE")]
public string CustomLocale { get; set; }

View File

@ -3,11 +3,11 @@
namespace SharedLibraryCore.Configuration.Attributes
{
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public class ConfiguratinLinked : Attribute
public class ConfigurationLinked : Attribute
{
public string[] LinkedPropertyNames { get; set; }
public ConfiguratinLinked(params string[] linkedPropertyNames)
public ConfigurationLinked(params string[] linkedPropertyNames)
{
LinkedPropertyNames = linkedPropertyNames;
}

View File

@ -7,7 +7,6 @@ namespace SharedLibraryCore.Configuration
{
public class QuickMessageConfiguration
{
public Game Game { get; set; }
public Dictionary<string, string> Messages { get; set; }
}

View File

@ -58,11 +58,11 @@ namespace SharedLibraryCore.Configuration
var parserVersions = rconParsers.Select(_parser => _parser.Version).ToArray();
var selection = Utilities.PromptSelection($"{loc["SETUP_SERVER_RCON_PARSER_VERSION"]} ({IPAddress}:{Port})", $"{loc["SETUP_PROMPT_DEFAULT"]} (Call of Duty)", null, parserVersions);
if (selection.Item1 > 0)
if (selection.Item1 >= 0)
{
RConParserVersion = selection.Item2;
if (!rconParsers[selection.Item1 - 1].CanGenerateLogPath)
if (selection.Item1 > 0 && !rconParsers[selection.Item1 - 1].CanGenerateLogPath)
{
Console.WriteLine(loc["SETUP_SERVER_NO_LOG"]);
ManualLogPath = Utilities.PromptString(loc["SETUP_SERVER_LOG_PATH"]);
@ -72,7 +72,7 @@ namespace SharedLibraryCore.Configuration
parserVersions = eventParsers.Select(_parser => _parser.Version).ToArray();
selection = Utilities.PromptSelection($"{loc["SETUP_SERVER_EVENT_PARSER_VERSION"]} ({IPAddress}:{Port})", $"{loc["SETUP_PROMPT_DEFAULT"]} (Call of Duty)", null, parserVersions);
if (selection.Item1 > 0)
if (selection.Item1 >= 0)
{
EventParserVersion = selection.Item2;
}

View File

@ -18,27 +18,11 @@ namespace SharedLibraryCore.Database
public async Task Seed()
{
// make sure database exists
//context.Database.EnsureCreated();
context.Database.Migrate();
if (context.AliasLinks.Count() == 0)
{
context.AliasLinks.Add(new EFAliasLink()
{
AliasLinkId = 1
});
var currentAlias = new EFAlias()
{
AliasId = 1,
Active = true,
DateAdded = DateTime.UtcNow,
Name = "IW4MAdmin",
LinkId = 1
};
context.Aliases.Add(currentAlias);
var link = new EFAliasLink();
context.Clients.Add(new EFClient()
{
@ -50,8 +34,14 @@ namespace SharedLibraryCore.Database
Level = Permission.Console,
Masked = true,
NetworkId = 0,
AliasLinkId = 1,
CurrentAliasId = 1,
AliasLink = link,
CurrentAlias = new EFAlias()
{
Link = link,
Active = true,
DateAdded = DateTime.UtcNow,
Name = "IW4MAdmin",
},
});
await context.SaveChangesAsync();

View File

@ -1,5 +1,7 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System;
@ -20,6 +22,12 @@ namespace SharedLibraryCore.Database
public DbSet<EFMeta> EFMeta { get; set; }
public DbSet<EFChangeHistory> EFChangeHistory { get; set; }
[Obsolete]
private static readonly ILoggerFactory _loggerFactory = new LoggerFactory(new[] {
new ConsoleLoggerProvider((category, level) => level == LogLevel.Information, true)
});
static string _ConnectionString;
static string _provider;
private static readonly string _migrationPluginDirectory = @"X:\IW4MAdmin\BUILD\Plugins";
@ -29,7 +37,7 @@ namespace SharedLibraryCore.Database
{
#if DEBUG == true
activeContextCount++;
Console.WriteLine($"Initialized DB Context #{activeContextCount}");
//Console.WriteLine($"Initialized DB Context #{activeContextCount}");
#endif
}
@ -37,7 +45,7 @@ namespace SharedLibraryCore.Database
{
#if DEBUG == true
activeContextCount++;
Console.WriteLine($"Initialized DB Context #{activeContextCount}");
//Console.WriteLine($"Initialized DB Context #{activeContextCount}");
#endif
}
@ -45,7 +53,7 @@ namespace SharedLibraryCore.Database
{
#if DEBUG == true
Console.WriteLine($"Disposed DB Context #{activeContextCount}");
//Console.WriteLine($"Disposed DB Context #{activeContextCount}");
activeContextCount--;
#endif
}
@ -103,6 +111,13 @@ namespace SharedLibraryCore.Database
break;
}
}
#if DEBUG
#pragma warning disable CS0612 // Type or member is obsolete
// optionsBuilder.UseLoggerFactory(_loggerFactory)
#pragma warning restore CS0612 // Type or member is obsolete
// .EnableSensitiveDataLogging();
#endif
}
protected override void OnModelCreating(ModelBuilder modelBuilder)

View File

@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace SharedLibraryCore.Database.Models
{
public partial class EFAlias : SharedEntity
public partial class EFAlias
{
[Key]
public int AliasId { get; set; }

View File

@ -35,26 +35,7 @@ namespace SharedLibraryCore.Database.Models
public string Password { get; set; }
public string PasswordSalt { get; set; }
// list of meta for the client
public virtual ICollection<EFMeta> Meta { get; set; }
[NotMapped]
public virtual string Name
{
get { return CurrentAlias?.Name ?? "--"; }
set { if (CurrentAlias != null) CurrentAlias.Name = value; }
}
[NotMapped]
public virtual int? IPAddress
{
get { return CurrentAlias.IPAddress; }
set { CurrentAlias.IPAddress = value; }
}
[NotMapped]
public string IPAddressString => IPAddress.ConvertIPtoString();
[NotMapped]
public virtual IDictionary<int, long> LinkedAccounts { get; set; }
public virtual ICollection<EFMeta> Meta { get; set; }
public virtual ICollection<EFPenalty> ReceivedPenalties { get; set; }
public virtual ICollection<EFPenalty> AdministeredPenalties { get; set; }
}

View File

@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace SharedLibraryCore.Database.Models
{
public class EFPenalty : SharedEntity
public partial class EFPenalty : SharedEntity
{
[Key]
public int PenaltyId { get; set; }
@ -29,6 +29,6 @@ namespace SharedLibraryCore.Database.Models
public string AutomatedOffense { get; set; }
[Required]
public bool IsEvadedOffense { get; set; }
public Objects.Penalty.PenaltyType Type { get; set; }
public PenaltyType Type { get; set; }
}
}

View File

@ -1,6 +1,6 @@
using System;
using static SharedLibraryCore.Database.Models.EFClient;
using static SharedLibraryCore.Objects.Penalty;
using static SharedLibraryCore.Database.Models.EFPenalty;
namespace SharedLibraryCore.Dtos
{

View File

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SharedLibraryCore.Events
{
/// <summary>
/// represents change from one value to another
/// </summary>
class Change
{
/// <summary>
/// represents the previous value of the item
/// </summary>
public string PreviousValue { get; set; }
/// <summary>
/// represents the new/current value of the item
/// </summary>
public string NewValue { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Events;
using System;
using System.Threading;
using System.Threading.Tasks;
@ -7,6 +8,9 @@ namespace SharedLibraryCore
{
public class GameEvent
{
// define what the delagate function looks like
public delegate void OnServerEventEventHandler(object sender, GameEventArgs e);
public enum EventFailReason
{
/// <summary>
@ -181,6 +185,14 @@ namespace SharedLibraryCore
Other
}
[Flags]
public enum EventRequiredEntity
{
None = 1,
Origin = 2,
Target = 4
}
static long NextEventId;
static long GetNextEventId()
{
@ -195,6 +207,7 @@ namespace SharedLibraryCore
}
public EventType Type;
public EventRequiredEntity RequiredEntity { get; set; }
public string Data; // Data is usually the message sent by player
public string Message;
public EFClient Origin;
@ -212,13 +225,19 @@ namespace SharedLibraryCore
/// asynchronously wait for GameEvent to be processed
/// </summary>
/// <returns>waitable task </returns>
public Task<GameEvent> WaitAsync(int timeOut = int.MaxValue)
public Task<GameEvent> WaitAsync(TimeSpan timeSpan, CancellationToken token)
{
return Task.Run(() =>
{
OnProcessed.Wait(timeOut);
OnProcessed.Wait(timeSpan, token);
return this;
});
}
public GameEvent Wait()
{
OnProcessed.Wait();
return this;
}
}
}

View File

@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SharedLibraryCore.Helpers
{
public sealed class AsyncStatus
{
DateTime StartTime;
int TimesRun;
int UpdateFrequency;
public double RunAverage { get; private set; }
public object Dependant { get; private set; }
public Task RequestedTask { get; private set; }
public CancellationTokenSource TokenSrc { get; private set; }
public AsyncStatus(object dependant, int frequency)
{
TokenSrc = new CancellationTokenSource();
StartTime = DateTime.Now;
Dependant = dependant;
UpdateFrequency = frequency;
// technically 0 but it's faster than checking for division by 0
TimesRun = 1;
}
public CancellationToken GetToken()
{
return TokenSrc.Token;
}
public double ElapsedMillisecondsTime()
{
return (DateTime.Now - StartTime).TotalMilliseconds;
}
public void Update(Task<bool> T)
{
// reset the token source
TokenSrc.Dispose();
TokenSrc = new CancellationTokenSource();
RequestedTask = T;
// Console.WriteLine($"Starting Task {T.Id} ");
RequestedTask.Start();
if (TimesRun > 25)
TimesRun = 1;
RunAverage = RunAverage + ((DateTime.Now - StartTime).TotalMilliseconds - RunAverage - UpdateFrequency) / TimesRun;
StartTime = DateTime.Now;
}
public void Abort()
{
RequestedTask = null;
}
}
}

View File

@ -5,7 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SharedLibraryCore.Objects
namespace SharedLibraryCore.Helpers
{
public class Report
{

View File

@ -13,7 +13,7 @@ namespace SharedLibraryCore.Helpers
[Key]
public int Vector3Id { get; set; }
public float X { get; protected set; }
public float Y { get; protected set; }
public float Y { get; protected set; }
public float Z { get; protected set; }
// this is for EF and really should be somewhere else
@ -38,11 +38,16 @@ namespace SharedLibraryCore.Helpers
{
bool valid = Regex.Match(s, @"\((-?[0-9]+\.?[0-9]*|-?[0-9]+\.?[0-9]*e-[0-9]+),\ (-?[0-9]+\.?[0-9]*|-?[0-9]+\.?[0-9]*e-[0-9]+),\ (-?[0-9]+\.?[0-9]*|-?[0-9]+\.?[0-9]*e-[0-9]+)\)").Success;
if (!valid)
{
throw new FormatException("Vector3 is not in correct format");
}
string removeParenthesis = s.Substring(1, s.Length - 2);
string[] eachPoint = removeParenthesis.Split(',');
return new Vector3(float.Parse(eachPoint[0], System.Globalization.NumberStyles.Any), float.Parse(eachPoint[1], System.Globalization.NumberStyles.Any), float.Parse(eachPoint[2], System.Globalization.NumberStyles.Any));
return new Vector3(float.Parse(eachPoint[0], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture),
float.Parse(eachPoint[1], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture),
float.Parse(eachPoint[2], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture));
}
public static double Distance(Vector3 a, Vector3 b)
@ -52,16 +57,14 @@ namespace SharedLibraryCore.Helpers
public static double AbsoluteDistance(Vector3 a, Vector3 b)
{
double deltaX = Math.Abs(b.X -a.X);
double deltaX = Math.Abs(b.X - a.X);
double deltaY = Math.Abs(b.Y - a.Y);
double deltaZ = Math.Abs(b.Z - a.Z);
// this 'fixes' the roll-over angles
double dx = deltaX < 360.0 / 2 ? deltaX : 360.0 - deltaX;
double dy = deltaY < 360.0 / 2 ? deltaY : 360.0 - deltaY;
double dz = deltaZ < 360.0 / 2 ? deltaZ : 360.0 - deltaZ;
return Math.Sqrt((dx * dx) + (dy * dy) /*+ (dz * dz)*/);
return Math.Sqrt((dx * dx) + (dy * dy));
}
public static double ViewAngleDistance(Vector3 a, Vector3 b, Vector3 c)

View File

@ -7,11 +7,10 @@ namespace SharedLibraryCore.Interfaces
/// <summary>
/// Generates a game event based on log line input
/// </summary>
/// <param name="server">server the event occurred on</param>
/// <param name="logLine">single log line string</param>
/// <returns></returns>
/// todo: make this integrate without needing the server
GameEvent GetEvent(Server server, string logLine);
GameEvent GenerateGameEvent(string logLine);
/// <summary>
/// Get game specific folder prefix for log files
/// </summary>

View File

@ -1,19 +1,20 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Services;
using SharedLibraryCore.Configuration;
using System.Reflection;
using SharedLibraryCore.Database.Models;
using System.Threading;
using static SharedLibraryCore.GameEvent;
namespace SharedLibraryCore.Interfaces
{
public interface IManager
{
Task Init();
void Start();
Task Start();
void Stop();
void Restart();
ILogger GetLogger(long serverId);
IList<Server> GetServers();
IList<Command> GetCommands();
@ -23,17 +24,11 @@ namespace SharedLibraryCore.Interfaces
ClientService GetClientService();
AliasService GetAliasService();
PenaltyService GetPenaltyService();
IDictionary<int, EFClient> GetPrivilegedClients();
/// <summary>
/// Get the event handlers
/// </summary>
/// <returns>EventHandler for the manager</returns>
IEventHandler GetEventHandler();
/// <summary>
/// Signal to the manager that event(s) needs to be processed
/// </summary>
void SetHasEvent();
bool ShutdownRequested();
IList<Assembly> GetPluginAssemblies();
/// <summary>
/// provides a page list to add and remove from
@ -47,5 +42,8 @@ namespace SharedLibraryCore.Interfaces
string Version { get;}
ITokenAuthentication TokenAuthenticator { get; }
string ExternalIPAddress { get; }
CancellationToken CancellationToken { get; }
bool IsRestartRequested { get; }
OnServerEventEventHandler OnServerEvent { get; set; }
}
}

View File

@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Objects;
using SharedLibraryCore.RCon;
using static SharedLibraryCore.Server;

View File

@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace SharedLibraryCore.Interfaces
{
interface ISerializable<T>
{
void Write();
}
public class Serialize<T> : ISerializable<T>
{
public static T Read(string filename)
{
try
{
string configText = File.ReadAllText(filename);
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(configText);
}
catch (Exception e)
{
throw new Exceptions.SerializeException($"Could not deserialize file {filename}: {e.Message}");
}
}
public void Write()
{
try
{
string configText = Newtonsoft.Json.JsonConvert.SerializeObject(this);
File.WriteAllText(Filename(), configText);
}
catch (Exception e)
{
throw new Exceptions.SerializeException($"Could not serialize file {Filename()}: {e.Message}");
}
}
public static void Write(string filename, T data)
{
try
{
string configText = Newtonsoft.Json.JsonConvert.SerializeObject(data, Newtonsoft.Json.Formatting.Indented);
File.WriteAllText(filename, configText);
}
catch (Exception e)
{
throw new Exceptions.SerializeException($"Could not serialize file {filename}: {e.Message}");
}
}
public virtual string Filename() { return ToString(); }
}
}

View File

@ -1,6 +1,6 @@
using static SharedLibraryCore.Database.Models.EFClient;
namespace SharedLibraryCore.Objects
namespace SharedLibraryCore.Localization
{
public sealed class ClientPermission
{

View File

@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database;
using SharedLibraryCore.Objects;
using System;
namespace SharedLibraryCore.Migrations

View File

@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database;
using SharedLibraryCore.Objects;
using System;
namespace SharedLibraryCore.Migrations

View File

@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database;
using SharedLibraryCore.Objects;
using System;
namespace SharedLibraryCore.Migrations

View File

@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database;
using SharedLibraryCore.Objects;
using System;
namespace SharedLibraryCore.Migrations

View File

@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database;
using SharedLibraryCore.Objects;
using System;
namespace SharedLibraryCore.Migrations

View File

@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database;
using SharedLibraryCore.Objects;
using System;
namespace SharedLibraryCore.Migrations

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