Compare commits
62 Commits
2.3-prerel
...
2.3
Author | SHA1 | Date | |
---|---|---|---|
cade2242bf | |||
1d7377f975 | |||
fb6d20e214 | |||
c630f65317 | |||
76cfe30c0f | |||
a7872aaffd | |||
88af032736 | |||
3af9f55bf1 | |||
1e9a87d6fa | |||
fe6fe39800 | |||
7f7353c505 | |||
198f596ab3 | |||
58a73e581f | |||
47d5df1aa1 | |||
d644387091 | |||
27a05ce6db | |||
11d2df1fe8 | |||
a820929582 | |||
6726217354 | |||
563c73221e | |||
652f3fb86b | |||
91078eec0f | |||
f6857ac635 | |||
001ecc5961 | |||
5fef69d697 | |||
2ba0b1e7d3 | |||
1c66ac9117 | |||
034d887abd | |||
06af995202 | |||
f8505781a0 | |||
f613f0aace | |||
ac32034910 | |||
2542b7de12 | |||
68f6be23a6 | |||
042327840f | |||
3d468e32b9 | |||
16d2ec82b8 | |||
421e90cf70 | |||
8119ff9f83 | |||
253c7c8721 | |||
cb80def122 | |||
e669d0be82 | |||
495197c19d | |||
a5414c2c57 | |||
cbfb3919fc | |||
d789542d0f | |||
c6c2ec7784 | |||
4645bd84e8 | |||
10829b32ad | |||
e86904b11e | |||
82390340c9 | |||
163523d586 | |||
95d64df321 | |||
0b0290a871 | |||
5f588bb0f7 | |||
b99cc424e7 | |||
1dc0f5a240 | |||
43c4d4af38 | |||
db11a5f480 | |||
b51af7ca9a | |||
2cceb2f3e7 | |||
599a14b646 |
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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
|
@ -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"
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
@ -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:
|
||||
|
@ -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']
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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 "$(TargetPath)" "$(SolutionDir)BUILD\Plugins""/>
|
||||
<Exec Command="copy "$(TargetDir)Microsoft.SyndicationFeed.ReaderWriter.dll" "$(SolutionDir)BUILD\Plugins""/>
|
||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||
<Exec Command="copy "$(TargetDir)Microsoft.SyndicationFeed.ReaderWriter.dll" "$(SolutionDir)BUILD\Plugins"" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
@ -79,7 +79,7 @@ namespace AutomessageFeed
|
||||
|
||||
public Task OnUnloadAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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 "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||
</Target>
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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}}';
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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) {
|
||||
},
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
79
Plugins/Stats/Cheat/README.md
Normal file
79
Plugins/Stats/Cheat/README.md
Normal 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.
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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))
|
||||
|
@ -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"]}--"
|
||||
|
@ -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())
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||
</Target>
|
||||
|
@ -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));
|
||||
|
@ -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");
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -1,7 +1,6 @@
|
||||
using IW4MAdmin.Application;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Objects;
|
||||
using Xunit;
|
||||
|
||||
namespace Tests
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
Plugins/Tests/TestHelpers.cs
Normal file
24
Plugins/Tests/TestHelpers.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
38
Plugins/Tests/TestRconParser.cs
Normal file
38
Plugins/Tests/TestRconParser.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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
100
README.md
@ -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 — `n/a`
|
||||
|
||||
`Use Pluto T6 parser`
|
||||
* Used if setting up a server for Plutonium T6 (BO2)
|
||||
* Default — `false`
|
||||
|
||||
`Use Pluto IW5 parser`
|
||||
* Used if setting a server for Plutonium IW5 (MW3)
|
||||
* Default — `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 — `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 — `http://0.0.0.0:1624`
|
||||
* Default — `http://0.0.0.0:1624` (indicates that it will listen on all IP Addresses available to the default interface)
|
||||
|
||||
`CustomLocale`
|
||||
* Specifies a [locale name](https://msdn.microsoft.com/en-us/library/39cwe7zf.aspx) to use instead of system default
|
||||
@ -114,7 +117,7 @@ If you wish to further customize your experience of **IW4MAdmin**, the following
|
||||
* Default — `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 — http://gamelogserver.com/
|
||||
* Default — `null`
|
||||
* `AutoMessages`
|
||||
* Specifies the list of messages that are broadcasted to the particular server
|
||||
* Default — `null`
|
||||
* Default — `[]`
|
||||
* `Rules`
|
||||
* Specifies the list of rules that apply to the particular server
|
||||
* Default — `null`
|
||||
* Default — `[]`
|
||||
* `ReservedSlotNumber`
|
||||
* Specifies the number of client slots to reserve for privileged users
|
||||
* Default — `0`
|
||||
* `GameLogServerUrl`
|
||||
* Specifies the HTTP Url for the Game Log Server
|
||||
* Default — `null`
|
||||
|
||||
`AutoMessagePeriod`
|
||||
* Specifies (in seconds) how often messages should be broadcasted to each server
|
||||
@ -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` — [function] Handler executed when the plugin is loaded by code
|
||||
- `manager` — [parameter object] Object reference to the application manager (see the IManager interface definition)
|
||||
- `onUnloadAsync` — [function] Handler executed when the plugin is unloaded by code (see live reloading)
|
||||
- `onTickAsync` — [function] Handler executed approximately once per second by code *(unimplemented as of version 2.1)*
|
||||
- `onTickAsync` — [function] Handler executed approximately once per second by code *(unimplemented as of version 2.\*)*
|
||||
- `server` — [parameter object] Object containing information and methods about the server the event occured on (see the Server class declaration)
|
||||
### Live Reloading
|
||||
Thanks to JavaScript's flexibility and parsability, the plugin importer scans the plugins folder and reloads the JavaScript plugins on demand as they're modified. This allows faster development/testing/debugging.
|
||||
|
||||
---
|
||||
### 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` — Base url corresponding to your IW4MAdmin `WebfrontBindUrl`.
|
||||
Example http://127.0.0.1
|
||||
- `DiscordWebhookNotificationUrl` — [required] Discord generated URL to send notifications/alerts to; this includes **Reports** and **Bans**
|
||||
Example https://discordapp.com/api/webhooks/id/token
|
||||
- `DiscordWebhookInformationUrl` — [optional] Discord generated URL to send information to; this includes information such as player messages
|
||||
- `NotifyRoleIds` — [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
|
||||
|
@ -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"
|
@ -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"
|
@ -2,7 +2,6 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Objects;
|
||||
|
||||
namespace SharedLibraryCore
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -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;
|
||||
}
|
@ -7,7 +7,6 @@ namespace SharedLibraryCore.Configuration
|
||||
{
|
||||
public class QuickMessageConfiguration
|
||||
{
|
||||
|
||||
public Game Game { get; set; }
|
||||
public Dictionary<string, string> Messages { get; set; }
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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; }
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharedLibraryCore.Objects
|
||||
namespace SharedLibraryCore.Helpers
|
||||
{
|
||||
public class Report
|
||||
{
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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(); }
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using static SharedLibraryCore.Database.Models.EFClient;
|
||||
|
||||
namespace SharedLibraryCore.Objects
|
||||
namespace SharedLibraryCore.Localization
|
||||
{
|
||||
public sealed class ClientPermission
|
||||
{
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
Reference in New Issue
Block a user