Compare commits
129 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 | |||
02622ea7de | |||
03ae3b5822 | |||
0711249a46 | |||
18f4ffa9ff | |||
934fead5c2 | |||
02cad10d77 | |||
b134cd4728 | |||
b9c4a1b5f6 | |||
f0fd4c66e9 | |||
52fe8fc847 | |||
9f8c35dbed | |||
9d9be7f8af | |||
dd82a5e3fa | |||
8667532d24 | |||
863ba8b096 | |||
8ab89e113d | |||
00634780d4 | |||
6f80f1edbb | |||
9393b35c39 | |||
37d3f4f90d | |||
8521df85f5 | |||
7b8126d57e | |||
1e2e2218e3 | |||
11e3235d5d | |||
cae6d8389e | |||
1056c7c335 | |||
95e4ee672e | |||
bf0a0befc6 | |||
53c3ff6ce3 | |||
c8ec0eefa9 | |||
82bae772f0 | |||
af3aea77bc | |||
b3e5f468a1 | |||
c21bf2ebf1 | |||
d318a57830 | |||
61f1436faf | |||
0c527a5f65 | |||
1457b843e2 | |||
de69bed792 | |||
4b1f44cc2a | |||
74cdf8e885 | |||
5d41059641 | |||
2e6889d9bb | |||
9c4d23f0b4 | |||
b4e3e8526a | |||
e3944fb8c2 | |||
40f1697c97 | |||
2bbf2988da | |||
5e36bf4316 | |||
a362caebac | |||
2260d8974d | |||
5e04274da6 | |||
6d2e6aee4f | |||
9e74c42246 | |||
dea5b3f954 | |||
7c6419a16a | |||
0194196a33 | |||
044991272f | |||
f3290cf066 | |||
29eedea093 | |||
ce02f5dd68 | |||
e6bfa408f8 | |||
0a1dc46760 | |||
97ba6aae2e | |||
a456fab0e5 | |||
3e5282df87 | |||
59e0072744 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -223,7 +223,7 @@ global.min.js
|
|||||||
bootstrap-custom.min.css
|
bootstrap-custom.min.css
|
||||||
**/Master/static
|
**/Master/static
|
||||||
**/Master/dev_env
|
**/Master/dev_env
|
||||||
/WebfrontCore/Views/Plugins/Stats
|
/WebfrontCore/Views/Plugins/*
|
||||||
/WebfrontCore/wwwroot/**/dds
|
/WebfrontCore/wwwroot/**/dds
|
||||||
|
|
||||||
/DiscordWebhook/env
|
/DiscordWebhook/env
|
||||||
@ -232,4 +232,5 @@ bootstrap-custom.min.css
|
|||||||
launchSettings.json
|
launchSettings.json
|
||||||
/VpnDetectionPrivate.js
|
/VpnDetectionPrivate.js
|
||||||
/Plugins/ScriptPlugins/VpnDetectionPrivate.js
|
/Plugins/ScriptPlugins/VpnDetectionPrivate.js
|
||||||
**/Master/env_master
|
**/Master/env_master
|
||||||
|
/GameLogServer/log_env
|
||||||
|
@ -6,7 +6,7 @@ namespace IW4MAdmin.Application.API.GameLogServer
|
|||||||
[Header("User-Agent", "IW4MAdmin-RestEase")]
|
[Header("User-Agent", "IW4MAdmin-RestEase")]
|
||||||
public interface IGameLogServer
|
public interface IGameLogServer
|
||||||
{
|
{
|
||||||
[Get("log/{path}")]
|
[Get("log/{path}/{key}")]
|
||||||
Task<LogInfo> Log([Path] string path);
|
Task<LogInfo> Log([Path] string path, [Path] string key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,5 +13,7 @@ namespace IW4MAdmin.Application.API.GameLogServer
|
|||||||
public int Length { get; set; }
|
public int Length { get; set; }
|
||||||
[JsonProperty("data")]
|
[JsonProperty("data")]
|
||||||
public string Data { get; set; }
|
public string Data { get; set; }
|
||||||
|
[JsonProperty("next_key")]
|
||||||
|
public string NextKey { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using RestEase;
|
using RestEase;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
@ -11,6 +12,7 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
public class HeartbeatState
|
public class HeartbeatState
|
||||||
{
|
{
|
||||||
public bool Connected { get; set; }
|
public bool Connected { get; set; }
|
||||||
|
public CancellationToken Token { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Heartbeat
|
public class Heartbeat
|
||||||
@ -45,7 +47,7 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
Map = s.CurrentMap.Name,
|
Map = s.CurrentMap.Name,
|
||||||
MaxClientNum = s.MaxClients,
|
MaxClientNum = s.MaxClients,
|
||||||
Id = s.EndPoint,
|
Id = s.EndPoint,
|
||||||
Port = (short)s.GetPort(),
|
Port = (short)s.Port,
|
||||||
IPAddress = s.IP
|
IPAddress = s.IP
|
||||||
}).ToList()
|
}).ToList()
|
||||||
};
|
};
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
<RuntimeFrameworkVersion>2.1.5</RuntimeFrameworkVersion>
|
|
||||||
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
|
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
|
||||||
|
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||||
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
|
||||||
<Version>2.2.4.1</Version>
|
<Version>2.2.8.4</Version>
|
||||||
<Authors>RaidMax</Authors>
|
<Authors>RaidMax</Authors>
|
||||||
<Company>Forever None</Company>
|
<Company>Forever None</Company>
|
||||||
<Product>IW4MAdmin</Product>
|
<Product>IW4MAdmin</Product>
|
||||||
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated server</Description>
|
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated servers</Description>
|
||||||
<Copyright>2019</Copyright>
|
<Copyright>2019</Copyright>
|
||||||
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
|
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
|
||||||
<PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl>
|
<PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl>
|
||||||
@ -21,18 +21,21 @@
|
|||||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||||
<Win32Resource />
|
<Win32Resource />
|
||||||
<RootNamespace>IW4MAdmin.Application</RootNamespace>
|
<RootNamespace>IW4MAdmin.Application</RootNamespace>
|
||||||
|
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="RestEase" Version="1.4.7" />
|
<PackageReference Include="RestEase" Version="1.4.9" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.0" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||||
|
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||||
<TieredCompilation>true</TieredCompilation>
|
<TieredCompilation>true</TieredCompilation>
|
||||||
<AssemblyVersion>2.2.4.1</AssemblyVersion>
|
<AssemblyVersion>2.2.8.4</AssemblyVersion>
|
||||||
<FileVersion>2.2.4.1</FileVersion>
|
<FileVersion>2.2.8.4</FileVersion>
|
||||||
|
<LangVersion>7.1</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -45,41 +48,10 @@
|
|||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Update="Properties\IW4MAdmin.en-US.Designer.cs">
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>IW4MAdmin.en-US.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Update="Properties\IW4MAdmin.en-US.resx">
|
|
||||||
<Generator>ResXFileCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>IW4MAdmin.en-US.Designer.cs</LastGenOutput>
|
|
||||||
</EmbeddedResource>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="DefaultSettings.json">
|
<None Update="DefaultSettings.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
<None Update="Localization\IW4MAdmin.en-US.json">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Localization\IW4MAdmin.es-EC.json">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Localization\IW4MAdmin.pt-BR.json">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Localization\IW4MAdmin.ru-RU.json">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.1.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||||
|
@ -1,50 +1,49 @@
|
|||||||
using IW4MAdmin.Application.API.Master;
|
using IW4MAdmin.Application.API.Master;
|
||||||
using IW4MAdmin.Application.EventParsers;
|
using IW4MAdmin.Application.EventParsers;
|
||||||
|
using IW4MAdmin.Application.Misc;
|
||||||
using IW4MAdmin.Application.RconParsers;
|
using IW4MAdmin.Application.RconParsers;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Commands;
|
using SharedLibraryCore.Commands;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
using SharedLibraryCore.Database;
|
using SharedLibraryCore.Database;
|
||||||
using SharedLibraryCore.Database.Models;
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Dtos;
|
||||||
using SharedLibraryCore.Events;
|
using SharedLibraryCore.Events;
|
||||||
using SharedLibraryCore.Exceptions;
|
using SharedLibraryCore.Exceptions;
|
||||||
using SharedLibraryCore.Helpers;
|
using SharedLibraryCore.Helpers;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using SharedLibraryCore.Services;
|
using SharedLibraryCore.Services;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static SharedLibraryCore.GameEvent;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
public class ApplicationManager : IManager
|
public class ApplicationManager : IManager
|
||||||
{
|
{
|
||||||
private List<Server> _servers;
|
private ConcurrentBag<Server> _servers;
|
||||||
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
|
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
|
||||||
public Dictionary<int, EFClient> PrivilegedClients { get; set; }
|
|
||||||
public ILogger Logger => GetLogger(0);
|
public ILogger Logger => GetLogger(0);
|
||||||
public bool Running { get; private set; }
|
public bool Running { get; private set; }
|
||||||
public bool IsInitialized { 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
|
// expose the event handler so we can execute the events
|
||||||
public OnServerEventEventHandler OnServerEvent { get; set; }
|
public OnServerEventEventHandler OnServerEvent { get; set; }
|
||||||
public DateTime StartTime { get; private set; }
|
public DateTime StartTime { get; private set; }
|
||||||
public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString();
|
public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString();
|
||||||
|
|
||||||
public IList<IRConParser> AdditionalRConParsers => _additionalRConParsers.ToList<IRConParser>();
|
public IList<IRConParser> AdditionalRConParsers { get; }
|
||||||
|
public IList<IEventParser> AdditionalEventParsers { get; }
|
||||||
public IList<IEventParser> AdditionalEventParsers => _additionalEventParsers.ToList<IEventParser>();
|
public ITokenAuthentication TokenAuthenticator { get; }
|
||||||
|
public CancellationToken CancellationToken => _tokenSource.Token;
|
||||||
private readonly IList<DynamicRConParser> _additionalRConParsers;
|
public string ExternalIPAddress { get; private set; }
|
||||||
private readonly IList<DynamicEventParser> _additionalEventParsers;
|
public bool IsRestartRequested { get; private set; }
|
||||||
|
|
||||||
static ApplicationManager Instance;
|
static ApplicationManager Instance;
|
||||||
readonly List<AsyncStatus> TaskStatuses;
|
|
||||||
List<Command> Commands;
|
List<Command> Commands;
|
||||||
readonly List<MessageToken> MessageTokens;
|
readonly List<MessageToken> MessageTokens;
|
||||||
ClientService ClientSvc;
|
ClientService ClientSvc;
|
||||||
@ -52,28 +51,30 @@ namespace IW4MAdmin.Application
|
|||||||
readonly PenaltyService PenaltySvc;
|
readonly PenaltyService PenaltySvc;
|
||||||
public BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
public BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
||||||
GameEventHandler Handler;
|
GameEventHandler Handler;
|
||||||
ManualResetEventSlim OnQuit;
|
|
||||||
readonly IPageList PageList;
|
readonly IPageList PageList;
|
||||||
readonly SemaphoreSlim ProcessingEvent = new SemaphoreSlim(1, 1);
|
|
||||||
readonly Dictionary<long, ILogger> Loggers = new Dictionary<long, ILogger>();
|
readonly Dictionary<long, ILogger> Loggers = new Dictionary<long, ILogger>();
|
||||||
|
private readonly MetaService _metaService;
|
||||||
|
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
|
||||||
|
private readonly CancellationTokenSource _tokenSource;
|
||||||
|
|
||||||
private ApplicationManager()
|
private ApplicationManager()
|
||||||
{
|
{
|
||||||
_servers = new List<Server>();
|
_servers = new ConcurrentBag<Server>();
|
||||||
Commands = new List<Command>();
|
Commands = new List<Command>();
|
||||||
TaskStatuses = new List<AsyncStatus>();
|
|
||||||
MessageTokens = new List<MessageToken>();
|
MessageTokens = new List<MessageToken>();
|
||||||
ClientSvc = new ClientService();
|
ClientSvc = new ClientService();
|
||||||
AliasSvc = new AliasService();
|
AliasSvc = new AliasService();
|
||||||
PenaltySvc = new PenaltyService();
|
PenaltySvc = new PenaltyService();
|
||||||
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
||||||
StartTime = DateTime.UtcNow;
|
StartTime = DateTime.UtcNow;
|
||||||
OnQuit = new ManualResetEventSlim();
|
|
||||||
PageList = new PageList();
|
PageList = new PageList();
|
||||||
_additionalEventParsers = new List<DynamicEventParser>();
|
AdditionalEventParsers = new List<IEventParser>();
|
||||||
_additionalRConParsers = new List<DynamicRConParser>();
|
AdditionalRConParsers = new List<IRConParser>();
|
||||||
OnServerEvent += OnGameEvent;
|
OnServerEvent += OnGameEvent;
|
||||||
OnServerEvent += EventApi.OnGameEvent;
|
OnServerEvent += EventApi.OnGameEvent;
|
||||||
|
TokenAuthenticator = new TokenAuthentication();
|
||||||
|
_metaService = new MetaService();
|
||||||
|
_tokenSource = new CancellationTokenSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnGameEvent(object sender, GameEventArgs args)
|
private async void OnGameEvent(object sender, GameEventArgs args)
|
||||||
@ -126,7 +127,7 @@ namespace IW4MAdmin.Application
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
newEvent.FailReason = GameEvent.EventFailReason.Exception;
|
newEvent.FailReason = GameEvent.EventFailReason.Exception;
|
||||||
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}");
|
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner));
|
||||||
Logger.WriteDebug(ex.GetExceptionInfo());
|
Logger.WriteDebug(ex.GetExceptionInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +157,7 @@ namespace IW4MAdmin.Application
|
|||||||
// store the server hash code and task for it
|
// store the server hash code and task for it
|
||||||
var runningUpdateTasks = new Dictionary<long, Task>();
|
var runningUpdateTasks = new Dictionary<long, Task>();
|
||||||
|
|
||||||
while (Running)
|
while (!_tokenSource.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// select the server ids that have completed the update task
|
// select the server ids that have completed the update task
|
||||||
var serverTasksToRemove = runningUpdateTasks
|
var serverTasksToRemove = runningUpdateTasks
|
||||||
@ -187,7 +188,12 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await server.ProcessUpdatesAsync(new CancellationToken());
|
await server.ProcessUpdatesAsync(_tokenSource.Token);
|
||||||
|
|
||||||
|
if (server.Throttled)
|
||||||
|
{
|
||||||
|
await Task.Delay((int)_throttleTimeout.TotalMilliseconds, _tokenSource.Token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -195,6 +201,11 @@ namespace IW4MAdmin.Application
|
|||||||
Logger.WriteWarning($"Failed to update status for {server}");
|
Logger.WriteWarning($"Failed to update status for {server}");
|
||||||
Logger.WriteDebug(e.GetExceptionInfo());
|
Logger.WriteDebug(e.GetExceptionInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
server.IsInitialized = true;
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@ -203,81 +214,26 @@ namespace IW4MAdmin.Application
|
|||||||
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
|
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
|
||||||
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
|
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
|
||||||
#endif
|
#endif
|
||||||
await Task.Delay(ConfigHandler.Configuration().RConPollRate);
|
try
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// trigger the event processing loop to end
|
|
||||||
SetHasEvent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Init()
|
public async Task Init()
|
||||||
{
|
{
|
||||||
Running = true;
|
Running = true;
|
||||||
|
ExternalIPAddress = await Utilities.GetExternalIP();
|
||||||
#region CONFIG
|
|
||||||
var config = ConfigHandler.Configuration();
|
|
||||||
|
|
||||||
// copy over default config if it doesn't exist
|
|
||||||
if (config == null)
|
|
||||||
{
|
|
||||||
var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration();
|
|
||||||
ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
|
|
||||||
var newConfig = ConfigHandler.Configuration();
|
|
||||||
|
|
||||||
newConfig.AutoMessagePeriod = defaultConfig.AutoMessagePeriod;
|
|
||||||
newConfig.AutoMessages = defaultConfig.AutoMessages;
|
|
||||||
newConfig.GlobalRules = defaultConfig.GlobalRules;
|
|
||||||
newConfig.Maps = defaultConfig.Maps;
|
|
||||||
|
|
||||||
if (newConfig.Servers == null)
|
|
||||||
{
|
|
||||||
ConfigHandler.Set(newConfig);
|
|
||||||
newConfig.Servers = new List<ServerConfiguration>();
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
newConfig.Servers.Add((ServerConfiguration)new ServerConfiguration().Generate());
|
|
||||||
} while (Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["SETUP_SERVER_SAVE"]));
|
|
||||||
|
|
||||||
config = newConfig;
|
|
||||||
await ConfigHandler.Save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (config != null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(config.Id))
|
|
||||||
{
|
|
||||||
config.Id = Guid.NewGuid().ToString();
|
|
||||||
await ConfigHandler.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
|
|
||||||
{
|
|
||||||
config.WebfrontBindUrl = "http://0.0.0.0:1624";
|
|
||||||
await ConfigHandler.Save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (config.Servers.Count == 0)
|
|
||||||
{
|
|
||||||
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
|
|
||||||
}
|
|
||||||
|
|
||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
|
||||||
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252");
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region DATABASE
|
|
||||||
using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString,
|
|
||||||
GetApplicationSettings().Configuration()?.DatabaseProvider))
|
|
||||||
{
|
|
||||||
await new ContextSeed(db).Seed();
|
|
||||||
}
|
|
||||||
|
|
||||||
PrivilegedClients = (await ClientSvc.GetPrivilegedClients()).ToDictionary(_client => _client.ClientId);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region PLUGINS
|
#region PLUGINS
|
||||||
SharedLibraryCore.Plugins.PluginImporter.Load(this);
|
SharedLibraryCore.Plugins.PluginImporter.Load(this);
|
||||||
@ -297,6 +253,102 @@ namespace IW4MAdmin.Application
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region CONFIG
|
||||||
|
var config = ConfigHandler.Configuration();
|
||||||
|
|
||||||
|
// copy over default config if it doesn't exist
|
||||||
|
if (config == null)
|
||||||
|
{
|
||||||
|
var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration();
|
||||||
|
ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
|
||||||
|
var newConfig = ConfigHandler.Configuration();
|
||||||
|
|
||||||
|
newConfig.AutoMessages = defaultConfig.AutoMessages;
|
||||||
|
newConfig.GlobalRules = defaultConfig.GlobalRules;
|
||||||
|
newConfig.Maps = defaultConfig.Maps;
|
||||||
|
newConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
|
||||||
|
newConfig.QuickMessages = defaultConfig.QuickMessages;
|
||||||
|
|
||||||
|
if (newConfig.Servers == null)
|
||||||
|
{
|
||||||
|
ConfigHandler.Set(newConfig);
|
||||||
|
newConfig.Servers = new List<ServerConfiguration>();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var serverConfig = new ServerConfiguration();
|
||||||
|
foreach (var parser in AdditionalRConParsers)
|
||||||
|
{
|
||||||
|
serverConfig.AddRConParser(parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var parser in AdditionalEventParsers)
|
||||||
|
{
|
||||||
|
serverConfig.AddEventParser(parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig.Servers.Add((ServerConfiguration)serverConfig.Generate());
|
||||||
|
} while (Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["SETUP_SERVER_SAVE"]));
|
||||||
|
|
||||||
|
config = newConfig;
|
||||||
|
await ConfigHandler.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(config.Id))
|
||||||
|
{
|
||||||
|
config.Id = Guid.NewGuid().ToString();
|
||||||
|
await ConfigHandler.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
|
||||||
|
{
|
||||||
|
config.WebfrontBindUrl = "http://0.0.0.0:1624";
|
||||||
|
await ConfigHandler.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var serverConfig in config.Servers)
|
||||||
|
{
|
||||||
|
Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
|
||||||
|
|
||||||
|
if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null)
|
||||||
|
{
|
||||||
|
foreach (var parser in AdditionalRConParsers)
|
||||||
|
{
|
||||||
|
serverConfig.AddRConParser(parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var parser in AdditionalEventParsers)
|
||||||
|
{
|
||||||
|
serverConfig.AddEventParser(parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
serverConfig.ModifyParsers();
|
||||||
|
}
|
||||||
|
await ConfigHandler.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.Servers.Count == 0)
|
||||||
|
{
|
||||||
|
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
|
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252");
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region DATABASE
|
||||||
|
using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString,
|
||||||
|
GetApplicationSettings().Configuration()?.DatabaseProvider))
|
||||||
|
{
|
||||||
|
await new ContextSeed(db).Seed();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region COMMANDS
|
#region COMMANDS
|
||||||
if (ClientSvc.GetOwners().Result.Count == 0)
|
if (ClientSvc.GetOwners().Result.Count == 0)
|
||||||
{
|
{
|
||||||
@ -304,6 +356,7 @@ namespace IW4MAdmin.Application
|
|||||||
}
|
}
|
||||||
|
|
||||||
Commands.Add(new CQuit());
|
Commands.Add(new CQuit());
|
||||||
|
Commands.Add(new CRestart());
|
||||||
Commands.Add(new CKick());
|
Commands.Add(new CKick());
|
||||||
Commands.Add(new CSay());
|
Commands.Add(new CSay());
|
||||||
Commands.Add(new CTempBan());
|
Commands.Add(new CTempBan());
|
||||||
@ -335,11 +388,12 @@ namespace IW4MAdmin.Application
|
|||||||
Commands.Add(new CIP());
|
Commands.Add(new CIP());
|
||||||
Commands.Add(new CMask());
|
Commands.Add(new CMask());
|
||||||
Commands.Add(new CPruneAdmins());
|
Commands.Add(new CPruneAdmins());
|
||||||
Commands.Add(new CKillServer());
|
//Commands.Add(new CKillServer());
|
||||||
Commands.Add(new CSetPassword());
|
Commands.Add(new CSetPassword());
|
||||||
Commands.Add(new CPing());
|
Commands.Add(new CPing());
|
||||||
Commands.Add(new CSetGravatar());
|
Commands.Add(new CSetGravatar());
|
||||||
Commands.Add(new CNextMap());
|
Commands.Add(new CNextMap());
|
||||||
|
Commands.Add(new RequestTokenCommand());
|
||||||
|
|
||||||
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
|
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
|
||||||
{
|
{
|
||||||
@ -347,22 +401,150 @@ namespace IW4MAdmin.Application
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region INIT
|
#region META
|
||||||
|
async Task<List<ProfileMeta>> getProfileMeta(int clientId, int offset, int count, DateTime? startAt)
|
||||||
|
{
|
||||||
|
var metaList = new List<ProfileMeta>();
|
||||||
|
|
||||||
|
// we don't want to return anything because it means we're trying to retrieve paged meta data
|
||||||
|
if (count > 1)
|
||||||
|
{
|
||||||
|
return metaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastMapMeta = await _metaService.GetPersistentMeta("LastMapPlayed", new EFClient() { ClientId = clientId });
|
||||||
|
|
||||||
|
if (lastMapMeta != null)
|
||||||
|
{
|
||||||
|
metaList.Add(new ProfileMeta()
|
||||||
|
{
|
||||||
|
Id = lastMapMeta.MetaId,
|
||||||
|
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_MAP"],
|
||||||
|
Value = lastMapMeta.Value,
|
||||||
|
Show = true,
|
||||||
|
Type = ProfileMeta.MetaType.Information,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastServerMeta = await _metaService.GetPersistentMeta("LastServerPlayed", new EFClient() { ClientId = clientId });
|
||||||
|
|
||||||
|
if (lastServerMeta != null)
|
||||||
|
{
|
||||||
|
metaList.Add(new ProfileMeta()
|
||||||
|
{
|
||||||
|
Id = lastServerMeta.MetaId,
|
||||||
|
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_SERVER"],
|
||||||
|
Value = lastServerMeta.Value,
|
||||||
|
Show = true,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = await GetClientService().Get(clientId);
|
||||||
|
|
||||||
|
metaList.Add(new ProfileMeta()
|
||||||
|
{
|
||||||
|
Id = client.ClientId,
|
||||||
|
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_TIME_HOURS"]} {Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PLAYER"]}",
|
||||||
|
Value = Math.Round(client.TotalConnectionTime / 3600.0, 1).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||||
|
Show = true,
|
||||||
|
Column = 1,
|
||||||
|
Order = 0,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
|
});
|
||||||
|
|
||||||
|
metaList.Add(new ProfileMeta()
|
||||||
|
{
|
||||||
|
Id = client.ClientId,
|
||||||
|
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_FSEEN"],
|
||||||
|
Value = Utilities.GetTimePassed(client.FirstConnection, false),
|
||||||
|
Show = true,
|
||||||
|
Column = 1,
|
||||||
|
Order = 1,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
|
});
|
||||||
|
|
||||||
|
metaList.Add(new ProfileMeta()
|
||||||
|
{
|
||||||
|
Id = client.ClientId,
|
||||||
|
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_LSEEN"],
|
||||||
|
Value = Utilities.GetTimePassed(client.LastConnection, false),
|
||||||
|
Show = true,
|
||||||
|
Column = 1,
|
||||||
|
Order = 2,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
|
});
|
||||||
|
|
||||||
|
metaList.Add(new ProfileMeta()
|
||||||
|
{
|
||||||
|
Id = client.ClientId,
|
||||||
|
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_CONNECTIONS"],
|
||||||
|
Value = client.Connections.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||||
|
Show = true,
|
||||||
|
Column = 1,
|
||||||
|
Order = 3,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
|
});
|
||||||
|
|
||||||
|
metaList.Add(new ProfileMeta()
|
||||||
|
{
|
||||||
|
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_MASKED"],
|
||||||
|
Value = client.Masked ? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"] : Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"],
|
||||||
|
Sensitive = true,
|
||||||
|
Column = 1,
|
||||||
|
Order = 4,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
|
});
|
||||||
|
|
||||||
|
return metaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<List<ProfileMeta>> getPenaltyMeta(int clientId, int offset, int count, DateTime? startAt)
|
||||||
|
{
|
||||||
|
if (count <= 1)
|
||||||
|
{
|
||||||
|
return new List<ProfileMeta>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var penalties = await GetPenaltyService().GetClientPenaltyForMetaAsync(clientId, count, offset, startAt);
|
||||||
|
|
||||||
|
return penalties.Select(_penalty => new ProfileMeta()
|
||||||
|
{
|
||||||
|
Id = _penalty.Id,
|
||||||
|
Type = _penalty.PunisherId == clientId ? ProfileMeta.MetaType.Penalized : ProfileMeta.MetaType.ReceivedPenalty,
|
||||||
|
Value = _penalty,
|
||||||
|
When = _penalty.TimePunished,
|
||||||
|
Sensitive = _penalty.Sensitive
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
MetaService.AddRuntimeMeta(getProfileMeta);
|
||||||
|
MetaService.AddRuntimeMeta(getPenaltyMeta);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
await InitializeServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeServers()
|
||||||
|
{
|
||||||
|
var config = ConfigHandler.Configuration();
|
||||||
|
int successServers = 0;
|
||||||
|
Exception lastException = null;
|
||||||
|
|
||||||
async Task Init(ServerConfiguration Conf)
|
async Task Init(ServerConfiguration Conf)
|
||||||
{
|
{
|
||||||
// setup the event handler after the class is initialized
|
// setup the event handler after the class is initialized
|
||||||
Handler = new GameEventHandler(this);
|
Handler = new GameEventHandler(this);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ServerInstance = new IW4MServer(this, Conf);
|
var ServerInstance = new IW4MServer(this, Conf);
|
||||||
await ServerInstance.Initialize();
|
await ServerInstance.Initialize();
|
||||||
|
|
||||||
lock (_servers)
|
_servers.Add(ServerInstance);
|
||||||
{
|
|
||||||
_servers.Add(ServerInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.WriteVerbose($"{Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"]} {ServerInstance.Hostname}");
|
Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname));
|
||||||
// add the start event for this server
|
// add the start event for this server
|
||||||
|
|
||||||
var e = new GameEvent()
|
var e = new GameEvent()
|
||||||
@ -373,46 +555,55 @@ namespace IW4MAdmin.Application
|
|||||||
};
|
};
|
||||||
|
|
||||||
Handler.AddEvent(e);
|
Handler.AddEvent(e);
|
||||||
|
successServers++;
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (ServerException e)
|
catch (ServerException e)
|
||||||
{
|
{
|
||||||
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"]} [{Conf.IPAddress}:{Conf.Port}]");
|
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"].FormatExt($"[{Conf.IPAddress}:{Conf.Port}]"));
|
||||||
|
|
||||||
if (e.GetType() == typeof(DvarException))
|
if (e.GetType() == typeof(DvarException))
|
||||||
{
|
{
|
||||||
Logger.WriteDebug($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"]} {(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// throw the exception to the main method to stop before instantly exiting
|
lastException = e;
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
|
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
|
||||||
#endregion
|
|
||||||
|
if (successServers == 0)
|
||||||
|
{
|
||||||
|
throw lastException;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successServers != config.Servers.Count)
|
||||||
|
{
|
||||||
|
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
|
||||||
|
{
|
||||||
|
throw lastException;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
try
|
||||||
{
|
{
|
||||||
await Heartbeat.Send(this, true);
|
await Heartbeat.Send(this, true);
|
||||||
heartbeatState.Connected = true;
|
connected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
heartbeatState.Connected = false;
|
connected = false;
|
||||||
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
|
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -438,7 +629,7 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
{
|
{
|
||||||
heartbeatState.Connected = false;
|
connected = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -448,36 +639,45 @@ namespace IW4MAdmin.Application
|
|||||||
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
||||||
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
{
|
{
|
||||||
heartbeatState.Connected = false;
|
connected = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
await Task.Delay(30000);
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(30000, _tokenSource.Token);
|
||||||
|
}
|
||||||
|
catch { break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start()
|
public async Task Start()
|
||||||
{
|
{
|
||||||
// this needs to be run seperately from the main thread
|
await Task.WhenAll(new[]
|
||||||
var _ = Task.Run(() => SendHeartbeat(new HeartbeatState()));
|
|
||||||
_ = Task.Run(() => UpdateServerStates());
|
|
||||||
|
|
||||||
while (Running)
|
|
||||||
{
|
{
|
||||||
OnQuit.Wait();
|
SendHeartbeat(),
|
||||||
OnQuit.Reset();
|
UpdateServerStates()
|
||||||
}
|
});
|
||||||
_servers.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
|
_tokenSource.Cancel();
|
||||||
Running = false;
|
Running = false;
|
||||||
|
Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Restart()
|
||||||
|
{
|
||||||
|
IsRestartRequested = true;
|
||||||
|
Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ILogger GetLogger(long serverId)
|
public ILogger GetLogger(long serverId)
|
||||||
@ -535,29 +735,14 @@ namespace IW4MAdmin.Application
|
|||||||
return ConfigHandler;
|
return ConfigHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDictionary<int, EFClient> GetPrivilegedClients()
|
|
||||||
{
|
|
||||||
return PrivilegedClients;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShutdownRequested()
|
|
||||||
{
|
|
||||||
return !Running;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEventHandler GetEventHandler()
|
public IEventHandler GetEventHandler()
|
||||||
{
|
{
|
||||||
return Handler;
|
return Handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetHasEvent()
|
|
||||||
{
|
|
||||||
OnQuit.Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<Assembly> GetPluginAssemblies()
|
public IList<Assembly> GetPluginAssemblies()
|
||||||
{
|
{
|
||||||
return SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies;
|
return SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies.Union(SharedLibraryCore.Plugins.PluginImporter.Assemblies).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPageList GetPageList()
|
public IPageList GetPageList()
|
||||||
|
@ -97,11 +97,25 @@ if "%CurrentConfiguration%" == "Release" (
|
|||||||
if exist "%SolutionDir%Publish\Windows\refs" move "%SolutionDir%Publish\Windows\refs" "%SolutionDir%Publish\Windows\Lib\refs"
|
if exist "%SolutionDir%Publish\Windows\refs" move "%SolutionDir%Publish\Windows\refs" "%SolutionDir%Publish\Windows\Lib\refs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "%CurrentConfiguration%" == "Prerelease" (
|
||||||
|
echo PR-LOC
|
||||||
|
if not exist "%SolutionDir%Publish\WindowsPrerelease\Localization" md "%SolutionDir%Publish\WindowsPrerelease\Localization"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%CurrentConfiguration%" == "Release" (
|
||||||
|
echo R-LOC
|
||||||
|
if not exist "%SolutionDir%Publish\Windows\Localization" md "%SolutionDir%Publish\Windows\Localization"
|
||||||
|
)
|
||||||
|
|
||||||
echo making start scripts
|
echo making start scripts
|
||||||
@(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\WindowsPrerelease\StartIW4MAdmin.cmd"
|
||||||
@(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\Windows\StartIW4MAdmin.cmd"
|
||||||
|
|
||||||
@(echo #!/bin/bash && echo dotnet Lib\IW4MAdmin.dll) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.sh"
|
@(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.sh"
|
||||||
@(echo #!/bin/bash && echo dotnet Lib\IW4MAdmin.dll) > "%SolutionDir%Publish\Windows\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 "%CurrentConfiguration%"
|
echo setting permissions...
|
||||||
|
cacls "%SolutionDir%Publish\WindowsPrerelease" /t /e /p Everyone:F
|
||||||
|
cacls "%SolutionDir%Publish\Windows" /t /e /p Everyone:F
|
@ -16,7 +16,231 @@
|
|||||||
"Keep grenade launcher use to a minimum",
|
"Keep grenade launcher use to a minimum",
|
||||||
"Balance teams at ALL times"
|
"Balance teams at ALL times"
|
||||||
],
|
],
|
||||||
|
"DisallowedClientNames": [ "Unknown Soldier", "VickNet", "UnknownSoldier", "CHEATER", "Play77" ],
|
||||||
|
"QuickMessages": [
|
||||||
|
{
|
||||||
|
"Game": "IW4",
|
||||||
|
"Messages": {
|
||||||
|
"QUICKMESSAGE_AREA_SECURE": "Area secure!",
|
||||||
|
"QUICKMESSAGE_ARE_YOU_CRAZY": "Are you crazy?",
|
||||||
|
"QUICKMESSAGE_ATTACK_LEFT_FLANK": "Attack left flank!",
|
||||||
|
"QUICKMESSAGE_ATTACK_RIGHT_FLANK": "Attack right flank!",
|
||||||
|
"QUICKMESSAGE_COME_ON": "Come on.",
|
||||||
|
"QUICKMESSAGE_ENEMIES_SPOTTED": "Multiple contacts!",
|
||||||
|
"QUICKMESSAGE_ENEMY_DOWN": "Enemy down!",
|
||||||
|
"QUICKMESSAGE_ENEMY_GRENADE": "Enemy grenade!",
|
||||||
|
"QUICKMESSAGE_ENEMY_SPOTTED": "Contact!",
|
||||||
|
"QUICKMESSAGE_FALL_BACK": "Fall back!",
|
||||||
|
"QUICKMESSAGE_FOLLOW_ME": "On me!",
|
||||||
|
"QUICKMESSAGE_GREAT_SHOT": "Nice shot!",
|
||||||
|
"QUICKMESSAGE_GRENADE": "Grenade!",
|
||||||
|
"QUICKMESSAGE_HOLD_THIS_POSITION": "Hold this position!",
|
||||||
|
"QUICKMESSAGE_HOLD_YOUR_FIRE": "Hold your fire!",
|
||||||
|
"QUICKMESSAGE_IM_IN_POSITION": "In position.",
|
||||||
|
"QUICKMESSAGE_IM_ON_MY_WAY": "Moving.",
|
||||||
|
"QUICKMESSAGE_MOVE_IN": "Move in!",
|
||||||
|
"QUICKMESSAGE_NEED_REINFORCEMENTS": "Need reinforcements!",
|
||||||
|
"QUICKMESSAGE_NO_SIR": "Negative.",
|
||||||
|
"QUICKMESSAGE_ON_MY_WAY": "On my way.",
|
||||||
|
"QUICKMESSAGE_REGROUP": "Regroup!",
|
||||||
|
"QUICKMESSAGE_SNIPER": "Sniper!",
|
||||||
|
"QUICKMESSAGE_SORRY": "Sorry.",
|
||||||
|
"QUICKMESSAGE_SQUAD_ATTACK_LEFT_FLANK": "Squad, attack left flank!",
|
||||||
|
"QUICKMESSAGE_SQUAD_ATTACK_RIGHT_FLANK": "Squad, attack right flank!",
|
||||||
|
"QUICKMESSAGE_SQUAD_HOLD_THIS_POSITION": "Squad, hold this position!",
|
||||||
|
"QUICKMESSAGE_SQUAD_REGROUP": "Squad, regroup!",
|
||||||
|
"QUICKMESSAGE_SQUAD_STICK_TOGETHER": "Squad, stick together!",
|
||||||
|
"QUICKMESSAGE_STICK_TOGETHER": "Stick together!",
|
||||||
|
"QUICKMESSAGE_SUPPRESSING_FIRE": "Base of fire!",
|
||||||
|
"QUICKMESSAGE_TOOK_LONG_ENOUGH": "Took long enough!",
|
||||||
|
"QUICKMESSAGE_TOOK_YOU_LONG_ENOUGH": "Took you long enough!",
|
||||||
|
"QUICKMESSAGE_WATCH_SIX": "Watch your six!",
|
||||||
|
"QUICKMESSAGE_YES_SIR": "Roger.",
|
||||||
|
"QUICKMESSAGE_YOURE_CRAZY": "You're crazy!",
|
||||||
|
"QUICKMESSAGE_YOURE_NUTS": "You're nuts!",
|
||||||
|
"QUICKMESSAGE_YOU_OUTTA_YOUR_MIND": "You outta your mind?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"Maps": [
|
"Maps": [
|
||||||
|
{
|
||||||
|
"Game": "IW3",
|
||||||
|
"Maps": [
|
||||||
|
{
|
||||||
|
"Alias": "Ambush",
|
||||||
|
"Name": "mp_convoy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Backlot",
|
||||||
|
"Name": "mp_backlot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Bloc",
|
||||||
|
"Name": "mp_bloc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Bog",
|
||||||
|
"Name": "mp_bog"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Countdown",
|
||||||
|
"Name": "mp_countdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Crash",
|
||||||
|
"Name": "mp_crash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Crossfire",
|
||||||
|
"Name": "mp_crossfire"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "District",
|
||||||
|
"Name": "mp_citystreets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Downpour",
|
||||||
|
"Name": "mp_farm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Overgrown",
|
||||||
|
"Name": "mp_overgrown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Pipeline",
|
||||||
|
"Name": "mp_pipeline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Shipment",
|
||||||
|
"Name": "mp_shipment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Showdown",
|
||||||
|
"Name": "mp_showdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Strike",
|
||||||
|
"Name": "mp_strike"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Vacant",
|
||||||
|
"Name": "mp_vacant"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Wet Work",
|
||||||
|
"Name": "mp_cargoship"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Winter Crash",
|
||||||
|
"Name": "mp_crash_snow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Broadcast",
|
||||||
|
"Name": "mp_broadcast"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Creek",
|
||||||
|
"Name": "mp_creek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Chinatown",
|
||||||
|
"Name": "mp_carentan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Killhouse",
|
||||||
|
"Name": "mp_killhouse"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Game": "T4",
|
||||||
|
"Maps": [
|
||||||
|
{
|
||||||
|
"Alias": "Airfield",
|
||||||
|
"Name": "mp_airfield"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Asylum",
|
||||||
|
"Name": "mp_asylum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Castle",
|
||||||
|
"Name": "mp_castle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Cliffside",
|
||||||
|
"Name": "mp_shrine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Courtyard",
|
||||||
|
"Name": "mp_courtyard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Dome",
|
||||||
|
"Name": "mp_dome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Downfall",
|
||||||
|
"Name": "mp_downfall"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Hanger",
|
||||||
|
"Name": "mp_hangar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Makin",
|
||||||
|
"Name": "mp_makin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Outskirts",
|
||||||
|
"Name": "mp_outskirts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Roundhouse",
|
||||||
|
"Name": "mp_roundhouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Upheaval",
|
||||||
|
"Name": "mp_suburban"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Knee Deep",
|
||||||
|
"Name": "mp_kneedeep"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Nightfire",
|
||||||
|
"Name": "mp_nachtfeuer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Station",
|
||||||
|
"Name": "mp_subway"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Banzai",
|
||||||
|
"Name": "mp_kwai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Corrosion",
|
||||||
|
"Name": "mp_stalingrad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Sub Pens",
|
||||||
|
"Name": "mp_docks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Battery",
|
||||||
|
"Name": "mp_drum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Breach",
|
||||||
|
"Name": "mp_bgate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Revolution",
|
||||||
|
"Name": "mp_vodka"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Game": "IW4",
|
"Game": "IW4",
|
||||||
"Maps": [
|
"Maps": [
|
||||||
@ -242,12 +466,270 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"Name": "Village",
|
"Name": "Village",
|
||||||
"Alias": "co_hunted"
|
"Alias": "co_hunted"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Game": "T6M",
|
"Game": "T5",
|
||||||
|
"Maps": [
|
||||||
|
{
|
||||||
|
"Alias": "Array",
|
||||||
|
"Name": "mp_array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Berlin Wall",
|
||||||
|
"Name": "mp_berlinwall2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Convoy",
|
||||||
|
"Name": "mp_gridlock"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Cracked",
|
||||||
|
"Name": "mp_cracked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Crisis",
|
||||||
|
"Name": "mp_crisis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Discovery",
|
||||||
|
"Name": "mp_discovery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Drive-In",
|
||||||
|
"Name": "mp_drivein"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Firing Range",
|
||||||
|
"Name": "mp_firingrange"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Grid",
|
||||||
|
"Name": "mp_duga"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Hangar 18",
|
||||||
|
"Name": "mp_area51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Hanoi",
|
||||||
|
"Name": "mp_hanoi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Havana",
|
||||||
|
"Name": "mp_cairo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Hazard",
|
||||||
|
"Name": "mp_golfcourse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Hotel",
|
||||||
|
"Name": "mp_hotel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Jungle",
|
||||||
|
"Name": "mp_havoc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Kowloon",
|
||||||
|
"Name": "mp_kowloon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Launch",
|
||||||
|
"Name": "mp_cosmodrome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Nuketown",
|
||||||
|
"Name": "mp_nuked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Radiation",
|
||||||
|
"Name": "mp_radiation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Silo",
|
||||||
|
"Name": "mp_silo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Stadium",
|
||||||
|
"Name": "mp_stadium"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Stockpile",
|
||||||
|
"Name": "mp_outskirts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Summit",
|
||||||
|
"Name": "mp_mountain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Villa",
|
||||||
|
"Name": "mp_villa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "WMD",
|
||||||
|
"Name": "mp_russianbase"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Zoo",
|
||||||
|
"Name": "mp_zoo"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Game": "IW5",
|
||||||
|
"Maps": [
|
||||||
|
{
|
||||||
|
"Alias": "Seatown",
|
||||||
|
"Name": "mp_seatown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Lockdown",
|
||||||
|
"Name": "mp_alpha"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Mission",
|
||||||
|
"Name": "mp_bravo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Carbon",
|
||||||
|
"Name": "mp_carbon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Dome",
|
||||||
|
"Name": "mp_dome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Arkaden",
|
||||||
|
"Name": "mp_plaza2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Downturn",
|
||||||
|
"Name": "mp_exchange"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Bootleg",
|
||||||
|
"Name": "mp_bootleg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Hardhat",
|
||||||
|
"Name": "mp_hardhat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Interchange",
|
||||||
|
"Name": "mp_interchange"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Fallen",
|
||||||
|
"Name": "mp_lambeth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Outpost",
|
||||||
|
"Name": "mp_radar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Bakaara",
|
||||||
|
"Name": "mp_mogadishu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Resistance",
|
||||||
|
"Name": "mp_paris"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Underground",
|
||||||
|
"Name": "mp_underground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Village",
|
||||||
|
"Name": "mp_village"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Aground",
|
||||||
|
"Name": "mp_aground_ss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Boardwalk",
|
||||||
|
"Name": "mp_boardwalk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "U-turn",
|
||||||
|
"Name": "mp_burn_ss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Foundation",
|
||||||
|
"Name": "mp_cement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Erosion",
|
||||||
|
"Name": "mp_courtyard_ss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Intersection",
|
||||||
|
"Name": "mp_crosswalk_ss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Getaway",
|
||||||
|
"Name": "mp_hillside_ss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Piazza",
|
||||||
|
"Name": "mp_italy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Sanctuary",
|
||||||
|
"Name": "mp_meteora"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Gulch",
|
||||||
|
"Name": "mp_moab"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Black Box",
|
||||||
|
"Name": "mp_morningwood"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Parish",
|
||||||
|
"Name": "mp_nola"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Overwatch",
|
||||||
|
"Name": "mp_overwatch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Liberation",
|
||||||
|
"Name": "mp_park"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Oasis",
|
||||||
|
"Name": "mp_qadeem"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Lookout",
|
||||||
|
"Name": "mp_restrepo_ss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Off Shore",
|
||||||
|
"Name": "mp_roughneck"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Decommission",
|
||||||
|
"Name": "mp_shipbreaker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Vortex",
|
||||||
|
"Name": "mp_six_ss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alias": "Terminal",
|
||||||
|
"Name": "mp_terminal_cls"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Game": "T6",
|
||||||
"Maps": [
|
"Maps": [
|
||||||
{
|
{
|
||||||
"Alias": "Aftermath",
|
"Alias": "Aftermath",
|
||||||
|
293
Application/EventParsers/BaseEventParser.cs
Normal file
293
Application/EventParsers/BaseEventParser.cs
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using static SharedLibraryCore.Server;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
|
{
|
||||||
|
class BaseEventParser : IEventParser
|
||||||
|
{
|
||||||
|
public BaseEventParser()
|
||||||
|
{
|
||||||
|
Configuration = new DynamicEventParserConfiguration()
|
||||||
|
{
|
||||||
|
GameDirectory = "main",
|
||||||
|
};
|
||||||
|
|
||||||
|
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32});([0-9]+);(.+);(.*)$";
|
||||||
|
Configuration.Say.AddMapping(ParserRegex.GroupType.EventType, 1);
|
||||||
|
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
|
||||||
|
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
|
||||||
|
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginName, 4);
|
||||||
|
Configuration.Say.AddMapping(ParserRegex.GroupType.Message, 5);
|
||||||
|
|
||||||
|
Configuration.Quit.Pattern = @"^(Q);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);([0-9]+);(.*)$";
|
||||||
|
Configuration.Quit.AddMapping(ParserRegex.GroupType.EventType, 1);
|
||||||
|
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
|
||||||
|
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
|
||||||
|
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginName, 4);
|
||||||
|
|
||||||
|
Configuration.Join.Pattern = @"^(J);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);([0-9]+);(.*)$";
|
||||||
|
Configuration.Join.AddMapping(ParserRegex.GroupType.EventType, 1);
|
||||||
|
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
|
||||||
|
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.AddMapping(ParserRegex.GroupType.EventType, 1);
|
||||||
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
|
||||||
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
|
||||||
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetTeam, 4);
|
||||||
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetName, 5);
|
||||||
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginNetworkId, 6);
|
||||||
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginClientNumber, 7);
|
||||||
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginTeam, 8);
|
||||||
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginName, 9);
|
||||||
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.Weapon, 10);
|
||||||
|
Configuration.Damage.AddMapping(ParserRegex.GroupType.Damage, 11);
|
||||||
|
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.AddMapping(ParserRegex.GroupType.EventType, 1);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetTeam, 4);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetName, 5);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginNetworkId, 6);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginClientNumber, 7);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginTeam, 8);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginName, 9);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.Weapon, 10);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.Damage, 11);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
|
||||||
|
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEventParserConfiguration Configuration { get; set; }
|
||||||
|
|
||||||
|
public string Version { get; set; } = "CoD";
|
||||||
|
|
||||||
|
public Game GameName { get; set; } = Game.COD;
|
||||||
|
|
||||||
|
public string URLProtocolFormat { get; set; } = "CoD://{{ip}}:{{port}}";
|
||||||
|
|
||||||
|
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 == "say" || eventType == "sayteam")
|
||||||
|
{
|
||||||
|
var matchResult = Regex.Match(logLine, Configuration.Say.Pattern);
|
||||||
|
|
||||||
|
if (matchResult.Success)
|
||||||
|
{
|
||||||
|
string message = matchResult
|
||||||
|
.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
|
||||||
|
.ToString()
|
||||||
|
.Replace("\x15", "")
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
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.Say,
|
||||||
|
Data = message,
|
||||||
|
Origin = new EFClient() { NetworkId = originId },
|
||||||
|
Message = message,
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType == "K")
|
||||||
|
{
|
||||||
|
var match = Regex.Match(logLine, Configuration.Kill.Pattern);
|
||||||
|
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
return new GameEvent()
|
||||||
|
{
|
||||||
|
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 == "D")
|
||||||
|
{
|
||||||
|
var regexMatch = Regex.Match(logLine, Configuration.Damage.Pattern);
|
||||||
|
|
||||||
|
if (regexMatch.Success)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
return new GameEvent()
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.Damage,
|
||||||
|
Data = logLine,
|
||||||
|
Origin = new EFClient() { NetworkId = originId },
|
||||||
|
Target = new EFClient() { NetworkId = targetId },
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType == "J")
|
||||||
|
{
|
||||||
|
var regexMatch = Regex.Match(logLine, Configuration.Join.Pattern);
|
||||||
|
|
||||||
|
if (regexMatch.Success)
|
||||||
|
{
|
||||||
|
return new GameEvent()
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.PreConnect,
|
||||||
|
Data = logLine,
|
||||||
|
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().ConvertGuidToLong(),
|
||||||
|
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
|
||||||
|
State = EFClient.ClientState.Connecting,
|
||||||
|
},
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType == "Q")
|
||||||
|
{
|
||||||
|
var regexMatch = Regex.Match(logLine, Configuration.Quit.Pattern);
|
||||||
|
if (regexMatch.Success)
|
||||||
|
{
|
||||||
|
return new GameEvent()
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.PreDisconnect,
|
||||||
|
Data = logLine,
|
||||||
|
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().ConvertGuidToLong(),
|
||||||
|
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
|
||||||
|
State = EFClient.ClientState.Disconnecting
|
||||||
|
},
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.Origin
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType.Contains("ExitLevel"))
|
||||||
|
{
|
||||||
|
return new GameEvent()
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.MapEnd,
|
||||||
|
Data = lineSplit[0],
|
||||||
|
Origin = Utilities.IW4MAdminClient(),
|
||||||
|
Target = Utilities.IW4MAdminClient(),
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType.Contains("InitGame"))
|
||||||
|
{
|
||||||
|
string dump = eventType.Replace("InitGame: ", "");
|
||||||
|
|
||||||
|
return new GameEvent()
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.MapChange,
|
||||||
|
Data = lineSplit[0],
|
||||||
|
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(),
|
||||||
|
Target = Utilities.IW4MAdminClient(),
|
||||||
|
RequiredEntity = GameEvent.EventRequiredEntity.None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,11 @@ using static SharedLibraryCore.Server;
|
|||||||
|
|
||||||
namespace IW4MAdmin.Application.EventParsers
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
{
|
{
|
||||||
sealed internal class DynamicEventParser : IW4EventParser
|
/// <summary>
|
||||||
|
/// empty generic implementation of the IEventParserConfiguration
|
||||||
|
/// allows script plugins to generate dynamic event parsers
|
||||||
|
/// </summary>
|
||||||
|
sealed internal class DynamicEventParser : BaseEventParser
|
||||||
{
|
{
|
||||||
public string Version { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
namespace IW4MAdmin.Application.EventParsers
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
{
|
{
|
||||||
class DynamicEventParserConfiguration : IEventParserConfiguration
|
/// <summary>
|
||||||
|
/// generic implementation of the IEventParserConfiguration
|
||||||
|
/// allows script plugins to generate dynamic configurations
|
||||||
|
/// </summary>
|
||||||
|
sealed internal class DynamicEventParserConfiguration : IEventParserConfiguration
|
||||||
{
|
{
|
||||||
public string GameDirectory { get; set; }
|
public string GameDirectory { get; set; }
|
||||||
|
|
||||||
public ParserRegex Say { get; set; } = new ParserRegex();
|
public ParserRegex Say { get; set; } = new ParserRegex();
|
||||||
public ParserRegex Join { get; set; } = new ParserRegex();
|
public ParserRegex Join { get; set; } = new ParserRegex();
|
||||||
public ParserRegex Quit { get; set; } = new ParserRegex();
|
public ParserRegex Quit { get; set; } = new ParserRegex();
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.EventParsers
|
|
||||||
{
|
|
||||||
class IW3EventParser : IW4EventParser
|
|
||||||
{
|
|
||||||
public IW3EventParser() : base()
|
|
||||||
{
|
|
||||||
Configuration.GameDirectory = "main";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,298 +0,0 @@
|
|||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Database.Models;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.EventParsers
|
|
||||||
{
|
|
||||||
class IW4EventParser : IEventParser
|
|
||||||
{
|
|
||||||
public IW4EventParser()
|
|
||||||
{
|
|
||||||
Configuration = new DynamicEventParserConfiguration()
|
|
||||||
{
|
|
||||||
GameDirectory = "userraw",
|
|
||||||
};
|
|
||||||
|
|
||||||
Configuration.Say.Pattern = @"^(say|sayteam);(.{1,32});([0-9]+)(.*);(.*)$";
|
|
||||||
Configuration.Say.GroupMapping.Add(ParserRegex.GroupType.EventType, 1);
|
|
||||||
Configuration.Say.GroupMapping.Add(ParserRegex.GroupType.OriginNetworkId, 2);
|
|
||||||
Configuration.Say.GroupMapping.Add(ParserRegex.GroupType.OriginClientNumber, 3);
|
|
||||||
Configuration.Say.GroupMapping.Add(ParserRegex.GroupType.OriginName, 4);
|
|
||||||
Configuration.Say.GroupMapping.Add(ParserRegex.GroupType.Message, 5);
|
|
||||||
|
|
||||||
Configuration.Quit.Pattern = @"^(Q);(.{16,32}|bot[0-9]+);([0-9]+);(.*)$";
|
|
||||||
Configuration.Quit.GroupMapping.Add(ParserRegex.GroupType.EventType, 1);
|
|
||||||
Configuration.Quit.GroupMapping.Add(ParserRegex.GroupType.OriginNetworkId, 2);
|
|
||||||
Configuration.Quit.GroupMapping.Add(ParserRegex.GroupType.OriginClientNumber, 3);
|
|
||||||
Configuration.Quit.GroupMapping.Add(ParserRegex.GroupType.OriginName, 4);
|
|
||||||
|
|
||||||
Configuration.Join.Pattern = @"^(J);(.{16,32}|bot[0-9]+);([0-9]+);(.*)$";
|
|
||||||
Configuration.Join.GroupMapping.Add(ParserRegex.GroupType.EventType, 1);
|
|
||||||
Configuration.Join.GroupMapping.Add(ParserRegex.GroupType.OriginNetworkId, 2);
|
|
||||||
Configuration.Join.GroupMapping.Add(ParserRegex.GroupType.OriginClientNumber, 3);
|
|
||||||
Configuration.Join.GroupMapping.Add(ParserRegex.GroupType.OriginName, 4);
|
|
||||||
|
|
||||||
Configuration.Damage.Pattern = @"^(D);([A-Fa-f0-9_]{16,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world);(.{1,24});([A-Fa-f0-9_]{16,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world);(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.EventType, 1);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.TargetNetworkId, 2);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.TargetClientNumber, 3);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.TargetTeam, 4);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.TargetName, 5);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.OriginNetworkId, 6);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.OriginClientNumber, 7);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.OriginTeam, 8);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.OriginName, 9);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.Weapon, 10);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.Damage, 11);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.MeansOfDeath, 12);
|
|
||||||
Configuration.Damage.GroupMapping.Add(ParserRegex.GroupType.HitLocation, 13);
|
|
||||||
|
|
||||||
Configuration.Kill.Pattern = @"^(K);([A-Fa-f0-9_]{16,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world);(.{1,24});([A-Fa-f0-9_]{16,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world);(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.EventType, 1);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.TargetNetworkId, 2);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.TargetClientNumber, 3);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.TargetTeam, 4);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.TargetName, 5);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.OriginNetworkId, 6);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.OriginClientNumber, 7);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.OriginTeam, 8);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.OriginName, 9);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.Weapon, 10);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.Damage, 11);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.MeansOfDeath, 12);
|
|
||||||
Configuration.Kill.GroupMapping.Add(ParserRegex.GroupType.HitLocation, 13);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEventParserConfiguration Configuration { get; set; }
|
|
||||||
|
|
||||||
public virtual GameEvent GetEvent(Server server, 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 = eventType,
|
|
||||||
Origin = origin,
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == "say" || eventType == "sayteam")
|
|
||||||
{
|
|
||||||
var matchResult = Regex.Match(logLine, Configuration.Say.Pattern);
|
|
||||||
|
|
||||||
if (matchResult.Success)
|
|
||||||
{
|
|
||||||
string message = matchResult
|
|
||||||
.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
|
|
||||||
.ToString()
|
|
||||||
.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] == '@')
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Command,
|
|
||||||
Data = message,
|
|
||||||
Origin = origin,
|
|
||||||
Owner = server,
|
|
||||||
Message = message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 origin = server.GetClientsAsList()
|
|
||||||
.First(c => c.NetworkId == match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong());
|
|
||||||
var target = server.GetClientsAsList()
|
|
||||||
.First(c => c.NetworkId == match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertLong());
|
|
||||||
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Kill,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = origin,
|
|
||||||
Target = target,
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == "ScriptKill")
|
|
||||||
{
|
|
||||||
var origin = server.GetClientsAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong());
|
|
||||||
var target = server.GetClientsAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong());
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.ScriptKill,
|
|
||||||
Data = logLine,
|
|
||||||
Origin = origin,
|
|
||||||
Target = target,
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == "ScriptDamage")
|
|
||||||
{
|
|
||||||
var origin = server.GetClientsAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong());
|
|
||||||
var target = server.GetClientsAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong());
|
|
||||||
|
|
||||||
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 origin = server.GetClientsAsList()
|
|
||||||
.First(c => c.NetworkId == regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong());
|
|
||||||
var target = server.GetClientsAsList()
|
|
||||||
.First(c => c.NetworkId == regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertLong());
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Damage,
|
|
||||||
Data = eventType,
|
|
||||||
Origin = origin,
|
|
||||||
Target = target,
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// join
|
|
||||||
if (eventType == "J")
|
|
||||||
{
|
|
||||||
var regexMatch = Regex.Match(logLine, Configuration.Join.Pattern);
|
|
||||||
if (regexMatch.Success)
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.PreConnect,
|
|
||||||
Data = logLine,
|
|
||||||
Owner = server,
|
|
||||||
Origin = new EFClient()
|
|
||||||
{
|
|
||||||
CurrentAlias = new EFAlias()
|
|
||||||
{
|
|
||||||
Active = false,
|
|
||||||
Name = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().StripColors(),
|
|
||||||
},
|
|
||||||
NetworkId = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong(),
|
|
||||||
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
|
|
||||||
State = EFClient.ClientState.Connecting,
|
|
||||||
CurrentServer = server
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == "Q")
|
|
||||||
{
|
|
||||||
var regexMatch = Regex.Match(logLine, Configuration.Quit.Pattern);
|
|
||||||
if (regexMatch.Success)
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.PreDisconnect,
|
|
||||||
Data = logLine,
|
|
||||||
Owner = server,
|
|
||||||
Origin = new EFClient()
|
|
||||||
{
|
|
||||||
CurrentAlias = new EFAlias()
|
|
||||||
{
|
|
||||||
Active = false,
|
|
||||||
Name = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().StripColors()
|
|
||||||
},
|
|
||||||
NetworkId = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertLong(),
|
|
||||||
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
|
|
||||||
State = EFClient.ClientState.Disconnecting
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType.Contains("ExitLevel"))
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.MapEnd,
|
|
||||||
Data = lineSplit[0],
|
|
||||||
Origin = Utilities.IW4MAdminClient(server),
|
|
||||||
Target = Utilities.IW4MAdminClient(server),
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType.Contains("InitGame"))
|
|
||||||
{
|
|
||||||
string dump = eventType.Replace("InitGame: ", "");
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.MapChange,
|
|
||||||
Data = lineSplit[0],
|
|
||||||
Origin = Utilities.IW4MAdminClient(server),
|
|
||||||
Target = Utilities.IW4MAdminClient(server),
|
|
||||||
Owner = server,
|
|
||||||
Extra = dump.DictionaryFromKeyValue()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Unknown,
|
|
||||||
Origin = Utilities.IW4MAdminClient(server),
|
|
||||||
Target = Utilities.IW4MAdminClient(server),
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.EventParsers
|
|
||||||
{
|
|
||||||
class T5MEventParser : IW4EventParser
|
|
||||||
{
|
|
||||||
public T5MEventParser() : base()
|
|
||||||
{
|
|
||||||
Configuration.GameDirectory = "v2";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.EventParsers
|
|
||||||
{
|
|
||||||
class T6MEventParser : IW4EventParser
|
|
||||||
{
|
|
||||||
public T6MEventParser() : base()
|
|
||||||
{
|
|
||||||
Configuration.GameDirectory = $"t6r{Path.DirectorySeparatorChar}data";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +1,26 @@
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Events;
|
using SharedLibraryCore.Events;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
class GameEventHandler : IEventHandler
|
class GameEventHandler : IEventHandler
|
||||||
{
|
{
|
||||||
readonly IManager Manager;
|
readonly ApplicationManager Manager;
|
||||||
public GameEventHandler(IManager mgr)
|
public GameEventHandler(IManager mgr)
|
||||||
{
|
{
|
||||||
Manager = mgr;
|
Manager = (ApplicationManager)mgr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddEvent(GameEvent gameEvent)
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -10,10 +8,10 @@ namespace IW4MAdmin.Application.IO
|
|||||||
{
|
{
|
||||||
class GameLogEventDetection
|
class GameLogEventDetection
|
||||||
{
|
{
|
||||||
Server Server;
|
private long previousFileSize;
|
||||||
long PreviousFileSize;
|
private readonly Server _server;
|
||||||
IGameLogReader Reader;
|
private readonly IGameLogReader _reader;
|
||||||
readonly string GameLogFile;
|
private readonly string _gameLogFile;
|
||||||
|
|
||||||
class EventState
|
class EventState
|
||||||
{
|
{
|
||||||
@ -21,27 +19,20 @@ namespace IW4MAdmin.Application.IO
|
|||||||
public string ServerId { get; set; }
|
public string ServerId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameLogEventDetection(Server server, string gameLogPath, string gameLogName)
|
public GameLogEventDetection(Server server, string gameLogPath, Uri gameLogServerUri)
|
||||||
{
|
{
|
||||||
GameLogFile = gameLogPath;
|
_gameLogFile = gameLogPath;
|
||||||
// todo: abtract this more
|
_reader = gameLogServerUri != null ? new GameLogReaderHttp(gameLogServerUri, gameLogPath, server.EventParser) : _reader = new GameLogReader(gameLogPath, server.EventParser);
|
||||||
if (gameLogPath.StartsWith("http"))
|
_server = server;
|
||||||
{
|
|
||||||
Reader = new GameLogReaderHttp(gameLogPath, server.EventParser);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Reader = new GameLogReader(gameLogPath, server.EventParser);
|
|
||||||
}
|
|
||||||
|
|
||||||
Server = server;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PollForChanges()
|
public async Task PollForChanges()
|
||||||
{
|
{
|
||||||
while (!Server.Manager.ShutdownRequested())
|
while (!_server.Manager.CancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if ((Server.Manager as ApplicationManager).IsInitialized)
|
#if !DEBUG
|
||||||
|
if (_server.IsInitialized)
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -50,39 +41,40 @@ namespace IW4MAdmin.Application.IO
|
|||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Server.Logger.WriteWarning($"Failed to update log event for {Server.EndPoint}");
|
_server.Logger.WriteWarning($"Failed to update log event for {_server.EndPoint}");
|
||||||
Server.Logger.WriteDebug($"Exception: {e.Message}");
|
_server.Logger.WriteDebug(e.GetExceptionInfo());
|
||||||
Server.Logger.WriteDebug($"StackTrace: {e.StackTrace}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Thread.Sleep(Reader.UpdateInterval);
|
|
||||||
|
await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_server.Logger.WriteDebug("Stopped polling for changes");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateLogEvents()
|
private async Task UpdateLogEvents()
|
||||||
{
|
{
|
||||||
long fileSize = Reader.Length;
|
long fileSize = _reader.Length;
|
||||||
|
|
||||||
if (PreviousFileSize == 0)
|
if (previousFileSize == 0)
|
||||||
PreviousFileSize = fileSize;
|
{
|
||||||
|
previousFileSize = fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
long fileDiff = fileSize - PreviousFileSize;
|
long fileDiff = fileSize - previousFileSize;
|
||||||
|
|
||||||
// this makes the http log get pulled
|
// this makes the http log get pulled
|
||||||
if (fileDiff < 1 && fileSize != -1)
|
if (fileDiff < 1 && fileSize != -1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PreviousFileSize = fileSize;
|
var events = await _reader.ReadEventsFromLog(_server, fileDiff, previousFileSize);
|
||||||
|
|
||||||
var events = await Reader.ReadEventsFromLog(Server, fileDiff, 0);
|
|
||||||
|
|
||||||
foreach (var ev in events)
|
foreach (var ev in events)
|
||||||
{
|
{
|
||||||
Server.Manager.GetEventHandler().AddEvent(ev);
|
_server.Manager.GetEventHandler().AddEvent(ev);
|
||||||
await ev.WaitAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PreviousFileSize = fileSize;
|
previousFileSize = fileSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using SharedLibraryCore.Interfaces;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ namespace IW4MAdmin.Application.IO
|
|||||||
{
|
{
|
||||||
IEventParser Parser;
|
IEventParser Parser;
|
||||||
readonly string LogFile;
|
readonly string LogFile;
|
||||||
|
private bool? ignoreBots;
|
||||||
|
|
||||||
public long Length => new FileInfo(LogFile).Length;
|
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)
|
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
|
// allocate the bytes for the new log lines
|
||||||
List<string> logLines = new List<string>();
|
List<string> logLines = new List<string>();
|
||||||
|
|
||||||
// open the file as a stream
|
// 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
|
byte[] buff = new byte[fileSizeDiff];
|
||||||
// take the old start position and go back the number of new characters
|
fs.Seek(startPosition, SeekOrigin.Begin);
|
||||||
rd.BaseStream.Seek(-fileSizeDiff, SeekOrigin.End);
|
await fs.ReadAsync(buff, 0, (int)fileSizeDiff, server.Manager.CancellationToken);
|
||||||
// the difference should be in the range of a int :P
|
var stringBuilder = new StringBuilder();
|
||||||
string newLine;
|
char[] charBuff = Utilities.EncodingType.GetChars(buff);
|
||||||
while (!String.IsNullOrEmpty(newLine = rd.ReadLine()))
|
|
||||||
|
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
|
try
|
||||||
{
|
{
|
||||||
// todo: catch elsewhere
|
var gameEvent = Parser.GenerateGameEvent(eventLine);
|
||||||
events.Add(Parser.GetEvent(server, 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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
server.Logger.WriteWarning("Could not properly parse event line");
|
server.Logger.WriteWarning("Could not properly parse event line");
|
||||||
|
@ -4,6 +4,7 @@ using SharedLibraryCore;
|
|||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using static SharedLibraryCore.Utilities;
|
using static SharedLibraryCore.Utilities;
|
||||||
@ -17,53 +18,99 @@ namespace IW4MAdmin.Application.IO
|
|||||||
{
|
{
|
||||||
readonly IEventParser Parser;
|
readonly IEventParser Parser;
|
||||||
readonly IGameLogServer Api;
|
readonly IGameLogServer Api;
|
||||||
readonly string LogFile;
|
readonly string logPath;
|
||||||
|
private bool? ignoreBots;
|
||||||
|
private string lastKey = "next";
|
||||||
|
|
||||||
public GameLogReaderHttp(string logFile, IEventParser parser)
|
public GameLogReaderHttp(Uri gameLogServerUri, string logPath, IEventParser parser)
|
||||||
{
|
{
|
||||||
LogFile = logFile;
|
this.logPath = logPath.ToBase64UrlSafeString(); ;
|
||||||
Parser = parser;
|
Parser = parser;
|
||||||
Api = RestClient.For<IGameLogServer>(logFile);
|
Api = RestClient.For<IGameLogServer>(gameLogServerUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long Length => -1;
|
public long Length => -1;
|
||||||
|
|
||||||
public int UpdateInterval => 350;
|
public int UpdateInterval => 500;
|
||||||
|
|
||||||
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
|
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
|
||||||
{
|
{
|
||||||
#if DEBUG == true
|
if (!ignoreBots.HasValue)
|
||||||
server.Logger.WriteDebug($"Begin reading from http log");
|
|
||||||
#endif
|
|
||||||
var events = new List<GameEvent>();
|
|
||||||
string b64Path = server.LogPath.ToBase64UrlSafeString();
|
|
||||||
var response = await Api.Log(b64Path);
|
|
||||||
|
|
||||||
if (!response.Success)
|
|
||||||
{
|
{
|
||||||
server.Logger.WriteError($"Could not get log server info of {LogFile}/{b64Path} ({server.LogPath})");
|
ignoreBots = server.Manager.GetApplicationSettings().Configuration().IgnoreBots;
|
||||||
|
}
|
||||||
|
|
||||||
|
var events = new List<GameEvent>();
|
||||||
|
string b64Path = logPath;
|
||||||
|
var response = await Api.Log(b64Path, lastKey);
|
||||||
|
lastKey = response.NextKey;
|
||||||
|
|
||||||
|
if (!response.Success && string.IsNullOrEmpty(lastKey))
|
||||||
|
{
|
||||||
|
server.Logger.WriteError($"Could not get log server info of {logPath}/{b64Path} ({server.LogPath})");
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse each line
|
else if (!string.IsNullOrWhiteSpace(response.Data))
|
||||||
foreach (string eventLine in response.Data.Split(Environment.NewLine))
|
|
||||||
{
|
{
|
||||||
if (eventLine.Length > 0)
|
#if DEBUG
|
||||||
{
|
server.Manager.GetLogger(0).WriteInfo(response.Data);
|
||||||
try
|
|
||||||
{
|
|
||||||
var e = Parser.GetEvent(server, eventLine);
|
|
||||||
#if DEBUG == true
|
|
||||||
server.Logger.WriteDebug($"Parsed event with id {e.Id} from http");
|
|
||||||
#endif
|
#endif
|
||||||
events.Add(e);
|
// parse each line
|
||||||
}
|
foreach (string eventLine in response.Data.Split(Environment.NewLine))
|
||||||
|
{
|
||||||
catch (Exception e)
|
if (eventLine.Length > 0)
|
||||||
{
|
{
|
||||||
server.Logger.WriteWarning("Could not properly parse event line");
|
try
|
||||||
server.Logger.WriteDebug(e.Message);
|
{
|
||||||
server.Logger.WriteDebug(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);
|
||||||
|
}
|
||||||
|
#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,10 @@ using SharedLibraryCore.Configuration;
|
|||||||
using SharedLibraryCore.Database.Models;
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Dtos;
|
using SharedLibraryCore.Dtos;
|
||||||
using SharedLibraryCore.Exceptions;
|
using SharedLibraryCore.Exceptions;
|
||||||
|
using SharedLibraryCore.Helpers;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using SharedLibraryCore.Localization;
|
using SharedLibraryCore.Localization;
|
||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Services;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -17,6 +18,7 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static SharedLibraryCore.Database.Models.EFClient;
|
||||||
|
|
||||||
namespace IW4MAdmin
|
namespace IW4MAdmin
|
||||||
{
|
{
|
||||||
@ -24,7 +26,7 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
private static readonly Index loc = Utilities.CurrentLocalization.LocalizationIndex;
|
private static readonly Index loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
private GameLogEventDetection LogEvent;
|
private GameLogEventDetection LogEvent;
|
||||||
private DateTime SessionStart;
|
|
||||||
public int Id { get; private set; }
|
public int Id { get; private set; }
|
||||||
|
|
||||||
public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg)
|
public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg)
|
||||||
@ -34,85 +36,71 @@ namespace IW4MAdmin
|
|||||||
override public async Task OnClientConnected(EFClient clientFromLog)
|
override public async Task OnClientConnected(EFClient clientFromLog)
|
||||||
{
|
{
|
||||||
Logger.WriteDebug($"Client slot #{clientFromLog.ClientNumber} now reserved");
|
Logger.WriteDebug($"Client slot #{clientFromLog.ClientNumber} now reserved");
|
||||||
Clients[clientFromLog.ClientNumber] = new EFClient();
|
|
||||||
|
|
||||||
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;
|
||||||
// first time client is connecting to server
|
client = await Manager.GetClientService().Create(clientFromLog);
|
||||||
if (client == null)
|
|
||||||
{
|
|
||||||
Logger.WriteDebug($"Client {clientFromLog} first time connecting");
|
|
||||||
client = await Manager.GetClientService().Create(clientFromLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
// client has connected in the past
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 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...");
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
client.State = EFClient.ClientState.Connected;
|
|
||||||
#if DEBUG == true
|
|
||||||
Logger.WriteDebug($"End PreConnect for {client}");
|
|
||||||
#endif
|
|
||||||
var e = new GameEvent()
|
|
||||||
{
|
|
||||||
Origin = client,
|
|
||||||
Owner = this,
|
|
||||||
Type = GameEvent.EventType.Connect
|
|
||||||
};
|
|
||||||
|
|
||||||
Manager.GetEventHandler().AddEvent(e);
|
|
||||||
|
|
||||||
if (client.IPAddress != null)
|
|
||||||
{
|
|
||||||
await client.OnJoin(client.IPAddress);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception ex)
|
/// this is only a temporary version until the IPAddress is transmitted
|
||||||
|
client.CurrentAlias = new EFAlias()
|
||||||
{
|
{
|
||||||
Logger.WriteError($"{loc["SERVER_ERROR_ADDPLAYER"]} {clientFromLog}");
|
Name = clientFromLog.Name,
|
||||||
Logger.WriteError(ex.GetExceptionInfo());
|
IPAddress = clientFromLog.IPAddress
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
override public async Task OnClientDisconnected(EFClient client)
|
Logger.WriteInfo($"Client {client} connected...");
|
||||||
{
|
|
||||||
Logger.WriteInfo($"Client {client} [{client.State.ToString().ToLower()}] disconnecting...");
|
// Do the player specific stuff
|
||||||
await client.OnDisconnect();
|
client.ClientNumber = clientFromLog.ClientNumber;
|
||||||
Clients[client.ClientNumber] = null;
|
client.Score = clientFromLog.Score;
|
||||||
|
client.Ping = clientFromLog.Ping;
|
||||||
|
client.CurrentServer = this;
|
||||||
|
|
||||||
|
Clients[client.ClientNumber] = client;
|
||||||
#if DEBUG == true
|
#if DEBUG == true
|
||||||
Logger.WriteDebug($"End PreDisconnect for {client}");
|
Logger.WriteDebug($"End PreConnect for {client}");
|
||||||
#endif
|
#endif
|
||||||
var e = new GameEvent()
|
var e = new GameEvent()
|
||||||
{
|
{
|
||||||
Origin = client,
|
Origin = client,
|
||||||
Owner = this,
|
Owner = this,
|
||||||
Type = GameEvent.EventType.Disconnect
|
Type = GameEvent.EventType.Connect
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await client.OnJoin(client.IPAddress);
|
||||||
|
client.State = ClientState.Connected;
|
||||||
Manager.GetEventHandler().AddEvent(e);
|
Manager.GetEventHandler().AddEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override public async Task OnClientDisconnected(EFClient client)
|
||||||
|
{
|
||||||
|
#if DEBUG == true
|
||||||
|
if (client.ClientNumber >= 0)
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
Manager.GetEventHandler().AddEvent(e);
|
||||||
|
#if DEBUG == true
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task ExecuteEvent(GameEvent E)
|
public override async Task ExecuteEvent(GameEvent E)
|
||||||
{
|
{
|
||||||
bool canExecuteCommand = true;
|
bool canExecuteCommand = true;
|
||||||
@ -176,28 +164,85 @@ namespace IW4MAdmin
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
override protected async Task<bool> ProcessEvent(GameEvent E)
|
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;
|
||||||
|
Logger.WriteError(exception.Message);
|
||||||
|
if (exception.Data["internal_exception"] != null)
|
||||||
|
{
|
||||||
|
Logger.WriteDebug($"Internal Exception: {exception.Data["internal_exception"]}");
|
||||||
|
}
|
||||||
|
Logger.WriteInfo("Connection lost to server, so we are throttling the poll rate");
|
||||||
|
Throttled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (E.Type == GameEvent.EventType.ConnectionRestored)
|
||||||
|
{
|
||||||
|
if (Throttled)
|
||||||
|
{
|
||||||
|
Logger.WriteVerbose(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
|
||||||
|
}
|
||||||
|
Logger.WriteInfo("Connection restored to server, so we are no longer throttling the poll rate");
|
||||||
|
Throttled = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.ChangePermission)
|
if (E.Type == GameEvent.EventType.ChangePermission)
|
||||||
{
|
{
|
||||||
if (!E.Target.IsPrivileged())
|
var newPermission = (Permission)E.Extra;
|
||||||
{
|
Logger.WriteInfo($"{E.Origin} is setting {E.Target} to permission level {newPermission}");
|
||||||
// remove banned or demoted privileged user
|
await Manager.GetClientService().UpdateLevel(newPermission, E.Target, E.Origin);
|
||||||
Manager.GetPrivilegedClients().Remove(E.Target.ClientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Manager.GetPrivilegedClients()[E.Target.ClientId] = E.Target;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.PreConnect)
|
else if (E.Type == GameEvent.EventType.PreConnect)
|
||||||
{
|
{
|
||||||
|
// we don't want to track bots in the database at all if ignore bots is requested
|
||||||
|
if (E.Origin.IsBot && Manager.GetApplicationSettings().Configuration().IgnoreBots)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingClient = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
|
||||||
|
|
||||||
|
// they're already connected
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (Clients[E.Origin.ClientNumber] == null)
|
||||||
{
|
{
|
||||||
#if DEBUG == true
|
#if DEBUG == true
|
||||||
Logger.WriteDebug($"Begin PreConnect for {E.Origin}");
|
Logger.WriteDebug($"Begin PreConnect for {E.Origin}");
|
||||||
#endif
|
#endif
|
||||||
await OnClientConnected(E.Origin);
|
// we can go ahead and put them in so that they don't get re added
|
||||||
|
Clients[E.Origin.ClientNumber] = 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()
|
ChatHistory.Add(new ChatInfo()
|
||||||
{
|
{
|
||||||
@ -212,19 +257,27 @@ namespace IW4MAdmin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for some reason there's still a client in the spot
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return false;
|
Logger.WriteWarning($"{E.Origin} is connecting but {Clients[E.Origin.ClientNumber]} is currently in that client slot");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.Flag)
|
else if (E.Type == GameEvent.EventType.Flag)
|
||||||
{
|
{
|
||||||
// todo: maybe move this to a seperate function
|
DateTime? expires = null;
|
||||||
Penalty newPenalty = new Penalty()
|
|
||||||
|
if (E.Extra is TimeSpan ts)
|
||||||
{
|
{
|
||||||
Type = Penalty.PenaltyType.Flag,
|
expires = DateTime.UtcNow + ts;
|
||||||
Expires = DateTime.UtcNow,
|
}
|
||||||
|
|
||||||
|
// todo: maybe move this to a seperate function
|
||||||
|
var newPenalty = new EFPenalty()
|
||||||
|
{
|
||||||
|
Type = EFPenalty.PenaltyType.Flag,
|
||||||
|
Expires = expires,
|
||||||
Offender = E.Target,
|
Offender = E.Target,
|
||||||
Offense = E.Data,
|
Offense = E.Data,
|
||||||
Punisher = E.Origin,
|
Punisher = E.Origin,
|
||||||
@ -233,22 +286,49 @@ namespace IW4MAdmin
|
|||||||
};
|
};
|
||||||
|
|
||||||
var addedPenalty = await Manager.GetPenaltyService().Create(newPenalty);
|
var addedPenalty = await Manager.GetPenaltyService().Create(newPenalty);
|
||||||
await Manager.GetClientService().Update(E.Target);
|
E.Target.SetLevel(Permission.Flagged, E.Origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.Unflag)
|
else if (E.Type == GameEvent.EventType.Unflag)
|
||||||
{
|
{
|
||||||
await Manager.GetClientService().Update(E.Target);
|
var unflagPenalty = new EFPenalty()
|
||||||
|
{
|
||||||
|
Type = EFPenalty.PenaltyType.Unflag,
|
||||||
|
Expires = DateTime.UtcNow,
|
||||||
|
Offender = E.Target,
|
||||||
|
Offense = E.Data,
|
||||||
|
Punisher = E.Origin,
|
||||||
|
When = DateTime.UtcNow,
|
||||||
|
Link = E.Target.AliasLink
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
else if (E.Type == GameEvent.EventType.Report)
|
||||||
{
|
{
|
||||||
this.Reports.Add(new Report()
|
Reports.Add(new Report()
|
||||||
{
|
{
|
||||||
Origin = E.Origin,
|
Origin = E.Origin,
|
||||||
Target = E.Target,
|
Target = E.Target,
|
||||||
Reason = E.Data
|
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)
|
else if (E.Type == GameEvent.EventType.TempBan)
|
||||||
@ -277,37 +357,21 @@ namespace IW4MAdmin
|
|||||||
await Warn(E.Data, E.Target, E.Origin);
|
await Warn(E.Data, E.Target, E.Origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.Quit)
|
else if (E.Type == GameEvent.EventType.Disconnect)
|
||||||
{
|
{
|
||||||
var origin = GetClientsAsList().FirstOrDefault(_client => _client.NetworkId.Equals(E.Origin));
|
ChatHistory.Add(new ChatInfo()
|
||||||
|
|
||||||
if (origin != null)
|
|
||||||
{
|
{
|
||||||
var e = new GameEvent()
|
Name = E.Origin.Name,
|
||||||
{
|
Message = "DISCONNECTED",
|
||||||
Type = GameEvent.EventType.Disconnect,
|
Time = DateTime.UtcNow
|
||||||
Origin = origin,
|
});
|
||||||
Owner = this
|
|
||||||
};
|
|
||||||
|
|
||||||
Manager.GetEventHandler().AddEvent(e);
|
await new MetaService().AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin);
|
||||||
}
|
await new MetaService().AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin);
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.PreDisconnect)
|
else if (E.Type == GameEvent.EventType.PreDisconnect)
|
||||||
{
|
{
|
||||||
if ((DateTime.UtcNow - SessionStart).TotalSeconds < 30)
|
|
||||||
{
|
|
||||||
Logger.WriteInfo($"Cancelling pre disconnect for {E.Origin} as it occured too close to map end");
|
|
||||||
E.FailReason = GameEvent.EventFailReason.Invalid;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// predisconnect comes from minimal rcon polled players and minimal log players
|
// predisconnect comes from minimal rcon polled players and minimal log players
|
||||||
// so we need to disconnect the "full" version of the client
|
// so we need to disconnect the "full" version of the client
|
||||||
var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
|
var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(E.Origin));
|
||||||
@ -317,18 +381,16 @@ namespace IW4MAdmin
|
|||||||
#if DEBUG == true
|
#if DEBUG == true
|
||||||
Logger.WriteDebug($"Begin PreDisconnect for {client}");
|
Logger.WriteDebug($"Begin PreDisconnect for {client}");
|
||||||
#endif
|
#endif
|
||||||
ChatHistory.Add(new ChatInfo()
|
|
||||||
{
|
|
||||||
Name = client.Name,
|
|
||||||
Message = "DISCONNECTED",
|
|
||||||
Time = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
|
|
||||||
await OnClientDisconnected(client);
|
await OnClientDisconnected(client);
|
||||||
|
#if DEBUG == true
|
||||||
|
Logger.WriteDebug($"End PreDisconnect for {client}");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else if (client?.State != ClientState.Disconnecting)
|
||||||
{
|
{
|
||||||
|
Logger.WriteWarning($"Client {E.Origin} detected as disconnecting, but could not find them in the player list");
|
||||||
|
Logger.WriteDebug($"Expected {E.Origin} but found {GetClientsAsList().FirstOrDefault(_client => _client.ClientNumber == E.Origin.ClientNumber)}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -345,18 +407,27 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
E.Data = E.Data.StripColors();
|
E.Data = E.Data.StripColors();
|
||||||
|
|
||||||
if (E.Data.Length > 0)
|
if (E.Data?.Length > 0)
|
||||||
{
|
{
|
||||||
// this may be a fix for a hard to reproduce null exception error
|
string message = E.Data;
|
||||||
lock (ChatHistory)
|
if (E.Data.IsQuickMessage())
|
||||||
{
|
{
|
||||||
ChatHistory.Add(new ChatInfo()
|
try
|
||||||
{
|
{
|
||||||
Name = E.Origin.Name,
|
message = Manager.GetApplicationSettings().Configuration()
|
||||||
Message = E.Data ?? "NULL",
|
.QuickMessages
|
||||||
Time = DateTime.UtcNow
|
.First(_qm => _qm.Game == GameName)
|
||||||
});
|
.Messages[E.Data.Substring(1)];
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatHistory.Add(new ChatInfo()
|
||||||
|
{
|
||||||
|
Name = E.Origin.Name,
|
||||||
|
Message = message,
|
||||||
|
Time = DateTime.UtcNow
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -389,6 +460,7 @@ namespace IW4MAdmin
|
|||||||
var dict = (Dictionary<string, string>)E.Extra;
|
var dict = (Dictionary<string, string>)E.Extra;
|
||||||
Gametype = dict["g_gametype"].StripColors();
|
Gametype = dict["g_gametype"].StripColors();
|
||||||
Hostname = dict["sv_hostname"].StripColors();
|
Hostname = dict["sv_hostname"].StripColors();
|
||||||
|
MaxClients = int.Parse(dict["sv_maxclients"]);
|
||||||
|
|
||||||
string mapname = dict["mapname"].StripColors();
|
string mapname = dict["mapname"].StripColors();
|
||||||
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map()
|
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map()
|
||||||
@ -402,7 +474,6 @@ namespace IW4MAdmin
|
|||||||
if (E.Type == GameEvent.EventType.MapEnd)
|
if (E.Type == GameEvent.EventType.MapEnd)
|
||||||
{
|
{
|
||||||
Logger.WriteInfo("Game ending...");
|
Logger.WriteInfo("Game ending...");
|
||||||
SessionStart = DateTime.UtcNow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Tell)
|
if (E.Type == GameEvent.EventType.Tell)
|
||||||
@ -439,9 +510,9 @@ namespace IW4MAdmin
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task OnClientUpdate(EFClient origin)
|
private async Task OnClientUpdate(EFClient origin)
|
||||||
{
|
{
|
||||||
var client = Clients[origin.ClientNumber];
|
var client = GetClientsAsList().FirstOrDefault(_client => _client.Equals(origin));
|
||||||
|
|
||||||
if (client != null)
|
if (client != null)
|
||||||
{
|
{
|
||||||
@ -449,19 +520,29 @@ namespace IW4MAdmin
|
|||||||
client.Score = origin.Score;
|
client.Score = origin.Score;
|
||||||
|
|
||||||
// update their IP if it hasn't been set yet
|
// update their IP if it hasn't been set yet
|
||||||
if (client.IPAddress == null && !client.IsBot)
|
if (client.IPAddress == null &&
|
||||||
|
!client.IsBot &&
|
||||||
|
client.State == ClientState.Connected)
|
||||||
{
|
{
|
||||||
return client.OnJoin(origin.IPAddress);
|
try
|
||||||
|
{
|
||||||
|
await client.OnJoin(origin.IPAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
origin.CurrentServer.Logger.WriteWarning($"Could not execute on join for {origin}");
|
||||||
|
origin.CurrentServer.Logger.WriteDebug(e.GetExceptionInfo());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// lists the connecting and disconnecting clients via RCon response
|
/// 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 1 = disconnecting clients
|
||||||
|
/// array index 2 = updated clients
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
async Task<IList<EFClient>[]> PollPlayersAsync()
|
async Task<IList<EFClient>[]> PollPlayersAsync()
|
||||||
@ -471,6 +552,7 @@ namespace IW4MAdmin
|
|||||||
#endif
|
#endif
|
||||||
var currentClients = GetClientsAsList();
|
var currentClients = GetClientsAsList();
|
||||||
var polledClients = (await this.GetStatusAsync()).AsEnumerable();
|
var polledClients = (await this.GetStatusAsync()).AsEnumerable();
|
||||||
|
|
||||||
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
|
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
|
||||||
{
|
{
|
||||||
polledClients = polledClients.Where(c => !c.IsBot);
|
polledClients = polledClients.Where(c => !c.IsBot);
|
||||||
@ -478,8 +560,6 @@ namespace IW4MAdmin
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms");
|
Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms");
|
||||||
#endif
|
#endif
|
||||||
Throttled = false;
|
|
||||||
|
|
||||||
var disconnectingClients = currentClients.Except(polledClients);
|
var disconnectingClients = currentClients.Except(polledClients);
|
||||||
var connectingClients = polledClients.Except(currentClients);
|
var connectingClients = polledClients.Except(currentClients);
|
||||||
var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients);
|
var updatedClients = polledClients.Except(connectingClients).Except(disconnectingClients);
|
||||||
@ -492,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 start = DateTime.Now;
|
||||||
DateTime playerCountStart = DateTime.Now;
|
DateTime playerCountStart = DateTime.Now;
|
||||||
DateTime lastCount = DateTime.Now;
|
DateTime lastCount = DateTime.Now;
|
||||||
@ -499,40 +603,28 @@ namespace IW4MAdmin
|
|||||||
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
#region SHUTDOWN
|
if (cts.IsCancellationRequested)
|
||||||
if (Manager.ShutdownRequested())
|
|
||||||
{
|
{
|
||||||
foreach (var client in GetClientsAsList())
|
await ShutdownInternal();
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
if (Manager.GetApplicationSettings().Configuration().RConPollRate == int.MaxValue)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
var polledClients = await PollPlayersAsync();
|
var polledClients = await PollPlayersAsync();
|
||||||
var waiterList = new List<GameEvent>();
|
var waiterList = new List<GameEvent>();
|
||||||
|
|
||||||
foreach (var disconnectingClient in polledClients[1])
|
foreach (var disconnectingClient in polledClients[1])
|
||||||
{
|
{
|
||||||
if (disconnectingClient.State == EFClient.ClientState.Disconnecting)
|
if (disconnectingClient.State == ClientState.Disconnecting)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -549,13 +641,23 @@ namespace IW4MAdmin
|
|||||||
// because we don't want to try to fill up a slot that's not empty yet
|
// because we don't want to try to fill up a slot that's not empty yet
|
||||||
waiterList.Add(e);
|
waiterList.Add(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for all the disconnect tasks to finish
|
// 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();
|
waiterList.Clear();
|
||||||
// this are our new connecting clients
|
// this are our new connecting clients
|
||||||
foreach (var client in polledClients[0])
|
foreach (var client in polledClients[0])
|
||||||
{
|
{
|
||||||
|
// note: this prevents players in ZMBI state from being registered with no name
|
||||||
|
if (string.IsNullOrEmpty(client.Name) || (client.Ping == 999 && !client.IsBot))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var e = new GameEvent()
|
var e = new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.PreConnect,
|
Type = GameEvent.EventType.PreConnect,
|
||||||
@ -568,7 +670,10 @@ namespace IW4MAdmin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wait for all the connect tasks to finish
|
// 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();
|
waiterList.Clear();
|
||||||
// these are the clients that have updated
|
// these are the clients that have updated
|
||||||
@ -585,12 +690,22 @@ namespace IW4MAdmin
|
|||||||
waiterList.Add(e);
|
waiterList.Add(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(waiterList.Select(e => e.WaitAsync(10 * 1000)));
|
foreach (var waiter in waiterList)
|
||||||
|
{
|
||||||
|
waiter.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
if (ConnectionErrors > 0)
|
if (ConnectionErrors > 0)
|
||||||
{
|
{
|
||||||
Logger.WriteVerbose($"{loc["MANAGER_CONNECTION_REST"]} {IP}:{Port}");
|
var _event = new GameEvent()
|
||||||
Throttled = false;
|
{
|
||||||
|
Type = GameEvent.EventType.ConnectionRestored,
|
||||||
|
Owner = this,
|
||||||
|
Origin = Utilities.IW4MAdminClient(this),
|
||||||
|
Target = Utilities.IW4MAdminClient(this)
|
||||||
|
};
|
||||||
|
|
||||||
|
Manager.GetEventHandler().AddEvent(_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectionErrors = 0;
|
ConnectionErrors = 0;
|
||||||
@ -602,9 +717,17 @@ namespace IW4MAdmin
|
|||||||
ConnectionErrors++;
|
ConnectionErrors++;
|
||||||
if (ConnectionErrors == 3)
|
if (ConnectionErrors == 3)
|
||||||
{
|
{
|
||||||
Logger.WriteError($"{e.Message} {IP}:{Port}, {loc["SERVER_ERROR_POLLING"]}");
|
var _event = new GameEvent()
|
||||||
Logger.WriteDebug($"Internal Exception: {e.Data["internal_exception"]}");
|
{
|
||||||
Throttled = true;
|
Type = GameEvent.EventType.ConnectionLost,
|
||||||
|
Owner = this,
|
||||||
|
Origin = Utilities.IW4MAdminClient(this),
|
||||||
|
Target = Utilities.IW4MAdminClient(this),
|
||||||
|
Extra = e,
|
||||||
|
Data = ConnectionErrors.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
Manager.GetEventHandler().AddEvent(_event);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -613,14 +736,14 @@ namespace IW4MAdmin
|
|||||||
lastCount = DateTime.Now;
|
lastCount = DateTime.Now;
|
||||||
|
|
||||||
// update the player history
|
// 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.Dequeue();
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientHistory.Enqueue(new SharedLibraryCore.Helpers.PlayerHistory(ClientNum));
|
ClientHistory.Enqueue(new PlayerHistory(ClientNum));
|
||||||
playerCountStart = DateTime.Now;
|
playerCountStart = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -629,7 +752,7 @@ namespace IW4MAdmin
|
|||||||
&& BroadcastMessages.Count > 0
|
&& BroadcastMessages.Count > 0
|
||||||
&& ClientNum > 0)
|
&& ClientNum > 0)
|
||||||
{
|
{
|
||||||
string[] messages = this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage]).Split(Environment.NewLine);
|
string[] messages = (await this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage])).Split(Environment.NewLine);
|
||||||
|
|
||||||
foreach (string message in messages)
|
foreach (string message in messages)
|
||||||
{
|
{
|
||||||
@ -643,12 +766,18 @@ namespace IW4MAdmin
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
await ShutdownInternal();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// this one is ok
|
// this one is ok
|
||||||
catch (ServerException e)
|
catch (ServerException e)
|
||||||
{
|
{
|
||||||
if (e is NetworkException)
|
if (e is NetworkException && !Throttled)
|
||||||
{
|
{
|
||||||
Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}");
|
Logger.WriteError(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
|
||||||
Logger.WriteDebug(e.GetExceptionInfo());
|
Logger.WriteDebug(e.GetExceptionInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,7 +786,7 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
catch (Exception E)
|
catch (Exception E)
|
||||||
{
|
{
|
||||||
Logger.WriteError($"{loc["SERVER_ERROR_EXCEPTION"]} {IP}:{Port}");
|
Logger.WriteError(loc["SERVER_ERROR_EXCEPTION"].FormatExt($"[{IP}:{Port}]"));
|
||||||
Logger.WriteDebug(E.GetExceptionInfo());
|
Logger.WriteDebug(E.GetExceptionInfo());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -665,44 +794,35 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
public async Task Initialize()
|
public async Task Initialize()
|
||||||
{
|
{
|
||||||
RconParser = ServerConfig.UseT6MParser ?
|
RconParser = Manager.AdditionalRConParsers
|
||||||
(IRConParser)new T6MRConParser() :
|
.FirstOrDefault(_parser => _parser.Version == ServerConfig.RConParserVersion);
|
||||||
new IW3RConParser();
|
|
||||||
|
EventParser = Manager.AdditionalEventParsers
|
||||||
|
.FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion);
|
||||||
|
|
||||||
|
RconParser = RconParser ?? new BaseRConParser();
|
||||||
|
EventParser = EventParser ?? new BaseEventParser();
|
||||||
|
|
||||||
|
RemoteConnection.SetConfiguration(RconParser.Configuration);
|
||||||
|
|
||||||
var version = await this.GetDvarAsync<string>("version");
|
var version = await this.GetDvarAsync<string>("version");
|
||||||
Version = version.Value;
|
Version = version.Value;
|
||||||
GameName = Utilities.GetGame(version.Value);
|
GameName = Utilities.GetGame(version?.Value ?? RconParser.Version);
|
||||||
|
|
||||||
if (GameName == Game.IW4)
|
|
||||||
{
|
|
||||||
EventParser = new IW4EventParser();
|
|
||||||
RconParser = new IW4RConParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (GameName == Game.T5M)
|
|
||||||
{
|
|
||||||
EventParser = new T5MEventParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (GameName == Game.T6M)
|
|
||||||
{
|
|
||||||
EventParser = new T6MEventParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
EventParser = new IW3EventParser(); // this uses the 'main' folder for log paths
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GameName == Game.UKN)
|
if (GameName == Game.UKN)
|
||||||
{
|
{
|
||||||
Logger.WriteWarning($"Game name not recognized: {version}");
|
GameName = RconParser.GameName;
|
||||||
|
|
||||||
EventParser = Manager.AdditionalEventParsers.FirstOrDefault(_parser => (_parser as DynamicEventParser).Version == version.Value) ?? EventParser;
|
|
||||||
RconParser = Manager.AdditionalRConParsers.FirstOrDefault(_parser => (_parser as DynamicRConParser).Version == version.Value) ?? RconParser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var infoResponse = await this.GetInfoAsync();
|
if (version?.Value?.Length != 0)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null;
|
||||||
// this is normally slow, but I'm only doing it because different games have different prefixes
|
// this is normally slow, but I'm only doing it because different games have different prefixes
|
||||||
var hostname = infoResponse == null ?
|
var hostname = infoResponse == null ?
|
||||||
(await this.GetDvarAsync<string>("sv_hostname")).Value :
|
(await this.GetDvarAsync<string>("sv_hostname")).Value :
|
||||||
@ -746,61 +866,66 @@ namespace IW4MAdmin
|
|||||||
this.MaxClients = maxplayers;
|
this.MaxClients = maxplayers;
|
||||||
this.FSGame = game;
|
this.FSGame = game;
|
||||||
this.Gametype = gametype;
|
this.Gametype = gametype;
|
||||||
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value;
|
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
|
||||||
|
|
||||||
if (logsync.Value == 0 || logfile.Value == string.Empty)
|
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
|
// this DVAR isn't set until the a map is loaded
|
||||||
await this.SetDvarAsync("logfile", 2);
|
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();
|
CustomCallback = await ScriptLoaded();
|
||||||
string mainPath = EventParser.Configuration.GameDirectory;
|
|
||||||
string logPath = string.Empty;
|
|
||||||
|
|
||||||
LogPath = game == string.Empty ?
|
// they've manually specified the log path
|
||||||
$"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{mainPath}{Path.DirectorySeparatorChar}{logfile.Value}" :
|
if (!string.IsNullOrEmpty(ServerConfig.ManualLogPath))
|
||||||
$"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{game.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{logfile.Value}";
|
|
||||||
|
|
||||||
bool remoteLog = false;
|
|
||||||
if (GameName == Game.IW5 || ServerConfig.ManualLogPath?.Length > 0)
|
|
||||||
{
|
{
|
||||||
logPath = ServerConfig.ManualLogPath;
|
LogPath = ServerConfig.ManualLogPath;
|
||||||
remoteLog = logPath.StartsWith("http");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logPath = LogPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remoteLog)
|
|
||||||
{
|
|
||||||
LogEvent = new GameLogEventDetection(this, logPath, logfile.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
string mainPath = EventParser.Configuration.GameDirectory;
|
||||||
|
|
||||||
|
LogPath = string.IsNullOrEmpty(game) ?
|
||||||
|
$"{basepath?.Value?.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{mainPath}{Path.DirectorySeparatorChar}{logfile?.Value}" :
|
||||||
|
$"{basepath?.Value?.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{game?.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{logfile?.Value}";
|
||||||
|
|
||||||
// fix wine drive name mangling
|
// fix wine drive name mangling
|
||||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
logPath = Regex.Replace($"{Path.DirectorySeparatorChar}{LogPath}", @"[A-Z]:/", "");
|
LogPath = Regex.Replace($"{Path.DirectorySeparatorChar}{LogPath}", @"[A-Z]:/", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!File.Exists(logPath))
|
if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null)
|
||||||
{
|
{
|
||||||
Logger.WriteError($"{logPath} {loc["SERVER_ERROR_DNE"]}");
|
Logger.WriteError(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
|
||||||
throw new ServerException($"{loc["SERVER_ERROR_LOG"]} {logPath}");
|
throw new ServerException(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
LogEvent = new GameLogEventDetection(this, logPath, logfile.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.WriteInfo($"Log file is {logPath}");
|
LogEvent = new GameLogEventDetection(this, LogPath, ServerConfig.GameLogServerUrl);
|
||||||
|
Logger.WriteInfo($"Log file is {LogPath}");
|
||||||
|
|
||||||
_ = Task.Run(() => LogEvent.PollForChanges());
|
_ = Task.Run(() => LogEvent.PollForChanges());
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
@ -835,9 +960,9 @@ namespace IW4MAdmin
|
|||||||
Target.CurrentServer.Broadcast(message);
|
Target.CurrentServer.Broadcast(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
Penalty newPenalty = new Penalty()
|
var newPenalty = new EFPenalty()
|
||||||
{
|
{
|
||||||
Type = Penalty.PenaltyType.Warning,
|
Type = EFPenalty.PenaltyType.Warning,
|
||||||
Expires = DateTime.UtcNow,
|
Expires = DateTime.UtcNow,
|
||||||
Offender = Target,
|
Offender = Target,
|
||||||
Punisher = Origin,
|
Punisher = Origin,
|
||||||
@ -865,7 +990,7 @@ namespace IW4MAdmin
|
|||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string formattedKick = String.Format(RconParser.Configuration.CommandPrefixes.Kick, Target.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7");
|
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, Target.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7");
|
||||||
await Target.CurrentServer.ExecuteCommandAsync(formattedKick);
|
await Target.CurrentServer.ExecuteCommandAsync(formattedKick);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -874,9 +999,9 @@ namespace IW4MAdmin
|
|||||||
await Target.CurrentServer.OnClientDisconnected(Target);
|
await Target.CurrentServer.OnClientDisconnected(Target);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var newPenalty = new Penalty()
|
var newPenalty = new EFPenalty()
|
||||||
{
|
{
|
||||||
Type = Penalty.PenaltyType.Kick,
|
Type = EFPenalty.PenaltyType.Kick,
|
||||||
Expires = DateTime.UtcNow,
|
Expires = DateTime.UtcNow,
|
||||||
Offender = Target,
|
Offender = Target,
|
||||||
Offense = Reason,
|
Offense = Reason,
|
||||||
@ -911,9 +1036,9 @@ namespace IW4MAdmin
|
|||||||
await Target.CurrentServer.OnClientDisconnected(Target);
|
await Target.CurrentServer.OnClientDisconnected(Target);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Penalty newPenalty = new Penalty()
|
var newPenalty = new EFPenalty()
|
||||||
{
|
{
|
||||||
Type = Penalty.PenaltyType.TempBan,
|
Type = EFPenalty.PenaltyType.TempBan,
|
||||||
Expires = DateTime.UtcNow + length,
|
Expires = DateTime.UtcNow + length,
|
||||||
Offender = Target,
|
Offender = Target,
|
||||||
Offense = Reason,
|
Offense = Reason,
|
||||||
@ -945,20 +1070,17 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// this is set only because they're still in the server.
|
|
||||||
targetClient.Level = EFClient.Permission.Banned;
|
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
string formattedString = String.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7({loc["SERVER_BAN_APPEAL"]} {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);
|
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString);
|
||||||
#else
|
#else
|
||||||
await targetClient.CurrentServer.OnClientDisconnected(targetClient);
|
await targetClient.CurrentServer.OnClientDisconnected(targetClient);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
Penalty newPenalty = new Penalty()
|
EFPenalty newPenalty = new EFPenalty()
|
||||||
{
|
{
|
||||||
Type = Penalty.PenaltyType.Ban,
|
Type = EFPenalty.PenaltyType.Ban,
|
||||||
Expires = null,
|
Expires = null,
|
||||||
Offender = targetClient,
|
Offender = targetClient,
|
||||||
Offense = reason,
|
Offense = reason,
|
||||||
@ -968,15 +1090,16 @@ namespace IW4MAdmin
|
|||||||
IsEvadedOffense = isEvade
|
IsEvadedOffense = isEvade
|
||||||
};
|
};
|
||||||
|
|
||||||
|
targetClient.SetLevel(Permission.Banned, originClient);
|
||||||
await Manager.GetPenaltyService().Create(newPenalty);
|
await Manager.GetPenaltyService().Create(newPenalty);
|
||||||
}
|
}
|
||||||
|
|
||||||
override public async Task Unban(string reason, EFClient Target, EFClient Origin)
|
override public async Task Unban(string reason, EFClient Target, EFClient Origin)
|
||||||
{
|
{
|
||||||
var unbanPenalty = new Penalty()
|
var unbanPenalty = new EFPenalty()
|
||||||
{
|
{
|
||||||
Type = Penalty.PenaltyType.Unban,
|
Type = EFPenalty.PenaltyType.Unban,
|
||||||
Expires = null,
|
Expires = DateTime.Now,
|
||||||
Offender = Target,
|
Offender = Target,
|
||||||
Offense = reason,
|
Offense = reason,
|
||||||
Punisher = Origin,
|
Punisher = Origin,
|
||||||
@ -985,16 +1108,17 @@ namespace IW4MAdmin
|
|||||||
Link = Target.AliasLink
|
Link = Target.AliasLink
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Target.SetLevel(Permission.User, Origin);
|
||||||
await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId);
|
await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId);
|
||||||
await Manager.GetPenaltyService().Create(unbanPenalty);
|
await Manager.GetPenaltyService().Create(unbanPenalty);
|
||||||
}
|
}
|
||||||
|
|
||||||
override public void InitializeTokens()
|
override public void InitializeTokens()
|
||||||
{
|
{
|
||||||
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("TOTALPLAYERS", (Server s) => Manager.GetClientService().GetTotalClientsAsync().Result.ToString()));
|
Manager.GetMessageTokens().Add(new MessageToken("TOTALPLAYERS", (Server s) => Task.Run(async () => (await Manager.GetClientService().GetTotalClientsAsync()).ToString())));
|
||||||
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("VERSION", (Server s) => Application.Program.Version.ToString()));
|
Manager.GetMessageTokens().Add(new 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).Result));
|
Manager.GetMessageTokens().Add(new MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.CNextMap.GetNextMap(s)));
|
||||||
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("ADMINS", (Server s) => SharedLibraryCore.Commands.CListAdmins.OnlineAdmins(s)));
|
Manager.GetMessageTokens().Add(new MessageToken("ADMINS", (Server s) => Task.FromResult(SharedLibraryCore.Commands.CListAdmins.OnlineAdmins(s))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,17 +17,20 @@ namespace IW4MAdmin.Application.Localization
|
|||||||
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
|
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
|
||||||
string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
|
string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
|
||||||
|
|
||||||
try
|
if (!Program.ServerManager.GetApplicationSettings()?.Configuration()?.UseLocalTranslations ?? false)
|
||||||
{
|
{
|
||||||
var api = Endpoint.Get();
|
try
|
||||||
var localization = api.GetLocalization(currentLocale).Result;
|
{
|
||||||
Utilities.CurrentLocalization = localization;
|
var api = Endpoint.Get();
|
||||||
return;
|
var localization = api.GetLocalization(currentLocale).Result;
|
||||||
}
|
Utilities.CurrentLocalization = localization;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
// the online localization failed so will default to local files
|
// the online localization failed so will default to local files
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// culture doesn't exist so we just want language
|
// culture doesn't exist so we just want language
|
||||||
|
@ -1,269 +0,0 @@
|
|||||||
{
|
|
||||||
"LocalizationName": "en-US",
|
|
||||||
"LocalizationIndex": {
|
|
||||||
"Set": {
|
|
||||||
"BROADCAST_OFFLINE": "^5IW4MAdmin ^7is going ^1OFFLINE",
|
|
||||||
"BROADCAST_ONLINE": "^5IW4MADMIN ^7is now ^2ONLINE",
|
|
||||||
"COMMAND_HELP_OPTIONAL": "optional",
|
|
||||||
"COMMAND_HELP_SYNTAX": "syntax:",
|
|
||||||
"COMMAND_MISSINGARGS": "Not enough arguments supplied",
|
|
||||||
"COMMAND_NOACCESS": "You do not have access to that command",
|
|
||||||
"COMMAND_NOTAUTHORIZED": "You are not authorized to execute that command",
|
|
||||||
"COMMAND_TARGET_MULTI": "Multiple players match that name",
|
|
||||||
"COMMAND_TARGET_NOTFOUND": "Unable to find specified player",
|
|
||||||
"COMMAND_UNKNOWN": "You entered an unknown command",
|
|
||||||
"COMMANDS_ADMINS_DESC": "list currently connected privileged clients",
|
|
||||||
"COMMANDS_ADMINS_NONE": "No visible administrators online",
|
|
||||||
"COMMANDS_ALIAS_ALIASES": "Aliases",
|
|
||||||
"COMMANDS_ALIAS_DESC": "get past aliases and ips of a client",
|
|
||||||
"COMMANDS_ALIAS_IPS": "IPs",
|
|
||||||
"COMMANDS_ARGS_CLEAR": "clear",
|
|
||||||
"COMMANDS_ARGS_CLIENTID": "client id",
|
|
||||||
"COMMANDS_ARGS_COMMANDS": "commands",
|
|
||||||
"COMMANDS_ARGS_DURATION": "duration (m|h|d|w|y)",
|
|
||||||
"COMMANDS_ARGS_INACTIVE": "inactive days",
|
|
||||||
"COMMANDS_ARGS_LEVEL": "level",
|
|
||||||
"COMMANDS_ARGS_MAP": "map",
|
|
||||||
"COMMANDS_ARGS_MESSAGE": "message",
|
|
||||||
"COMMANDS_ARGS_PASSWORD": "password",
|
|
||||||
"COMMANDS_ARGS_PLAYER": "player",
|
|
||||||
"COMMANDS_ARGS_REASON": "reason",
|
|
||||||
"COMMANDS_BAN_DESC": "permanently ban a client from the server",
|
|
||||||
"COMMANDS_BAN_FAIL": "You cannot ban",
|
|
||||||
"COMMANDS_BAN_SUCCESS": "has been permanently banned",
|
|
||||||
"COMMANDS_BANINFO_DESC": "get information about a ban for a client",
|
|
||||||
"COMMANDS_BANINFO_NONE": "No active ban was found for that player",
|
|
||||||
"COMMANDS_BANINO_SUCCESS": "was banned by ^5{0} ^7for:",
|
|
||||||
"COMMANDS_FASTRESTART_DESC": "fast restart current map",
|
|
||||||
"COMMANDS_FASTRESTART_MASKED": "The map has been fast restarted",
|
|
||||||
"COMMANDS_FASTRESTART_UNMASKED": "fast restarted the map",
|
|
||||||
"COMMANDS_FIND_DESC": "find client in database",
|
|
||||||
"COMMANDS_FIND_EMPTY": "No players found",
|
|
||||||
"COMMANDS_FIND_MIN": "Please enter at least 3 characters",
|
|
||||||
"COMMANDS_FLAG_DESC": "flag a suspicious client and announce to admins on join",
|
|
||||||
"COMMANDS_FLAG_FAIL": "You cannot flag",
|
|
||||||
"COMMANDS_FLAG_SUCCESS": "You have flagged",
|
|
||||||
"COMMANDS_FLAG_UNFLAG": "You have unflagged",
|
|
||||||
"COMMANDS_HELP_DESC": "list all available commands",
|
|
||||||
"COMMANDS_HELP_MOREINFO": "Type !help <command name> to get command usage syntax",
|
|
||||||
"COMMANDS_HELP_NOTFOUND": "Could not find that command",
|
|
||||||
"COMMANDS_IP_DESC": "view your external IP address",
|
|
||||||
"COMMANDS_IP_SUCCESS": "Your external IP is",
|
|
||||||
"COMMANDS_KICK_DESC": "kick a client by name",
|
|
||||||
"COMMANDS_KICK_FAIL": "You do not have the required privileges to kick",
|
|
||||||
"COMMANDS_KICK_SUCCESS": "has been kicked",
|
|
||||||
"COMMANDS_LIST_DESC": "list active clients",
|
|
||||||
"COMMANDS_MAP_DESC": "change to specified map",
|
|
||||||
"COMMANDS_MAP_SUCCESS": "Changing to map",
|
|
||||||
"COMMANDS_MAP_UKN": "Attempting to change to unknown map",
|
|
||||||
"COMMANDS_MAPROTATE": "Map rotating in ^55 ^7seconds",
|
|
||||||
"COMMANDS_MAPROTATE_DESC": "cycle to the next map in rotation",
|
|
||||||
"COMMANDS_MASK_DESC": "hide your presence as a privileged client",
|
|
||||||
"COMMANDS_MASK_OFF": "You are now unmasked",
|
|
||||||
"COMMANDS_MASK_ON": "You are now masked",
|
|
||||||
"COMMANDS_OWNER_DESC": "claim ownership of the server",
|
|
||||||
"COMMANDS_OWNER_FAIL": "This server already has an owner",
|
|
||||||
"COMMANDS_OWNER_SUCCESS": "Congratulations, you have claimed ownership of this server!",
|
|
||||||
"COMMANDS_PASSWORD_FAIL": "Your password must be at least 5 characters long",
|
|
||||||
"COMMANDS_PASSWORD_SUCCESS": "Your password has been set successfully",
|
|
||||||
"COMMANDS_PING_DESC": "get client's latency",
|
|
||||||
"COMMANDS_PING_SELF": "Your latency is",
|
|
||||||
"COMMANDS_PING_TARGET": "latency is",
|
|
||||||
"COMMANDS_PLUGINS_DESC": "view all loaded plugins",
|
|
||||||
"COMMANDS_PLUGINS_LOADED": "Loaded Plugins",
|
|
||||||
"COMMANDS_PM_DESC": "send message to other client",
|
|
||||||
"COMMANDS_PRUNE_DESC": "demote any privileged clients that have not connected recently (defaults to 30 days)",
|
|
||||||
"COMMANDS_PRUNE_FAIL": "Invalid number of inactive days",
|
|
||||||
"COMMANDS_PRUNE_SUCCESS": "inactive privileged users were pruned",
|
|
||||||
"COMMANDS_QUIT_DESC": "quit IW4MAdmin",
|
|
||||||
"COMMANDS_RCON_DESC": "send rcon command to server",
|
|
||||||
"COMMANDS_RCON_SUCCESS": "Successfully sent RCon command",
|
|
||||||
"COMMANDS_REPORT_DESC": "report a client for suspicious behavior",
|
|
||||||
"COMMANDS_REPORT_FAIL": "You cannot report",
|
|
||||||
"COMMANDS_REPORT_FAIL_CAMP": "You cannot report an player for camping",
|
|
||||||
"COMMANDS_REPORT_FAIL_DUPLICATE": "You have already reported this player",
|
|
||||||
"COMMANDS_REPORT_FAIL_SELF": "You cannot report yourself",
|
|
||||||
"COMMANDS_REPORT_SUCCESS": "Thank you for your report, an administrator has been notified",
|
|
||||||
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Reports successfully cleared",
|
|
||||||
"COMMANDS_REPORTS_DESC": "get or clear recent reports",
|
|
||||||
"COMMANDS_REPORTS_NONE": "No players reported yet",
|
|
||||||
"COMMANDS_RULES_DESC": "list server rules",
|
|
||||||
"COMMANDS_RULES_NONE": "The server owner has not set any rules",
|
|
||||||
"COMMANDS_SAY_DESC": "broadcast message to all clients",
|
|
||||||
"COMMANDS_SETLEVEL_DESC": "set client to specified privilege level",
|
|
||||||
"COMMANDS_SETLEVEL_FAIL": "Invalid group specified",
|
|
||||||
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "You can only promote ^5{0} ^7to ^5{1} ^7or lower privilege",
|
|
||||||
"COMMANDS_SETLEVEL_OWNER": "There can only be 1 owner. Modify your settings if multiple owners are required",
|
|
||||||
"COMMANDS_SETLEVEL_SELF": "You cannot change your own level",
|
|
||||||
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "This server does not allow you to promote",
|
|
||||||
"COMMANDS_SETLEVEL_SUCCESS": "was successfully promoted",
|
|
||||||
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "Congratulations! You have been promoted to",
|
|
||||||
"COMMANDS_SETPASSWORD_DESC": "set your authentication password",
|
|
||||||
"COMMANDS_TEMPBAN_DESC": "temporarily ban a client for specified time (defaults to 1 hour)",
|
|
||||||
"COMMANDS_TEMPBAN_FAIL": "You cannot temporarily ban",
|
|
||||||
"COMMANDS_TEMPBAN_SUCCESS": "has been temporarily banned for",
|
|
||||||
"COMMANDS_UNBAN_DESC": "unban client by client id",
|
|
||||||
"COMMANDS_UNBAN_FAIL": "is not banned",
|
|
||||||
"COMMANDS_UNBAN_SUCCESS": "Successfully unbanned",
|
|
||||||
"COMMANDS_UPTIME_DESC": "get current application running time",
|
|
||||||
"COMMANDS_UPTIME_TEXT": "has been online for",
|
|
||||||
"COMMANDS_USAGE_DESC": "get application memory usage",
|
|
||||||
"COMMANDS_USAGE_TEXT": "is using",
|
|
||||||
"COMMANDS_WARN_DESC": "warn client for infringing rules",
|
|
||||||
"COMMANDS_WARN_FAIL": "You do not have the required privileges to warn",
|
|
||||||
"COMMANDS_WARNCLEAR_DESC": "remove all warnings for a client",
|
|
||||||
"COMMANDS_WARNCLEAR_SUCCESS": "All warning cleared for",
|
|
||||||
"COMMANDS_WHO_DESC": "give information about yourself",
|
|
||||||
"GLOBAL_TIME_DAYS": "days",
|
|
||||||
"GLOBAL_ERROR": "Error",
|
|
||||||
"GLOBAL_TIME_HOURS": "hours",
|
|
||||||
"GLOBAL_INFO": "Info",
|
|
||||||
"GLOBAL_TIME_MINUTES": "minutes",
|
|
||||||
"GLOBAL_REPORT": "If you suspect someone of ^5CHEATING ^7use the ^5!report ^7command",
|
|
||||||
"GLOBAL_VERBOSE": "Verbose",
|
|
||||||
"GLOBAL_WARNING": "Warning",
|
|
||||||
"MANAGER_CONNECTION_REST": "Connection has been reestablished with",
|
|
||||||
"MANAGER_CONSOLE_NOSERV": "No servers are currently being monitored",
|
|
||||||
"MANAGER_EXIT": "Press any key to exit...",
|
|
||||||
"MANAGER_INIT_FAIL": "Fatal error during initialization",
|
|
||||||
"MANAGER_MONITORING_TEXT": "Now monitoring",
|
|
||||||
"MANAGER_SHUTDOWN_SUCCESS": "Shutdown complete",
|
|
||||||
"MANAGER_VERSION_CURRENT": "Your version is",
|
|
||||||
"MANAGER_VERSION_FAIL": "Could not get latest IW4MAdmin version",
|
|
||||||
"MANAGER_VERSION_SUCCESS": "IW4MAdmin is up to date",
|
|
||||||
"MANAGER_VERSION_UPDATE": "has an update. Latest version is",
|
|
||||||
"PLUGIN_IMPORTER_NOTFOUND": "No plugins found to load",
|
|
||||||
"PLUGIN_IMPORTER_REGISTERCMD": "Registered command",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_DESC": "login using password",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL": "Your password is incorrect",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS": "You are now logged in",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_DESC": "reset your stats to factory-new",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_FAIL": "You must be connected to a server to reset your stats",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_SUCCESS": "Your stats for this server have been reset",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOP_DESC": "view the top 5 players in this server",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOP_TEXT": "Top Players",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_DESC": "view your stats",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL": "Cannot find the player you specified",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME": "The specified player must be ingame",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME_SELF": "You must be ingame to view your stats",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_SUCCESS": "Stats for",
|
|
||||||
"PLUGINS_STATS_TEXT_DEATHS": "DEATHS",
|
|
||||||
"PLUGINS_STATS_TEXT_KILLS": "KILLS",
|
|
||||||
"PLUGINS_STATS_TEXT_NOQUALIFY": "No players qualify for top stats yet",
|
|
||||||
"PLUGINS_STATS_TEXT_SKILL": "SKILL",
|
|
||||||
"SERVER_BAN_APPEAL": "appeal at",
|
|
||||||
"SERVER_BAN_PREV": "Previously banned for",
|
|
||||||
"SERVER_BAN_TEXT": "You're banned",
|
|
||||||
"SERVER_ERROR_ADDPLAYER": "Unable to add player",
|
|
||||||
"SERVER_ERROR_COMMAND_INGAME": "An internal error occured while processing your command",
|
|
||||||
"SERVER_ERROR_COMMAND_LOG": "command generated an error",
|
|
||||||
"SERVER_ERROR_COMMUNICATION": "Could not communicate with",
|
|
||||||
"SERVER_ERROR_DNE": "does not exist",
|
|
||||||
"SERVER_ERROR_DVAR": "Could not get the dvar value for",
|
|
||||||
"SERVER_ERROR_DVAR_HELP": "ensure the server has a map loaded",
|
|
||||||
"SERVER_ERROR_EXCEPTION": "Unexpected exception on",
|
|
||||||
"SERVER_ERROR_LOG": "Invalid game log file",
|
|
||||||
"SERVER_ERROR_PLUGIN": "An error occured loading plugin",
|
|
||||||
"SERVER_ERROR_POLLING": "reducing polling rate",
|
|
||||||
"SERVER_ERROR_UNFIXABLE": "Not monitoring server due to uncorrectable errors",
|
|
||||||
"SERVER_KICK_CONTROLCHARS": "Your name cannot contain control characters",
|
|
||||||
"SERVER_KICK_GENERICNAME": "Please change your name using /name",
|
|
||||||
"SERVER_KICK_MINNAME": "Your name must contain at least 3 characters",
|
|
||||||
"SERVER_KICK_NAME_INUSE": "Your name is being used by someone else",
|
|
||||||
"SERVER_KICK_TEXT": "You were kicked",
|
|
||||||
"SERVER_KICK_VPNS_NOTALLOWED": "VPNs are not allowed on this server",
|
|
||||||
"SERVER_PLUGIN_ERROR": "A plugin generated an error",
|
|
||||||
"SERVER_REPORT_COUNT": "There are ^5{0} ^7recent reports",
|
|
||||||
"SERVER_TB_REMAIN": "You are temporarily banned",
|
|
||||||
"SERVER_TB_TEXT": "You're temporarily banned",
|
|
||||||
"SERVER_WARNING": "WARNING",
|
|
||||||
"SERVER_WARNLIMT_REACHED": "Too many warnings",
|
|
||||||
"SERVER_WEBSITE_GENERIC": "this server's website",
|
|
||||||
"SETUP_DISPLAY_SOCIAL": "Display social media link on webfront (discord, website, VK, etc..)",
|
|
||||||
"SETUP_ENABLE_CUSTOMSAY": "Enable custom say name",
|
|
||||||
"SETUP_ENABLE_MULTIOWN": "Enable multiple owners",
|
|
||||||
"SETUP_ENABLE_STEPPEDPRIV": "Enable stepped privilege hierarchy",
|
|
||||||
"SETUP_ENABLE_VPNS": "Enable client VPNs",
|
|
||||||
"SETUP_ENABLE_WEBFRONT": "Enable webfront",
|
|
||||||
"SETUP_ENCODING_STRING": "Enter encoding string",
|
|
||||||
"SETUP_IPHUB_KEY": "Enter iphub.info api key",
|
|
||||||
"SETUP_SAY_NAME": "Enter custom say name",
|
|
||||||
"SETUP_SERVER_IP": "Enter server IP Address",
|
|
||||||
"SETUP_SERVER_MANUALLOG": "Enter manual log file path",
|
|
||||||
"SETUP_SERVER_PORT": "Enter server port",
|
|
||||||
"SETUP_SERVER_RCON": "Enter server RCon password",
|
|
||||||
"SETUP_SERVER_SAVE": "Configuration saved, add another",
|
|
||||||
"SETUP_SERVER_USEIW5M": "Use Pluto IW5 Parser",
|
|
||||||
"SETUP_SERVER_USET6M": "Use Pluto T6 parser",
|
|
||||||
"SETUP_SOCIAL_LINK": "Enter social media link",
|
|
||||||
"SETUP_SOCIAL_TITLE": "Enter social media name",
|
|
||||||
"SETUP_USE_CUSTOMENCODING": "Use custom encoding parser",
|
|
||||||
"WEBFRONT_ACTION_BAN_NAME": "Ban",
|
|
||||||
"WEBFRONT_ACTION_LABEL_ID": "Client ID",
|
|
||||||
"WEBFRONT_ACTION_LABEL_PASSWORD": "Password",
|
|
||||||
"WEBFRONT_ACTION_LABEL_REASON": "Reason",
|
|
||||||
"WEBFRONT_ACTION_LOGIN_NAME": "Login",
|
|
||||||
"WEBFRONT_ACTION_UNBAN_NAME": "Unban",
|
|
||||||
"WEBFRONT_CLIENT_META_FALSE": "Is not",
|
|
||||||
"WEBFRONT_CLIENT_META_JOINED": "Joined with alias",
|
|
||||||
"WEBFRONT_CLIENT_META_MASKED": "Masked",
|
|
||||||
"WEBFRONT_CLIENT_META_TRUE": "Is",
|
|
||||||
"WEBFRONT_CLIENT_PRIVILEGED_TITLE": "Privileged Clients",
|
|
||||||
"WEBFRONT_CLIENT_PROFILE_TITLE": "Profile",
|
|
||||||
"WEBFRONT_CLIENT_SEARCH_MATCHING": "Clients Matching",
|
|
||||||
"WEBFRONT_CONSOLE_EXECUTE": "Execute",
|
|
||||||
"WEBFRONT_CONSOLE_TITLE": "Web Console",
|
|
||||||
"WEBFRONT_ERROR_DESC": "IW4MAdmin encountered an error",
|
|
||||||
"WEBFRONT_ERROR_GENERIC_DESC": "An error occurred while processing your request",
|
|
||||||
"WEBFRONT_ERROR_GENERIC_TITLE": "Sorry!",
|
|
||||||
"WEBFRONT_ERROR_TITLE": "Error!",
|
|
||||||
"WEBFRONT_HOME_TITLE": "Server Overview",
|
|
||||||
"WEBFRONT_NAV_CONSOLE": "Console",
|
|
||||||
"WEBFRONT_NAV_DISCORD": "Discord",
|
|
||||||
"WEBFRONT_NAV_HOME": "Home",
|
|
||||||
"WEBFRONT_NAV_LOGOUT": "Logout",
|
|
||||||
"WEBFRONT_NAV_PENALTIES": "Penalties",
|
|
||||||
"WEBFRONT_NAV_PRIVILEGED": "Admins",
|
|
||||||
"WEBFRONT_NAV_PROFILE": "Client Profile",
|
|
||||||
"WEBFRONT_NAV_SEARCH": "Find Client",
|
|
||||||
"WEBFRONT_NAV_SOCIAL": "Social",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_ADMIN": "Admin",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_AGO": "ago",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_NAME": "Name",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_OFFENSE": "Offense",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_REMAINING": "left",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_SHOW": "Show",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_SHOWONLY": "Show only",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_TIME": "Time/Left",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_TYPE": "Type",
|
|
||||||
"WEBFRONT_PENALTY_TITLE": "Client Penalties",
|
|
||||||
"WEBFRONT_PROFILE_FSEEN": "First seen",
|
|
||||||
"WEBFRONT_PROFILE_LEVEL": "Level",
|
|
||||||
"WEBFRONT_PROFILE_LSEEN": "Last seen",
|
|
||||||
"WEBFRONT_PROFILE_PLAYER": "Played",
|
|
||||||
"PLUGIN_STATS_SETUP_ENABLEAC": "Enable server-side anti-cheat (IW4 only)",
|
|
||||||
"PLUGIN_STATS_ERROR_ADD": "Could not add server to server stats",
|
|
||||||
"PLUGIN_STATS_CHEAT_DETECTED": "You appear to be cheating",
|
|
||||||
"PLUGINS_STATS_TEXT_KDR": "KDR",
|
|
||||||
"PLUGINS_STATS_META_SPM": "Score per Minute",
|
|
||||||
"PLUGINS_WELCOME_USERANNOUNCE": "^5{{ClientName}} ^7hails from ^5{{ClientLocation}}",
|
|
||||||
"PLUGINS_WELCOME_USERWELCOME": "Welcome ^5{{ClientName}}^7, this is your ^5{{TimesConnected}} ^7time connecting!",
|
|
||||||
"PLUGINS_WELCOME_PRIVANNOUNCE": "{{ClientLevel}} {{ClientName}} has joined the server",
|
|
||||||
"PLUGINS_LOGIN_AUTH": "not logged in",
|
|
||||||
"PLUGINS_PROFANITY_SETUP_ENABLE": "Enable profanity deterring",
|
|
||||||
"PLUGINS_PROFANITY_WARNMSG": "Please do not use profanity on this server",
|
|
||||||
"PLUGINS_PROFANITY_KICKMSG": "Excessive use of profanity",
|
|
||||||
"GLOBAL_DEBUG": "Debug",
|
|
||||||
"COMMANDS_UNFLAG_DESC": "Remove flag for client",
|
|
||||||
"COMMANDS_UNFLAG_FAIL": "You cannot unflag",
|
|
||||||
"COMMANDS_UNFLAG_NOTFLAGGED": "Client is not flagged",
|
|
||||||
"COMMANDS_FLAG_ALREADYFLAGGED": "Client is already flagged",
|
|
||||||
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT": "Most Played",
|
|
||||||
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC": "view the top 5 dedicated players on the server",
|
|
||||||
"WEBFRONT_PROFILE_MESSAGES": "Messages",
|
|
||||||
"WEBFRONT_CLIENT_META_CONNECTIONS": "Connections",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOPSTATS_RATING": "Rating",
|
|
||||||
"PLUGINS_STATS_COMMANDS_PERFORMANCE": "Performance"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,269 +0,0 @@
|
|||||||
{
|
|
||||||
"LocalizationName": "es-EC",
|
|
||||||
"LocalizationIndex": {
|
|
||||||
"Set": {
|
|
||||||
"BROADCAST_OFFLINE": "^5IW4MAdmin ^7está ^1DESCONECTANDOSE",
|
|
||||||
"BROADCAST_ONLINE": "^5IW4MADMIN ^7está ahora ^2en línea",
|
|
||||||
"COMMAND_HELP_OPTIONAL": "opcional",
|
|
||||||
"COMMAND_HELP_SYNTAX": "sintaxis:",
|
|
||||||
"COMMAND_MISSINGARGS": "No se han proporcionado suficientes argumentos",
|
|
||||||
"COMMAND_NOACCESS": "Tú no tienes acceso a ese comando",
|
|
||||||
"COMMAND_NOTAUTHORIZED": "Tú no estás autorizado para ejecutar ese comando",
|
|
||||||
"COMMAND_TARGET_MULTI": "Múltiples jugadores coinciden con ese nombre",
|
|
||||||
"COMMAND_TARGET_NOTFOUND": "No se puede encontrar el jugador especificado",
|
|
||||||
"COMMAND_UNKNOWN": "Has ingresado un comando desconocido",
|
|
||||||
"COMMANDS_ADMINS_DESC": "enlistar clientes privilegiados actualmente conectados",
|
|
||||||
"COMMANDS_ADMINS_NONE": "No hay administradores visibles en línea",
|
|
||||||
"COMMANDS_ALIAS_ALIASES": "Aliases",
|
|
||||||
"COMMANDS_ALIAS_DESC": "obtener alias e ips anteriores de un cliente",
|
|
||||||
"COMMANDS_ALIAS_IPS": "IPs",
|
|
||||||
"COMMANDS_ARGS_CLEAR": "borrar",
|
|
||||||
"COMMANDS_ARGS_CLIENTID": "id del cliente",
|
|
||||||
"COMMANDS_ARGS_COMMANDS": "comandos",
|
|
||||||
"COMMANDS_ARGS_DURATION": "duración (m|h|d|w|y)",
|
|
||||||
"COMMANDS_ARGS_INACTIVE": "días inactivo",
|
|
||||||
"COMMANDS_ARGS_LEVEL": "nivel",
|
|
||||||
"COMMANDS_ARGS_MAP": "mapa",
|
|
||||||
"COMMANDS_ARGS_MESSAGE": "mensaje",
|
|
||||||
"COMMANDS_ARGS_PASSWORD": "contraseña",
|
|
||||||
"COMMANDS_ARGS_PLAYER": "jugador",
|
|
||||||
"COMMANDS_ARGS_REASON": "razón",
|
|
||||||
"COMMANDS_BAN_DESC": "banear permanentemente un cliente del servidor",
|
|
||||||
"COMMANDS_BAN_FAIL": "Tú no puedes banear",
|
|
||||||
"COMMANDS_BAN_SUCCESS": "ha sido baneado permanentemente",
|
|
||||||
"COMMANDS_BANINFO_DESC": "obtener información sobre el ban de un cliente",
|
|
||||||
"COMMANDS_BANINFO_NONE": "No se encontró ban activo para ese jugador",
|
|
||||||
"COMMANDS_BANINO_SUCCESS": "fue baneado por ^5{0} ^7debido a:",
|
|
||||||
"COMMANDS_FASTRESTART_DESC": "dar reinicio rápido al mapa actial",
|
|
||||||
"COMMANDS_FASTRESTART_MASKED": "Al mapa se le ha dado un reinicio rápido",
|
|
||||||
"COMMANDS_FASTRESTART_UNMASKED": "ha dado rápido reinicio al mapa",
|
|
||||||
"COMMANDS_FIND_DESC": "encontrar cliente en la base de datos",
|
|
||||||
"COMMANDS_FIND_EMPTY": "No se encontraron jugadores",
|
|
||||||
"COMMANDS_FIND_MIN": "Por Favor introduzca al menos 3 caracteres",
|
|
||||||
"COMMANDS_FLAG_DESC": "marcar un cliente sospechoso y anunciar a los administradores al unirse",
|
|
||||||
"COMMANDS_FLAG_FAIL": "Tú no puedes marcar",
|
|
||||||
"COMMANDS_FLAG_SUCCESS": "Has marcado a",
|
|
||||||
"COMMANDS_FLAG_UNFLAG": "Has desmarcado a",
|
|
||||||
"COMMANDS_HELP_DESC": "enlistar todos los comandos disponibles",
|
|
||||||
"COMMANDS_HELP_MOREINFO": "Escribe !help <nombre del comando> para obtener la sintaxis de uso del comando",
|
|
||||||
"COMMANDS_HELP_NOTFOUND": "No se ha podido encontrar ese comando",
|
|
||||||
"COMMANDS_IP_DESC": "ver tu dirección IP externa",
|
|
||||||
"COMMANDS_IP_SUCCESS": "Tu IP externa es",
|
|
||||||
"COMMANDS_KICK_DESC": "expulsar a un cliente por su nombre",
|
|
||||||
"COMMANDS_KICK_FAIL": "No tienes los privilegios necesarios para expulsar a",
|
|
||||||
"COMMANDS_KICK_SUCCESS": "ha sido expulsado",
|
|
||||||
"COMMANDS_LIST_DESC": "enlistar clientes activos",
|
|
||||||
"COMMANDS_MAP_DESC": "cambiar al mapa especificado",
|
|
||||||
"COMMANDS_MAP_SUCCESS": "Cambiando al mapa",
|
|
||||||
"COMMANDS_MAP_UKN": "Intentando cambiar a un mapa desconocido",
|
|
||||||
"COMMANDS_MAPROTATE": "Rotación de mapa en ^55 ^7segundos",
|
|
||||||
"COMMANDS_MAPROTATE_DESC": "pasar al siguiente mapa en rotación",
|
|
||||||
"COMMANDS_MASK_DESC": "esconde tu presencia como un cliente privilegiado",
|
|
||||||
"COMMANDS_MASK_OFF": "Ahora estás desenmascarado",
|
|
||||||
"COMMANDS_MASK_ON": "Ahora estás enmascarado",
|
|
||||||
"COMMANDS_OWNER_DESC": "reclamar la propiedad del servidor",
|
|
||||||
"COMMANDS_OWNER_FAIL": "Este servidor ya tiene un propietario",
|
|
||||||
"COMMANDS_OWNER_SUCCESS": "¡Felicidades, has reclamado la propiedad de este servidor!",
|
|
||||||
"COMMANDS_PASSWORD_FAIL": "Tu contraseña debe tener al menos 5 caracteres de largo",
|
|
||||||
"COMMANDS_PASSWORD_SUCCESS": "Su contraseña ha sido establecida con éxito",
|
|
||||||
"COMMANDS_PING_DESC": "obtener ping del cliente",
|
|
||||||
"COMMANDS_PING_SELF": "Tu ping es",
|
|
||||||
"COMMANDS_PING_TARGET": "ping es",
|
|
||||||
"COMMANDS_PLUGINS_DESC": "ver todos los complementos cargados",
|
|
||||||
"COMMANDS_PLUGINS_LOADED": "Complementos cargados",
|
|
||||||
"COMMANDS_PM_DESC": "enviar mensaje a otro cliente",
|
|
||||||
"COMMANDS_PRUNE_DESC": "degradar a los clientes con privilegios que no se hayan conectado recientemente (el valor predeterminado es 30 días)",
|
|
||||||
"COMMANDS_PRUNE_FAIL": "Número inválido de días inactivos",
|
|
||||||
"COMMANDS_PRUNE_SUCCESS": "los usuarios privilegiados inactivos fueron podados",
|
|
||||||
"COMMANDS_QUIT_DESC": "salir de IW4MAdmin",
|
|
||||||
"COMMANDS_RCON_DESC": "enviar el comando rcon al servidor",
|
|
||||||
"COMMANDS_RCON_SUCCESS": "Exitosamente enviado el comando RCon",
|
|
||||||
"COMMANDS_REPORT_DESC": "reportar un cliente por comportamiento sospechoso",
|
|
||||||
"COMMANDS_REPORT_FAIL": "Tú no puedes reportar",
|
|
||||||
"COMMANDS_REPORT_FAIL_CAMP": "No puedes reportar a un jugador por campear",
|
|
||||||
"COMMANDS_REPORT_FAIL_DUPLICATE": "Ya has reportado a este jugador",
|
|
||||||
"COMMANDS_REPORT_FAIL_SELF": "No puedes reportarte a ti mismo",
|
|
||||||
"COMMANDS_REPORT_SUCCESS": "Gracias por su reporte, un administrador ha sido notificado",
|
|
||||||
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Reportes borrados con éxito",
|
|
||||||
"COMMANDS_REPORTS_DESC": "obtener o borrar informes recientes",
|
|
||||||
"COMMANDS_REPORTS_NONE": "No hay jugadores reportados aun",
|
|
||||||
"COMMANDS_RULES_DESC": "enlistar reglas del servidor",
|
|
||||||
"COMMANDS_RULES_NONE": "El propietario del servidor no ha establecido ninguna regla",
|
|
||||||
"COMMANDS_SAY_DESC": "transmitir el mensaje a todos los clientes",
|
|
||||||
"COMMANDS_SETLEVEL_DESC": "establecer el cliente al nivel de privilegio especificado",
|
|
||||||
"COMMANDS_SETLEVEL_FAIL": "Grupo inválido especificado",
|
|
||||||
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "Tú solo puedes promover ^5{0} ^7a ^5{1} ^7o menor privilegio",
|
|
||||||
"COMMANDS_SETLEVEL_OWNER": "Solo puede haber un propietario. Modifica tu configuración si múltiples propietarios son requeridos",
|
|
||||||
"COMMANDS_SETLEVEL_SELF": "No puedes cambiar tu propio nivel",
|
|
||||||
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "Este servidor no te permite promover",
|
|
||||||
"COMMANDS_SETLEVEL_SUCCESS": "fue promovido con éxito",
|
|
||||||
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "¡Felicitaciones! has ha sido promovido a",
|
|
||||||
"COMMANDS_SETPASSWORD_DESC": "configura tu contraseña de autenticación",
|
|
||||||
"COMMANDS_TEMPBAN_DESC": "banear temporalmente a un cliente por el tiempo especificado (predeterminado en 1 hora)",
|
|
||||||
"COMMANDS_TEMPBAN_FAIL": "Tú no puedes banear temporalmente",
|
|
||||||
"COMMANDS_TEMPBAN_SUCCESS": "ha sido baneado temporalmente por",
|
|
||||||
"COMMANDS_UNBAN_DESC": "desbanear al cliente por ID",
|
|
||||||
"COMMANDS_UNBAN_FAIL": "no está baneado",
|
|
||||||
"COMMANDS_UNBAN_SUCCESS": "Exitosamente desbaneado",
|
|
||||||
"COMMANDS_UPTIME_DESC": "obtener el tiempo de ejecución de la aplicación actual",
|
|
||||||
"COMMANDS_UPTIME_TEXT": "ha estado en línea por",
|
|
||||||
"COMMANDS_USAGE_DESC": "obtener uso de la memoria de la aplicación",
|
|
||||||
"COMMANDS_USAGE_TEXT": "está usando",
|
|
||||||
"COMMANDS_WARN_DESC": "advertir al cliente por infringir las reglas",
|
|
||||||
"COMMANDS_WARN_FAIL": "No tiene los privilegios necesarios para advertir a",
|
|
||||||
"COMMANDS_WARNCLEAR_DESC": "eliminar todas las advertencias de un cliente",
|
|
||||||
"COMMANDS_WARNCLEAR_SUCCESS": "Todas las advertencias borradas para",
|
|
||||||
"COMMANDS_WHO_DESC": "da información sobre ti",
|
|
||||||
"GLOBAL_TIME_DAYS": "días",
|
|
||||||
"GLOBAL_ERROR": "Error",
|
|
||||||
"GLOBAL_TIME_HOURS": "horas",
|
|
||||||
"GLOBAL_INFO": "Información",
|
|
||||||
"GLOBAL_TIME_MINUTES": "minutos",
|
|
||||||
"GLOBAL_REPORT": "Si sospechas que alguien ^5usa cheats ^7usa el comando ^5!report",
|
|
||||||
"GLOBAL_VERBOSE": "Detallado",
|
|
||||||
"GLOBAL_WARNING": "Advertencia",
|
|
||||||
"MANAGER_CONNECTION_REST": "La conexión ha sido restablecida con",
|
|
||||||
"MANAGER_CONSOLE_NOSERV": "No hay servidores que estén siendo monitoreados en este momento",
|
|
||||||
"MANAGER_EXIT": "Presione cualquier tecla para salir...",
|
|
||||||
"MANAGER_INIT_FAIL": "Error fatal durante la inicialización",
|
|
||||||
"MANAGER_MONITORING_TEXT": "Ahora monitoreando",
|
|
||||||
"MANAGER_SHUTDOWN_SUCCESS": "Apagado completo",
|
|
||||||
"MANAGER_VERSION_CURRENT": "Tu versión es",
|
|
||||||
"MANAGER_VERSION_FAIL": "No se ha podido conseguir la última versión de IW4MAdmin",
|
|
||||||
"MANAGER_VERSION_SUCCESS": "IW4MAdmin está actualizado",
|
|
||||||
"MANAGER_VERSION_UPDATE": "tiene una actualización. La última versión es",
|
|
||||||
"PLUGIN_IMPORTER_NOTFOUND": "No se encontraron complementos para cargar",
|
|
||||||
"PLUGIN_IMPORTER_REGISTERCMD": "Comando registrado",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_DESC": "iniciar sesión usando la contraseña",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL": "tu contraseña es incorrecta",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS": "Ahora está conectado",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_DESC": "restablece tus estadísticas a las nuevas de fábrica",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_FAIL": "Debes estar conectado a un servidor para restablecer tus estadísticas",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_SUCCESS": "Tus estadísticas para este servidor se han restablecido",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOP_DESC": "ver los 5 mejores jugadores en este servidor",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOP_TEXT": "Mejores Jugadores",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_DESC": "ver tus estadísticas",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL": "No se puede encontrar el jugador que especificó",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME": "El jugador especificado debe estar dentro del juego",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME_SELF": "Debes estar dentro del juego para ver tus estadísticas",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_SUCCESS": "Estadísticas para",
|
|
||||||
"PLUGINS_STATS_TEXT_DEATHS": "Muertes",
|
|
||||||
"PLUGINS_STATS_TEXT_KILLS": "Asesinatos",
|
|
||||||
"PLUGINS_STATS_TEXT_NOQUALIFY": "No hay jugadores que califiquen para los primeros lugares aun",
|
|
||||||
"PLUGINS_STATS_TEXT_SKILL": "Habilidad",
|
|
||||||
"SERVER_BAN_APPEAL": "apela en",
|
|
||||||
"SERVER_BAN_PREV": "Baneado anteriormente por",
|
|
||||||
"SERVER_BAN_TEXT": "Estás baneado",
|
|
||||||
"SERVER_ERROR_ADDPLAYER": "Incapaz de añadir al jugador",
|
|
||||||
"SERVER_ERROR_COMMAND_INGAME": "Un error interno ocurrió mientras se procesaba tu comando",
|
|
||||||
"SERVER_ERROR_COMMAND_LOG": "Comando generó error",
|
|
||||||
"SERVER_ERROR_COMMUNICATION": "No se ha podido comunicar con",
|
|
||||||
"SERVER_ERROR_DNE": "No existe",
|
|
||||||
"SERVER_ERROR_DVAR": "No se pudo obtener el valor dvar",
|
|
||||||
"SERVER_ERROR_DVAR_HELP": "asegúrate de que el servidor tenga un mapa cargado",
|
|
||||||
"SERVER_ERROR_EXCEPTION": "Excepción inesperada en",
|
|
||||||
"SERVER_ERROR_LOG": "Archivo de registro del juego invalido",
|
|
||||||
"SERVER_ERROR_PLUGIN": "Un error ocurrió mientras se cargaba el complemente",
|
|
||||||
"SERVER_ERROR_POLLING": "reduciendo la tasa de sondeo",
|
|
||||||
"SERVER_ERROR_UNFIXABLE": "No se está supervisando el servidor debido a errores incorregibles",
|
|
||||||
"SERVER_KICK_CONTROLCHARS": "Tu nombre no puede contener caracteres de control",
|
|
||||||
"SERVER_KICK_GENERICNAME": "Por favor cambia tu nombre usando /name",
|
|
||||||
"SERVER_KICK_MINNAME": "Tu nombre debe contener al menos 3 caracteres",
|
|
||||||
"SERVER_KICK_NAME_INUSE": "Tu nombre está siendo usado por alguien más",
|
|
||||||
"SERVER_KICK_TEXT": "Fuiste expulsado",
|
|
||||||
"SERVER_KICK_VPNS_NOTALLOWED": "Las VPNs no están permitidas en este servidor",
|
|
||||||
"SERVER_PLUGIN_ERROR": "Un complemento generó un error",
|
|
||||||
"SERVER_REPORT_COUNT": "Hay ^5{0} ^7reportes recientes",
|
|
||||||
"SERVER_TB_REMAIN": "Tú estás temporalmente baneado",
|
|
||||||
"SERVER_TB_TEXT": "Estás temporalmente baneado",
|
|
||||||
"SERVER_WARNING": "ADVERTENCIA",
|
|
||||||
"SERVER_WARNLIMT_REACHED": "Muchas advertencias",
|
|
||||||
"SERVER_WEBSITE_GENERIC": "el sitio web de este servidor",
|
|
||||||
"SETUP_DISPLAY_SOCIAL": "Mostrar el link del medio de comunicación en la parte frontal de la web. (discord, website, VK, etc..)",
|
|
||||||
"SETUP_ENABLE_CUSTOMSAY": "Habilitar nombre a decir personalizado",
|
|
||||||
"SETUP_ENABLE_MULTIOWN": "Habilitar múltiples propietarios",
|
|
||||||
"SETUP_ENABLE_STEPPEDPRIV": "Habilitar jerarquía de privilegios por escalones",
|
|
||||||
"SETUP_ENABLE_VPNS": "Habilitar VPNs clientes",
|
|
||||||
"SETUP_ENABLE_WEBFRONT": "Habilitar frente de la web",
|
|
||||||
"SETUP_ENCODING_STRING": "Ingresar cadena de codificación",
|
|
||||||
"SETUP_IPHUB_KEY": "Ingresar clave api de iphub.info",
|
|
||||||
"SETUP_SAY_NAME": "Ingresar nombre a decir personalizado",
|
|
||||||
"SETUP_SERVER_IP": "Ingresar Dirección IP del servidor",
|
|
||||||
"SETUP_SERVER_MANUALLOG": "Ingresar manualmente la ruta del archivo de registro",
|
|
||||||
"SETUP_SERVER_PORT": "Ingresar puerto del servidor",
|
|
||||||
"SETUP_SERVER_RCON": "Ingresar contraseña RCon del servidor",
|
|
||||||
"SETUP_SERVER_SAVE": "Configuración guardada, añadir otra",
|
|
||||||
"SETUP_SERVER_USEIW5M": "Usar analizador Pluto IW5",
|
|
||||||
"SETUP_SERVER_USET6M": "Usar analizador Pluto T6",
|
|
||||||
"SETUP_SOCIAL_LINK": "Ingresar link del medio de comunicación",
|
|
||||||
"SETUP_SOCIAL_TITLE": "Ingresa el nombre de la red de comunicación",
|
|
||||||
"SETUP_USE_CUSTOMENCODING": "Usar analizador de codificación personalizado",
|
|
||||||
"WEBFRONT_ACTION_BAN_NAME": "Ban",
|
|
||||||
"WEBFRONT_ACTION_LABEL_ID": "ID del Cliente",
|
|
||||||
"WEBFRONT_ACTION_LABEL_PASSWORD": "Contraseña",
|
|
||||||
"WEBFRONT_ACTION_LABEL_REASON": "Razón",
|
|
||||||
"WEBFRONT_ACTION_LOGIN_NAME": "Inicio de sesión",
|
|
||||||
"WEBFRONT_ACTION_UNBAN_NAME": "Desban",
|
|
||||||
"WEBFRONT_CLIENT_META_FALSE": "No está",
|
|
||||||
"WEBFRONT_CLIENT_META_JOINED": "Se unió con el alias",
|
|
||||||
"WEBFRONT_CLIENT_META_MASKED": "Enmascarado",
|
|
||||||
"WEBFRONT_CLIENT_META_TRUE": "Está",
|
|
||||||
"WEBFRONT_CLIENT_PRIVILEGED_TITLE": "Clientes privilegiados",
|
|
||||||
"WEBFRONT_CLIENT_PROFILE_TITLE": "Perfil",
|
|
||||||
"WEBFRONT_CLIENT_SEARCH_MATCHING": "Clientes que concuerdan",
|
|
||||||
"WEBFRONT_CONSOLE_EXECUTE": "Ejecutar",
|
|
||||||
"WEBFRONT_CONSOLE_TITLE": "Consola Web",
|
|
||||||
"WEBFRONT_ERROR_DESC": "IW4MAdmin encontró un error",
|
|
||||||
"WEBFRONT_ERROR_GENERIC_DESC": "Un error ha ocurrido mientras se procesaba tu solicitud",
|
|
||||||
"WEBFRONT_ERROR_GENERIC_TITLE": "¡Lo lamento!",
|
|
||||||
"WEBFRONT_ERROR_TITLE": "¡Error!",
|
|
||||||
"WEBFRONT_HOME_TITLE": "Vista general del servidor",
|
|
||||||
"WEBFRONT_NAV_CONSOLE": "Consola",
|
|
||||||
"WEBFRONT_NAV_DISCORD": "Discord",
|
|
||||||
"WEBFRONT_NAV_HOME": "Inicio",
|
|
||||||
"WEBFRONT_NAV_LOGOUT": "Cerrar sesión",
|
|
||||||
"WEBFRONT_NAV_PENALTIES": "Sanciones",
|
|
||||||
"WEBFRONT_NAV_PRIVILEGED": "Administradores",
|
|
||||||
"WEBFRONT_NAV_PROFILE": "Perfil del cliente",
|
|
||||||
"WEBFRONT_NAV_SEARCH": "Encontrar cliente",
|
|
||||||
"WEBFRONT_NAV_SOCIAL": "Social",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_ADMIN": "Administrador",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_AGO": "atrás",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_NAME": "Nombre",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_OFFENSE": "Ofensa",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_REMAINING": "restante",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_SHOW": "Mostrar",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_SHOWONLY": "Mostrar solamente",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_TIME": "Tiempo/Restante",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_TYPE": "Tipo",
|
|
||||||
"WEBFRONT_PENALTY_TITLE": "Faltas del cliente",
|
|
||||||
"WEBFRONT_PROFILE_FSEEN": "Primera vez visto hace",
|
|
||||||
"WEBFRONT_PROFILE_LEVEL": "Nivel",
|
|
||||||
"WEBFRONT_PROFILE_LSEEN": "Última vez visto hace",
|
|
||||||
"WEBFRONT_PROFILE_PLAYER": "Jugadas",
|
|
||||||
"PLUGIN_STATS_SETUP_ENABLEAC": "Habilitar anti-trampas junto al servidor (solo IW4)",
|
|
||||||
"PLUGIN_STATS_ERROR_ADD": "No se puedo añadir servidor a los estados del servidor",
|
|
||||||
"PLUGIN_STATS_CHEAT_DETECTED": "Pareces estar haciendo trampa",
|
|
||||||
"PLUGINS_STATS_TEXT_KDR": "KDR",
|
|
||||||
"PLUGINS_STATS_META_SPM": "Puntaje por minuto",
|
|
||||||
"PLUGINS_WELCOME_USERANNOUNCE": "^5{{ClientName}} ^7llega desde ^5{{ClientLocation}}",
|
|
||||||
"PLUGINS_WELCOME_USERWELCOME": "¡Bienvenido ^5{{ClientName}}^7, esta es tu visita numero ^5{{TimesConnected}} ^7 en el servidor!",
|
|
||||||
"PLUGINS_WELCOME_PRIVANNOUNCE": "{{ClientLevel}} {{ClientName}} Se ha unido al servidor",
|
|
||||||
"PLUGINS_LOGIN_AUTH": "No registrado",
|
|
||||||
"PLUGINS_PROFANITY_SETUP_ENABLE": "Habilitar la disuasión de blasfemias",
|
|
||||||
"PLUGINS_PROFANITY_WARNMSG": "Por favor no uses blasfemias en este servidor",
|
|
||||||
"PLUGINS_PROFANITY_KICKMSG": "Excesivo uso de blasfemias",
|
|
||||||
"GLOBAL_DEBUG": "Depurar",
|
|
||||||
"COMMANDS_UNFLAG_DESC": "Remover marca del cliente",
|
|
||||||
"COMMANDS_UNFLAG_FAIL": "Tu no puedes desmarcar",
|
|
||||||
"COMMANDS_UNFLAG_NOTFLAGGED": "El cliente no está marcado",
|
|
||||||
"COMMANDS_FLAG_ALREADYFLAGGED": "El cliente yá se encuentra marcado",
|
|
||||||
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT": "Más jugado",
|
|
||||||
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC": "ver el Top 5 de jugadores dedicados en el servidor",
|
|
||||||
"WEBFRONT_PROFILE_MESSAGES": "Mensajes",
|
|
||||||
"WEBFRONT_CLIENT_META_CONNECTIONS": "Conexiones",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOPSTATS_RATING": "Clasificación",
|
|
||||||
"PLUGINS_STATS_COMMANDS_PERFORMANCE": "Desempeño"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,269 +0,0 @@
|
|||||||
{
|
|
||||||
"LocalizationName": "pt-BR",
|
|
||||||
"LocalizationIndex": {
|
|
||||||
"Set": {
|
|
||||||
"BROADCAST_OFFLINE": "IW4MAdmin ficou offline",
|
|
||||||
"BROADCAST_ONLINE": "^5IW4MADMIN ^7agora está ^2ONLINE",
|
|
||||||
"COMMAND_HELP_OPTIONAL": "opcional",
|
|
||||||
"COMMAND_HELP_SYNTAX": "sintaxe:",
|
|
||||||
"COMMAND_MISSINGARGS": "Não foram oferecidos argumentos suficientes",
|
|
||||||
"COMMAND_NOACCESS": "Você não tem acesso a este comando",
|
|
||||||
"COMMAND_NOTAUTHORIZED": "Você não está autorizado a executar este comando",
|
|
||||||
"COMMAND_TARGET_MULTI": "Vários jogadores correspondem a esse nome",
|
|
||||||
"COMMAND_TARGET_NOTFOUND": "Não é possível encontrar o jogador especificado",
|
|
||||||
"COMMAND_UNKNOWN": "Você digitou um comando desconhecido",
|
|
||||||
"COMMANDS_ADMINS_DESC": "lista os clientes privilegiados conectados no momento",
|
|
||||||
"COMMANDS_ADMINS_NONE": "Não há administradores visíveis online",
|
|
||||||
"COMMANDS_ALIAS_ALIASES": "Nomes registrados",
|
|
||||||
"COMMANDS_ALIAS_DESC": "obtém a lista de histórico de nomes que o jogador usou no servidor",
|
|
||||||
"COMMANDS_ALIAS_IPS": "IPs",
|
|
||||||
"COMMANDS_ARGS_CLEAR": "apagar",
|
|
||||||
"COMMANDS_ARGS_CLIENTID": "id do jogador",
|
|
||||||
"COMMANDS_ARGS_COMMANDS": "comandos",
|
|
||||||
"COMMANDS_ARGS_DURATION": "duração (m|h|d|w|y)",
|
|
||||||
"COMMANDS_ARGS_INACTIVE": "dias inativos",
|
|
||||||
"COMMANDS_ARGS_LEVEL": "nível",
|
|
||||||
"COMMANDS_ARGS_MAP": "mapa",
|
|
||||||
"COMMANDS_ARGS_MESSAGE": "mensagem",
|
|
||||||
"COMMANDS_ARGS_PASSWORD": "senha",
|
|
||||||
"COMMANDS_ARGS_PLAYER": "jogador",
|
|
||||||
"COMMANDS_ARGS_REASON": "razão",
|
|
||||||
"COMMANDS_BAN_DESC": "banir permanentemente um cliente do servidor",
|
|
||||||
"COMMANDS_BAN_FAIL": "Você não pode banir permanentemente",
|
|
||||||
"COMMANDS_BAN_SUCCESS": "foi banido permanentemente",
|
|
||||||
"COMMANDS_BANINFO_DESC": "obtém informações sobre um banimento para um jogador",
|
|
||||||
"COMMANDS_BANINFO_NONE": "Nenhum banimento ativo foi encontrado para esse jogador",
|
|
||||||
"COMMANDS_BANINO_SUCCESS": "foi banido por ^5{0} ^7por:",
|
|
||||||
"COMMANDS_FASTRESTART_DESC": "reinicializa rapidamente o mapa atual, não recomendável o uso várias vezes seguidas",
|
|
||||||
"COMMANDS_FASTRESTART_MASKED": "O mapa foi reiniciado rapidamente",
|
|
||||||
"COMMANDS_FASTRESTART_UNMASKED": "reiniciou rapidamente o mapa",
|
|
||||||
"COMMANDS_FIND_DESC": "acha o jogador na base de dados",
|
|
||||||
"COMMANDS_FIND_EMPTY": "Nenhum jogador foi encontrado",
|
|
||||||
"COMMANDS_FIND_MIN": "Por favor, insira pelo menos 3 caracteres",
|
|
||||||
"COMMANDS_FLAG_DESC": "sinaliza um cliente suspeito e anuncia aos administradores ao entrar no servidor",
|
|
||||||
"COMMANDS_FLAG_FAIL": "Você não pode sinalizar",
|
|
||||||
"COMMANDS_FLAG_SUCCESS": "Você sinalizou",
|
|
||||||
"COMMANDS_FLAG_UNFLAG": "Você tirou a sinalização de",
|
|
||||||
"COMMANDS_HELP_DESC": "lista todos os comandos disponíveis",
|
|
||||||
"COMMANDS_HELP_MOREINFO": "Digite !help <comando> para saber como usar o comando",
|
|
||||||
"COMMANDS_HELP_NOTFOUND": "Não foi possível encontrar esse comando",
|
|
||||||
"COMMANDS_IP_DESC": "mostrar o seu endereço IP externo",
|
|
||||||
"COMMANDS_IP_SUCCESS": "Seu endereço IP externo é",
|
|
||||||
"COMMANDS_KICK_DESC": "expulsa o jogador pelo nome",
|
|
||||||
"COMMANDS_KICK_FAIL": "Você não tem os privilégios necessários para expulsar",
|
|
||||||
"COMMANDS_KICK_SUCCESS": "foi expulso",
|
|
||||||
"COMMANDS_LIST_DESC": "lista os jogadores ativos na partida",
|
|
||||||
"COMMANDS_MAP_DESC": "muda para o mapa especificado",
|
|
||||||
"COMMANDS_MAP_SUCCESS": "Mudando o mapa para",
|
|
||||||
"COMMANDS_MAP_UKN": "Tentando mudar para o mapa desconhecido",
|
|
||||||
"COMMANDS_MAPROTATE": "Rotacionando o mapa em ^55 ^7segundos",
|
|
||||||
"COMMANDS_MAPROTATE_DESC": "avança para o próximo mapa da rotação",
|
|
||||||
"COMMANDS_MASK_DESC": "esconde a sua presença como um jogador privilegiado",
|
|
||||||
"COMMANDS_MASK_OFF": "Você foi desmascarado",
|
|
||||||
"COMMANDS_MASK_ON": "Você agora está mascarado",
|
|
||||||
"COMMANDS_OWNER_DESC": "reivindica a propriedade do servidor",
|
|
||||||
"COMMANDS_OWNER_FAIL": "Este servidor já tem um dono",
|
|
||||||
"COMMANDS_OWNER_SUCCESS": "Parabéns, você reivindicou a propriedade deste servidor!",
|
|
||||||
"COMMANDS_PASSWORD_FAIL": "Sua senha deve ter pelo menos 5 caracteres",
|
|
||||||
"COMMANDS_PASSWORD_SUCCESS": "Sua senha foi configurada com sucesso",
|
|
||||||
"COMMANDS_PING_DESC": "mostra o quanto de latência tem o jogador",
|
|
||||||
"COMMANDS_PING_SELF": "Sua latência é",
|
|
||||||
"COMMANDS_PING_TARGET": "latência é",
|
|
||||||
"COMMANDS_PLUGINS_DESC": "mostra todos os plugins que estão carregados",
|
|
||||||
"COMMANDS_PLUGINS_LOADED": "Plugins carregados",
|
|
||||||
"COMMANDS_PM_DESC": "envia a mensagem para o outro jogador de maneira privada, use /!pm para ter efeito, se possível",
|
|
||||||
"COMMANDS_PRUNE_DESC": "rebaixa qualquer jogador privilegiado que não tenha se conectado recentemente (o padrão é 30 dias)",
|
|
||||||
"COMMANDS_PRUNE_FAIL": "Número inválido de dias ativo",
|
|
||||||
"COMMANDS_PRUNE_SUCCESS": "usuários privilegiados inativos foram removidos",
|
|
||||||
"COMMANDS_QUIT_DESC": "sair do IW4MAdmin",
|
|
||||||
"COMMANDS_RCON_DESC": "envia o comando Rcon para o servidor",
|
|
||||||
"COMMANDS_RCON_SUCCESS": "O comando para o RCon foi enviado com sucesso!",
|
|
||||||
"COMMANDS_REPORT_DESC": "denuncia o jogador por comportamento suspeito",
|
|
||||||
"COMMANDS_REPORT_FAIL": "Você não pode reportar",
|
|
||||||
"COMMANDS_REPORT_FAIL_CAMP": "Você não pode denunciar o jogador por camperar",
|
|
||||||
"COMMANDS_REPORT_FAIL_DUPLICATE": "Você já denunciou o jogador",
|
|
||||||
"COMMANDS_REPORT_FAIL_SELF": "Você não pode reportar a si mesmo",
|
|
||||||
"COMMANDS_REPORT_SUCCESS": "Obrigado pela sua denúncia, um administrador foi notificado",
|
|
||||||
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Lista de denúncias limpa com sucesso",
|
|
||||||
"COMMANDS_REPORTS_DESC": "obtém ou limpa as denúncias recentes",
|
|
||||||
"COMMANDS_REPORTS_NONE": "Ninguém foi denunciado ainda",
|
|
||||||
"COMMANDS_RULES_DESC": "lista as regras do servidor",
|
|
||||||
"COMMANDS_RULES_NONE": "O proprietário do servidor não definiu nenhuma regra, sinta-se livre",
|
|
||||||
"COMMANDS_SAY_DESC": "transmite mensagem para todos os jogadores",
|
|
||||||
"COMMANDS_SETLEVEL_DESC": "define o jogador para o nível de privilégio especificado",
|
|
||||||
"COMMANDS_SETLEVEL_FAIL": "grupo especificado inválido",
|
|
||||||
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "Você só pode promover do ^5{0} ^7para ^5{1} ^7ou um nível menor",
|
|
||||||
"COMMANDS_SETLEVEL_OWNER": "Só pode haver 1 dono. Modifique suas configurações se vários proprietários forem necessários",
|
|
||||||
"COMMANDS_SETLEVEL_SELF": "Você não pode mudar seu próprio nível",
|
|
||||||
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "Este servidor não permite que você promova",
|
|
||||||
"COMMANDS_SETLEVEL_SUCCESS": "foi promovido com sucesso",
|
|
||||||
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "Parabéns! Você foi promovido para",
|
|
||||||
"COMMANDS_SETPASSWORD_DESC": "define sua senha de autenticação",
|
|
||||||
"COMMANDS_TEMPBAN_DESC": "bane temporariamente um jogador por tempo especificado (o padrão é 1 hora)",
|
|
||||||
"COMMANDS_TEMPBAN_FAIL": "Você não pode banir temporariamente",
|
|
||||||
"COMMANDS_TEMPBAN_SUCCESS": "foi banido temporariamente por",
|
|
||||||
"COMMANDS_UNBAN_DESC": "retira o banimento de um jogador pelo seu ID",
|
|
||||||
"COMMANDS_UNBAN_FAIL": "não está banido",
|
|
||||||
"COMMANDS_UNBAN_SUCCESS": "Foi retirado o banimento com sucesso",
|
|
||||||
"COMMANDS_UPTIME_DESC": "obtém o tempo de execução do aplicativo a quando aberto",
|
|
||||||
"COMMANDS_UPTIME_TEXT": "está online por",
|
|
||||||
"COMMANDS_USAGE_DESC": "vê quanto o aplicativo está usando de memória RAM do seu computador",
|
|
||||||
"COMMANDS_USAGE_TEXT": "está usando",
|
|
||||||
"COMMANDS_WARN_DESC": "adverte o cliente por infringir as regras",
|
|
||||||
"COMMANDS_WARN_FAIL": "Você não tem os privilégios necessários para advertir",
|
|
||||||
"COMMANDS_WARNCLEAR_DESC": "remove todos os avisos para um cliente",
|
|
||||||
"COMMANDS_WARNCLEAR_SUCCESS": "Todos as advertências foram apagados para",
|
|
||||||
"COMMANDS_WHO_DESC": "dá informações sobre você",
|
|
||||||
"GLOBAL_TIME_DAYS": "dias",
|
|
||||||
"GLOBAL_ERROR": "Erro",
|
|
||||||
"GLOBAL_TIME_HOURS": "horas",
|
|
||||||
"GLOBAL_INFO": "Informação",
|
|
||||||
"GLOBAL_TIME_MINUTES": "minutos",
|
|
||||||
"GLOBAL_REPORT": "Se você está suspeitando alguém de alguma ^5TRAPAÇA ^7use o comando ^5!report",
|
|
||||||
"GLOBAL_VERBOSE": "Detalhe",
|
|
||||||
"GLOBAL_WARNING": "AVISO",
|
|
||||||
"MANAGER_CONNECTION_REST": "A conexão foi reestabelecida com",
|
|
||||||
"MANAGER_CONSOLE_NOSERV": "Não há servidores sendo monitorados neste momento",
|
|
||||||
"MANAGER_EXIT": "Pressione qualquer tecla para sair...",
|
|
||||||
"MANAGER_INIT_FAIL": "Erro fatal durante a inicialização",
|
|
||||||
"MANAGER_MONITORING_TEXT": "Agora monitorando",
|
|
||||||
"MANAGER_SHUTDOWN_SUCCESS": "Desligamento concluído",
|
|
||||||
"MANAGER_VERSION_CURRENT": "Está é a sua versão",
|
|
||||||
"MANAGER_VERSION_FAIL": "Não foi possível obter a versão mais recente do IW4MAdmin",
|
|
||||||
"MANAGER_VERSION_SUCCESS": "O IW4MAdmin está atualizado",
|
|
||||||
"MANAGER_VERSION_UPDATE": "Há uma atualização disponível. A versão mais recente é",
|
|
||||||
"PLUGIN_IMPORTER_NOTFOUND": "Não foram encontrados plugins para carregar",
|
|
||||||
"PLUGIN_IMPORTER_REGISTERCMD": "Comando registrado",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_DESC": "Inicie a sua sessão usando a senha",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL": "Sua senha está errada",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS": "Você agora está conectado",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_DESC": "reinicia suas estatísticas para uma nova",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_FAIL": "Você deve estar conectado a um servidor para reiniciar as suas estatísticas",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_SUCCESS": "Suas estatísticas nesse servidor foram reiniciadas",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOP_DESC": "visualiza os 5 melhores jogadores do servidor",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOP_TEXT": "Top Jogadores",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_DESC": "mostra suas estatísticas",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL": "Não foi encontrado o jogador que você especificou",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME": "o jogador especificado deve estar dentro do jogo",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME_SELF": "Você deve estar no jogo para ver suas estatísticas",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_SUCCESS": "Estatísticas para",
|
|
||||||
"PLUGINS_STATS_TEXT_DEATHS": "MORTES",
|
|
||||||
"PLUGINS_STATS_TEXT_KILLS": "BAIXAS",
|
|
||||||
"PLUGINS_STATS_TEXT_NOQUALIFY": "Não há ainda jogadores qualificados para os primeiros lugares",
|
|
||||||
"PLUGINS_STATS_TEXT_SKILL": "HABILIDADE",
|
|
||||||
"SERVER_BAN_APPEAL": "apele em",
|
|
||||||
"SERVER_BAN_PREV": "Banido preventivamente por",
|
|
||||||
"SERVER_BAN_TEXT": "Você está banido",
|
|
||||||
"SERVER_ERROR_ADDPLAYER": "Não foi possível adicionar o jogador",
|
|
||||||
"SERVER_ERROR_COMMAND_INGAME": "Ocorreu um erro interno ao processar seu comando",
|
|
||||||
"SERVER_ERROR_COMMAND_LOG": "o comando gerou um erro",
|
|
||||||
"SERVER_ERROR_COMMUNICATION": "Não foi possível fazer a comunicação com",
|
|
||||||
"SERVER_ERROR_DNE": "não existe",
|
|
||||||
"SERVER_ERROR_DVAR": "Não foi possível obter o valor de dvar para",
|
|
||||||
"SERVER_ERROR_DVAR_HELP": "garanta que o servidor tenha um mapa carregado",
|
|
||||||
"SERVER_ERROR_EXCEPTION": "Exceção inesperada em",
|
|
||||||
"SERVER_ERROR_LOG": "Log do jogo inválido",
|
|
||||||
"SERVER_ERROR_PLUGIN": "Ocorreu um erro ao carregar o plug-in",
|
|
||||||
"SERVER_ERROR_POLLING": "reduzir a taxa de sondagem do server",
|
|
||||||
"SERVER_ERROR_UNFIXABLE": "Não monitorando o servidor devido a erros incorrigíveis",
|
|
||||||
"SERVER_KICK_CONTROLCHARS": "Seu nome não pode conter caracteres de controle",
|
|
||||||
"SERVER_KICK_GENERICNAME": "Por favor, mude o seu nome usando o comando /name no console",
|
|
||||||
"SERVER_KICK_MINNAME": "Seu nome deve conter no mínimo três caracteres",
|
|
||||||
"SERVER_KICK_NAME_INUSE": "Seu nome já está sendo usado por outra pessoa",
|
|
||||||
"SERVER_KICK_TEXT": "Você foi expulso",
|
|
||||||
"SERVER_KICK_VPNS_NOTALLOWED": "VPNs não são permitidas neste servidor",
|
|
||||||
"SERVER_PLUGIN_ERROR": "Um plugin gerou erro",
|
|
||||||
"SERVER_REPORT_COUNT": "Você tem ^5{0} ^7denúncias recentes",
|
|
||||||
"SERVER_TB_REMAIN": "Você está banido temporariamente",
|
|
||||||
"SERVER_TB_TEXT": "Você está banido temporariamente",
|
|
||||||
"SERVER_WARNING": "AVISO",
|
|
||||||
"SERVER_WARNLIMT_REACHED": "Avisos demais! Leia o chat da próxima vez",
|
|
||||||
"SERVER_WEBSITE_GENERIC": "este é o site do servidor",
|
|
||||||
"SETUP_DISPLAY_SOCIAL": "Digitar link do convite do seu site no módulo da web (Discord, YouTube, etc.)",
|
|
||||||
"SETUP_ENABLE_CUSTOMSAY": "Habilitar a customização do nome do comando say",
|
|
||||||
"SETUP_ENABLE_MULTIOWN": "Habilitar vários proprietários",
|
|
||||||
"SETUP_ENABLE_STEPPEDPRIV": "Ativar hierarquia de privilégios escalonada",
|
|
||||||
"SETUP_ENABLE_VPNS": "Habilitar que os usuários usem VPN",
|
|
||||||
"SETUP_ENABLE_WEBFRONT": "Habilitar o módulo da web do IW4MAdmin",
|
|
||||||
"SETUP_ENCODING_STRING": "Digite sequência de codificação",
|
|
||||||
"SETUP_IPHUB_KEY": "Digite iphub.info api key",
|
|
||||||
"SETUP_SAY_NAME": "Habilitar a customização do nome do comando say",
|
|
||||||
"SETUP_SERVER_IP": "Digite o endereço IP do servidor",
|
|
||||||
"SETUP_SERVER_MANUALLOG": "Insira o caminho do arquivo de log manualmente",
|
|
||||||
"SETUP_SERVER_PORT": "Digite a porta do servidor",
|
|
||||||
"SETUP_SERVER_RCON": "Digite a senha do RCon do servidor",
|
|
||||||
"SETUP_SERVER_SAVE": "Configuração salva, adicionar outra",
|
|
||||||
"SETUP_SERVER_USEIW5M": "Usar analisador Pluto IW5 ",
|
|
||||||
"SETUP_SERVER_USET6M": "Usar analisador Pluto T6 ",
|
|
||||||
"SETUP_SOCIAL_LINK": "Digite o link da Rede Social",
|
|
||||||
"SETUP_SOCIAL_TITLE": "Digite o nome da rede social",
|
|
||||||
"SETUP_USE_CUSTOMENCODING": "Usar o analisador de codificação customizado",
|
|
||||||
"WEBFRONT_ACTION_BAN_NAME": "Banir",
|
|
||||||
"WEBFRONT_ACTION_LABEL_ID": "ID do cliente",
|
|
||||||
"WEBFRONT_ACTION_LABEL_PASSWORD": "Senha",
|
|
||||||
"WEBFRONT_ACTION_LABEL_REASON": "Razão",
|
|
||||||
"WEBFRONT_ACTION_LOGIN_NAME": "Iniciar a sessão",
|
|
||||||
"WEBFRONT_ACTION_UNBAN_NAME": "Retirar o banimento",
|
|
||||||
"WEBFRONT_CLIENT_META_FALSE": "Não está",
|
|
||||||
"WEBFRONT_CLIENT_META_JOINED": "Entrou com o nome",
|
|
||||||
"WEBFRONT_CLIENT_META_MASKED": "Mascarado",
|
|
||||||
"WEBFRONT_CLIENT_META_TRUE": "Está",
|
|
||||||
"WEBFRONT_CLIENT_PRIVILEGED_TITLE": "Jogadores Privilegiados",
|
|
||||||
"WEBFRONT_CLIENT_PROFILE_TITLE": "Pefil",
|
|
||||||
"WEBFRONT_CLIENT_SEARCH_MATCHING": "Jogadores correspondidos",
|
|
||||||
"WEBFRONT_CONSOLE_EXECUTE": "Executar",
|
|
||||||
"WEBFRONT_CONSOLE_TITLE": "Console da Web",
|
|
||||||
"WEBFRONT_ERROR_DESC": "O IW4MAdmin encontrou um erro",
|
|
||||||
"WEBFRONT_ERROR_GENERIC_DESC": "Ocorreu um erro ao processar seu pedido",
|
|
||||||
"WEBFRONT_ERROR_GENERIC_TITLE": "Desculpe!",
|
|
||||||
"WEBFRONT_ERROR_TITLE": "Erro!",
|
|
||||||
"WEBFRONT_HOME_TITLE": "Visão geral do servidor",
|
|
||||||
"WEBFRONT_NAV_CONSOLE": "Console",
|
|
||||||
"WEBFRONT_NAV_DISCORD": "Discord",
|
|
||||||
"WEBFRONT_NAV_HOME": "Início",
|
|
||||||
"WEBFRONT_NAV_LOGOUT": "Encerrar a sessão",
|
|
||||||
"WEBFRONT_NAV_PENALTIES": "Penalidades",
|
|
||||||
"WEBFRONT_NAV_PRIVILEGED": "Administradores",
|
|
||||||
"WEBFRONT_NAV_PROFILE": "Perfil do Jogador",
|
|
||||||
"WEBFRONT_NAV_SEARCH": "Achar jogador",
|
|
||||||
"WEBFRONT_NAV_SOCIAL": "Rede Social",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_ADMIN": "Administrador",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_AGO": "atrás",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_NAME": "Nome",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_OFFENSE": "Ofensa",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_REMAINING": "restantes",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_SHOW": "Mostrar",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_SHOWONLY": "Mostrar somente",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_TIME": "Tempo/Restante",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_TYPE": "Tipo",
|
|
||||||
"WEBFRONT_PENALTY_TITLE": "Penalidades dos jogadores",
|
|
||||||
"WEBFRONT_PROFILE_FSEEN": "Visto primeiro em",
|
|
||||||
"WEBFRONT_PROFILE_LEVEL": "Nível",
|
|
||||||
"WEBFRONT_PROFILE_LSEEN": "Visto por último em",
|
|
||||||
"WEBFRONT_PROFILE_PLAYER": "Jogou",
|
|
||||||
"PLUGIN_STATS_SETUP_ENABLEAC": "Habilitar a anti-trapaça no servidor (Somente IW4/MW2)",
|
|
||||||
"PLUGIN_STATS_ERROR_ADD": "Não foi possível adicionar o servidor para as estatísticas do servidor",
|
|
||||||
"PLUGIN_STATS_CHEAT_DETECTED": "Aparentemente você está trapaceando",
|
|
||||||
"PLUGINS_STATS_TEXT_KDR": "KDR",
|
|
||||||
"PLUGINS_STATS_META_SPM": "Pontuação por minuto",
|
|
||||||
"PLUGINS_WELCOME_USERANNOUNCE": "^5{{ClientName}} ^7 vem de ^5{{ClientLocation}}",
|
|
||||||
"PLUGINS_WELCOME_USERWELCOME": "Bem-vindo ^5{{ClientName}}^7, esta é a sua visita de número ^5{{TimesConnected}} ^7 no servidor!",
|
|
||||||
"PLUGINS_WELCOME_PRIVANNOUNCE": "{{ClientLevel}} {{ClientName}} entrou no servidor",
|
|
||||||
"PLUGINS_LOGIN_AUTH": "não está registrado",
|
|
||||||
"PLUGINS_PROFANITY_SETUP_ENABLE": "Habilitar o plugin de anti-palavrão",
|
|
||||||
"PLUGINS_PROFANITY_WARNMSG": "Por favor, não use palavras ofensivas neste servidor",
|
|
||||||
"PLUGINS_PROFANITY_KICKMSG": "Uso excessivo de palavrão, lave a boca da próxima vez",
|
|
||||||
"GLOBAL_DEBUG": "Depuração",
|
|
||||||
"COMMANDS_UNFLAG_DESC": "Remover a sinalização do jogador",
|
|
||||||
"COMMANDS_UNFLAG_FAIL": "Você não pode retirar a sinalização do jogador",
|
|
||||||
"COMMANDS_UNFLAG_NOTFLAGGED": "O jogador não está sinalizado",
|
|
||||||
"COMMANDS_FLAG_ALREADYFLAGGED": "O jogador já está sinalizado",
|
|
||||||
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT": "Mais jogado",
|
|
||||||
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC": "ver o top 5 de jogadores mais dedicados no servidor",
|
|
||||||
"WEBFRONT_PROFILE_MESSAGES": "Mensagens",
|
|
||||||
"WEBFRONT_CLIENT_META_CONNECTIONS": "Conexões",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOPSTATS_RATING": "Classificação",
|
|
||||||
"PLUGINS_STATS_COMMANDS_PERFORMANCE": "Desempenho"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,269 +0,0 @@
|
|||||||
{
|
|
||||||
"LocalizationName": "ru-RU",
|
|
||||||
"LocalizationIndex": {
|
|
||||||
"Set": {
|
|
||||||
"BROADCAST_OFFLINE": "^5IW4MAdmin ^1ВЫКЛЮЧАЕТСЯ",
|
|
||||||
"BROADCAST_ONLINE": "^5IW4MADMIN ^7сейчас В СЕТИ",
|
|
||||||
"COMMAND_HELP_OPTIONAL": "опционально",
|
|
||||||
"COMMAND_HELP_SYNTAX": "Проблема с выражением мысли ( пересмотри слова)",
|
|
||||||
"COMMAND_MISSINGARGS": "Приведено недостаточно аргументов",
|
|
||||||
"COMMAND_NOACCESS": "У вас нет доступа к этой команде",
|
|
||||||
"COMMAND_NOTAUTHORIZED": "Вы не авторизованы для исполнения этой команды",
|
|
||||||
"COMMAND_TARGET_MULTI": "Несколько игроков соответствуют этому имени",
|
|
||||||
"COMMAND_TARGET_NOTFOUND": "Невозможно найти указанного игрока",
|
|
||||||
"COMMAND_UNKNOWN": "Вы ввели неизвестную команду",
|
|
||||||
"COMMANDS_ADMINS_DESC": "перечислить присоединенных на данный момент игроков с правами",
|
|
||||||
"COMMANDS_ADMINS_NONE": "Нет видимых администраторов в сети",
|
|
||||||
"COMMANDS_ALIAS_ALIASES": "Имена",
|
|
||||||
"COMMANDS_ALIAS_DESC": "получить прошлые имена и IP игрока",
|
|
||||||
"COMMANDS_ALIAS_IPS": "IP",
|
|
||||||
"COMMANDS_ARGS_CLEAR": "очистить",
|
|
||||||
"COMMANDS_ARGS_CLIENTID": "ID игрока",
|
|
||||||
"COMMANDS_ARGS_COMMANDS": "команды",
|
|
||||||
"COMMANDS_ARGS_DURATION": "длительность (m|h|d|w|y)",
|
|
||||||
"COMMANDS_ARGS_INACTIVE": "дни бездействия",
|
|
||||||
"COMMANDS_ARGS_LEVEL": "уровень",
|
|
||||||
"COMMANDS_ARGS_MAP": "карта",
|
|
||||||
"COMMANDS_ARGS_MESSAGE": "сообщение",
|
|
||||||
"COMMANDS_ARGS_PASSWORD": "пароль",
|
|
||||||
"COMMANDS_ARGS_PLAYER": "игрок",
|
|
||||||
"COMMANDS_ARGS_REASON": "причина",
|
|
||||||
"COMMANDS_BAN_DESC": "навсегда забанить игрока на сервере",
|
|
||||||
"COMMANDS_BAN_FAIL": "Вы не можете выдавать бан",
|
|
||||||
"COMMANDS_BAN_SUCCESS": "был забанен навсегда",
|
|
||||||
"COMMANDS_BANINFO_DESC": "получить информацию о бане игрока",
|
|
||||||
"COMMANDS_BANINFO_NONE": "Не найдено действующего бана для этого игрока",
|
|
||||||
"COMMANDS_BANINO_SUCCESS": "был забанен игроком ^5{0} ^7на:",
|
|
||||||
"COMMANDS_FASTRESTART_DESC": "перезапустить нынешнюю карту",
|
|
||||||
"COMMANDS_FASTRESTART_MASKED": "Карта была перезапущена",
|
|
||||||
"COMMANDS_FASTRESTART_UNMASKED": "перезапустил карту",
|
|
||||||
"COMMANDS_FIND_DESC": "найти игрока в базе данных",
|
|
||||||
"COMMANDS_FIND_EMPTY": "Не найдено игроков",
|
|
||||||
"COMMANDS_FIND_MIN": "Пожалуйста, введите хотя бы 3 символа",
|
|
||||||
"COMMANDS_FLAG_DESC": "отметить подозрительного игрока и сообщить администраторам, чтобы присоединились",
|
|
||||||
"COMMANDS_FLAG_FAIL": "Вы не можете ставить отметки",
|
|
||||||
"COMMANDS_FLAG_SUCCESS": "Вы отметили",
|
|
||||||
"COMMANDS_FLAG_UNFLAG": "Вы сняли отметку",
|
|
||||||
"COMMANDS_HELP_DESC": "перечислить все доступные команды",
|
|
||||||
"COMMANDS_HELP_MOREINFO": "Введите !help <имя команды>, чтобы узнать синтаксис для использования команды",
|
|
||||||
"COMMANDS_HELP_NOTFOUND": "Не удалось найти эту команду",
|
|
||||||
"COMMANDS_IP_DESC": "просмотреть ваш внешний IP-адрес",
|
|
||||||
"COMMANDS_IP_SUCCESS": "Ваш внешний IP:",
|
|
||||||
"COMMANDS_KICK_DESC": "исключить игрока по имени",
|
|
||||||
"COMMANDS_KICK_FAIL": "У вас нет достаточных прав, чтобы исключать",
|
|
||||||
"COMMANDS_KICK_SUCCESS": "был исключен",
|
|
||||||
"COMMANDS_LIST_DESC": "перечислить действующих игроков",
|
|
||||||
"COMMANDS_MAP_DESC": "сменить на определенную карту",
|
|
||||||
"COMMANDS_MAP_SUCCESS": "Смена карты на",
|
|
||||||
"COMMANDS_MAP_UKN": "Попытка сменить на неизвестную карту",
|
|
||||||
"COMMANDS_MAPROTATE": "Смена карты через ^55 ^7секунд",
|
|
||||||
"COMMANDS_MAPROTATE_DESC": "переключиться на следующую карту в ротации",
|
|
||||||
"COMMANDS_MASK_DESC": "скрыть свое присутствие как игрока с правами",
|
|
||||||
"COMMANDS_MASK_OFF": "Вы теперь демаскированы",
|
|
||||||
"COMMANDS_MASK_ON": "Вы теперь замаскированы",
|
|
||||||
"COMMANDS_OWNER_DESC": "утверить владение сервером",
|
|
||||||
"COMMANDS_OWNER_FAIL": "Этот сервер уже имеет владельца",
|
|
||||||
"COMMANDS_OWNER_SUCCESS": "Поздравляю, вы утвердили владение этим сервером!",
|
|
||||||
"COMMANDS_PASSWORD_FAIL": "Ваш пароль должен быть хотя бы 5 символов в длину",
|
|
||||||
"COMMANDS_PASSWORD_SUCCESS": "Ваш пароль был успешно установлен",
|
|
||||||
"COMMANDS_PING_DESC": "получить пинг игрока",
|
|
||||||
"COMMANDS_PING_SELF": "Ваш пинг:",
|
|
||||||
"COMMANDS_PING_TARGET": "пинг:",
|
|
||||||
"COMMANDS_PLUGINS_DESC": "просмотреть все загруженные плагины",
|
|
||||||
"COMMANDS_PLUGINS_LOADED": "Загруженные плагины",
|
|
||||||
"COMMANDS_PM_DESC": "отправить сообщение другому игроку",
|
|
||||||
"COMMANDS_PRUNE_DESC": "понизить любых игроков с правами, которые не подключались за последнее время (по умолчанию: 30 дней)",
|
|
||||||
"COMMANDS_PRUNE_FAIL": "Неверное количество дней бездействия",
|
|
||||||
"COMMANDS_PRUNE_SUCCESS": "бездействующих пользователей с правами было сокращено",
|
|
||||||
"COMMANDS_QUIT_DESC": "покинуть IW4MAdmin",
|
|
||||||
"COMMANDS_RCON_DESC": "отправить RCon команду на сервер",
|
|
||||||
"COMMANDS_RCON_SUCCESS": "Успешно отправлена команда RCon",
|
|
||||||
"COMMANDS_REPORT_DESC": "пожаловаться на игрока за подозрительное поведение",
|
|
||||||
"COMMANDS_REPORT_FAIL": "Вы не можете пожаловаться",
|
|
||||||
"COMMANDS_REPORT_FAIL_CAMP": "Вы не можете пожаловаться на игрока за кемперство",
|
|
||||||
"COMMANDS_REPORT_FAIL_DUPLICATE": "Вы уже пожаловались на этого игрока",
|
|
||||||
"COMMANDS_REPORT_FAIL_SELF": "Вы не можете пожаловаться на самого себя",
|
|
||||||
"COMMANDS_REPORT_SUCCESS": "Спасибо за вашу жалобу, администратор оповещен",
|
|
||||||
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Жалобы успешно очищены",
|
|
||||||
"COMMANDS_REPORTS_DESC": "получить или очистить последние жалобы",
|
|
||||||
"COMMANDS_REPORTS_NONE": "Пока нет жалоб на игроков",
|
|
||||||
"COMMANDS_RULES_DESC": "перечислить правила сервера",
|
|
||||||
"COMMANDS_RULES_NONE": "Владелец сервера не установил никаких правил",
|
|
||||||
"COMMANDS_SAY_DESC": "транслировать сообщения всем игрокам",
|
|
||||||
"COMMANDS_SETLEVEL_DESC": "установить особый уровень прав игроку",
|
|
||||||
"COMMANDS_SETLEVEL_FAIL": "Указана неверная группа",
|
|
||||||
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "Вы только можете повысить ^5{0} ^7до ^5{1} ^7или понизить в правах",
|
|
||||||
"COMMANDS_SETLEVEL_OWNER": "Может быть только 1 владелец. Измените настройки, если требуется несколько владельцов",
|
|
||||||
"COMMANDS_SETLEVEL_SELF": "Вы не можете изменить свой уровень",
|
|
||||||
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "Этот сервер не разрешает вам повыситься",
|
|
||||||
"COMMANDS_SETLEVEL_SUCCESS": "был успешно повышен",
|
|
||||||
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "Поздравляю! Вы были повышены до",
|
|
||||||
"COMMANDS_SETPASSWORD_DESC": "установить свой пароль аутентификации",
|
|
||||||
"COMMANDS_TEMPBAN_DESC": "временно забанить игрока на определенное время (по умолчанию: 1 час)",
|
|
||||||
"COMMANDS_TEMPBAN_FAIL": "Вы не можете выдавать временный бан",
|
|
||||||
"COMMANDS_TEMPBAN_SUCCESS": "был временно забанен за",
|
|
||||||
"COMMANDS_UNBAN_DESC": "разбанить игрока по ID игрока",
|
|
||||||
"COMMANDS_UNBAN_FAIL": "не забанен",
|
|
||||||
"COMMANDS_UNBAN_SUCCESS": "Успешно разбанен",
|
|
||||||
"COMMANDS_UPTIME_DESC": "получить время с начала запуска текущего приложения",
|
|
||||||
"COMMANDS_UPTIME_TEXT": "был в сети",
|
|
||||||
"COMMANDS_USAGE_DESC": "узнать о потреблении памяти приложением",
|
|
||||||
"COMMANDS_USAGE_TEXT": "используется",
|
|
||||||
"COMMANDS_WARN_DESC": "предупредить игрока за нарушение правил",
|
|
||||||
"COMMANDS_WARN_FAIL": "У вас недостаточно прав, чтобы выносить предупреждения",
|
|
||||||
"COMMANDS_WARNCLEAR_DESC": "удалить все предупреждения у игрока",
|
|
||||||
"COMMANDS_WARNCLEAR_SUCCESS": "Все предупреждения очищены у",
|
|
||||||
"COMMANDS_WHO_DESC": "предоставить информацию о себе",
|
|
||||||
"GLOBAL_TIME_DAYS": "дней",
|
|
||||||
"GLOBAL_ERROR": "Ошибка",
|
|
||||||
"GLOBAL_TIME_HOURS": "часов",
|
|
||||||
"GLOBAL_INFO": "Информация",
|
|
||||||
"GLOBAL_TIME_MINUTES": "минут",
|
|
||||||
"GLOBAL_REPORT": "Если вы подозреваете кого-то в ^5ЧИТЕРСТВЕ^7, используйте команду ^5!report",
|
|
||||||
"GLOBAL_VERBOSE": "Подробно",
|
|
||||||
"GLOBAL_WARNING": "Предупреждение",
|
|
||||||
"MANAGER_CONNECTION_REST": "Соединение было восстановлено с помощью",
|
|
||||||
"MANAGER_CONSOLE_NOSERV": "На данный момент нет серверов под мониторингом",
|
|
||||||
"MANAGER_EXIT": "Нажмите любую клавишу, чтобы выйти...",
|
|
||||||
"MANAGER_INIT_FAIL": "Критическая ошибка во время инициализации",
|
|
||||||
"MANAGER_MONITORING_TEXT": "Идет мониторинг",
|
|
||||||
"MANAGER_SHUTDOWN_SUCCESS": "Выключение завершено",
|
|
||||||
"MANAGER_VERSION_CURRENT": "Ваша версия:",
|
|
||||||
"MANAGER_VERSION_FAIL": "Не удалось получить последнюю версию IW4MAdmin",
|
|
||||||
"MANAGER_VERSION_SUCCESS": "IW4MAdmin обновлен",
|
|
||||||
"MANAGER_VERSION_UPDATE": "- есть обновление. Последняя версия:",
|
|
||||||
"PLUGIN_IMPORTER_NOTFOUND": "Не найдено плагинов для загрузки",
|
|
||||||
"PLUGIN_IMPORTER_REGISTERCMD": "Зарегистрированная команда",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_DESC": "войти, используя пароль",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL": "Ваш пароль неверный",
|
|
||||||
"PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS": "Вы теперь вошли",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_DESC": "сбросить вашу статистику под ноль",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_FAIL": "Вы должны быть подключены к серверу, чтобы сбросить свою статистику",
|
|
||||||
"PLUGINS_STATS_COMMANDS_RESET_SUCCESS": "Ваша статистика на этом сервере была сброшена",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOP_DESC": "показать топ-5 лучших игроков на этом сервере",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOP_TEXT": "Лучшие игроки",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_DESC": "просмотреть свою статистику",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL": "Не удается найти игрока, которого вы указали.",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME": "Указанный игрок должен быть в игре",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME_SELF": "Вы должны быть в игре, чтобы просмотреть свою статистику",
|
|
||||||
"PLUGINS_STATS_COMMANDS_VIEW_SUCCESS": "Статистика",
|
|
||||||
"PLUGINS_STATS_TEXT_DEATHS": "СМЕРТЕЙ",
|
|
||||||
"PLUGINS_STATS_TEXT_KILLS": "УБИЙСТВ",
|
|
||||||
"PLUGINS_STATS_TEXT_NOQUALIFY": "Ещё нет совернующихся игроков за лучшую статистику",
|
|
||||||
"PLUGINS_STATS_TEXT_SKILL": "МАСТЕРСТВО",
|
|
||||||
"SERVER_BAN_APPEAL": "оспорить:",
|
|
||||||
"SERVER_BAN_PREV": "Ранее забанены за",
|
|
||||||
"SERVER_BAN_TEXT": "Вы забанены",
|
|
||||||
"SERVER_ERROR_ADDPLAYER": "Не удалось добавить игрока",
|
|
||||||
"SERVER_ERROR_COMMAND_INGAME": "Произошла внутренняя ошибка при обработке вашей команды",
|
|
||||||
"SERVER_ERROR_COMMAND_LOG": "команда сгенерировала ошибку",
|
|
||||||
"SERVER_ERROR_COMMUNICATION": "Не удалось связаться с",
|
|
||||||
"SERVER_ERROR_DNE": "не существует",
|
|
||||||
"SERVER_ERROR_DVAR": "Не удалось получить значение dvar:",
|
|
||||||
"SERVER_ERROR_DVAR_HELP": "убедитесь, что на сервере загружена карта",
|
|
||||||
"SERVER_ERROR_EXCEPTION": "Неожиданное исключение на",
|
|
||||||
"SERVER_ERROR_LOG": "Неверный игровой лог-файл",
|
|
||||||
"SERVER_ERROR_PLUGIN": "Произошла ошибка загрузки плагина",
|
|
||||||
"SERVER_ERROR_POLLING": "снижение частоты обновления данных",
|
|
||||||
"SERVER_ERROR_UNFIXABLE": "Мониторинг сервера выключен из-за неисправимых ошибок",
|
|
||||||
"SERVER_KICK_CONTROLCHARS": "Ваше имя не должно содержать спецсимволы",
|
|
||||||
"SERVER_KICK_GENERICNAME": "Пожалуйста, смените ваше имя, используя /name",
|
|
||||||
"SERVER_KICK_MINNAME": "Ваше имя должно содержать хотя бы 3 символа",
|
|
||||||
"SERVER_KICK_NAME_INUSE": "Ваше имя используется кем-то другим",
|
|
||||||
"SERVER_KICK_TEXT": "Вы были исключены",
|
|
||||||
"SERVER_KICK_VPNS_NOTALLOWED": "Использование VPN не разрешено на этом сервере",
|
|
||||||
"SERVER_PLUGIN_ERROR": "Плагин образовал ошибку",
|
|
||||||
"SERVER_REPORT_COUNT": "Имеется ^5{0} ^7жалоб за последнее время",
|
|
||||||
"SERVER_TB_REMAIN": "Вы временно забанены",
|
|
||||||
"SERVER_TB_TEXT": "Вы временно забанены",
|
|
||||||
"SERVER_WARNING": "ПРЕДУПРЕЖДЕНИЕ",
|
|
||||||
"SERVER_WARNLIMT_REACHED": "Слишком много предупреждений",
|
|
||||||
"SERVER_WEBSITE_GENERIC": "веб-сайт этого сервера",
|
|
||||||
"SETUP_DISPLAY_SOCIAL": "Отображать ссылку на социальную сеть в веб-интерфейсе (Discord, веб-сайт, ВК, и т.д.)",
|
|
||||||
"SETUP_ENABLE_CUSTOMSAY": "Включить кастомное имя для чата",
|
|
||||||
"SETUP_ENABLE_MULTIOWN": "Включить поддержку нескольких владельцев",
|
|
||||||
"SETUP_ENABLE_STEPPEDPRIV": "Включить последовательную иерархию прав",
|
|
||||||
"SETUP_ENABLE_VPNS": "Включить поддержку VPN у игроков",
|
|
||||||
"SETUP_ENABLE_WEBFRONT": "Включить веб-интерфейс",
|
|
||||||
"SETUP_ENCODING_STRING": "Введите кодировку",
|
|
||||||
"SETUP_IPHUB_KEY": "Введите iphub.info api-ключ",
|
|
||||||
"SETUP_SAY_NAME": "Введите кастомное имя для чата",
|
|
||||||
"SETUP_SERVER_IP": "Введите IP-адрес сервера",
|
|
||||||
"SETUP_SERVER_MANUALLOG": "Введите путь для лог-файла",
|
|
||||||
"SETUP_SERVER_PORT": "Введите порт сервера",
|
|
||||||
"SETUP_SERVER_RCON": "Введите RCon пароль сервера",
|
|
||||||
"SETUP_SERVER_SAVE": "Настройки сохранены, добавить",
|
|
||||||
"SETUP_SERVER_USEIW5M": "Использовать парсер Pluto IW5",
|
|
||||||
"SETUP_SERVER_USET6M": "Использовать парсер Pluto T6",
|
|
||||||
"SETUP_SOCIAL_LINK": "Ввести ссылку на социальную сеть",
|
|
||||||
"SETUP_SOCIAL_TITLE": "Ввести имя социальной сети",
|
|
||||||
"SETUP_USE_CUSTOMENCODING": "Использовать кастомную кодировку парсера",
|
|
||||||
"WEBFRONT_ACTION_BAN_NAME": "Забанить",
|
|
||||||
"WEBFRONT_ACTION_LABEL_ID": "ID игрока",
|
|
||||||
"WEBFRONT_ACTION_LABEL_PASSWORD": "Пароль",
|
|
||||||
"WEBFRONT_ACTION_LABEL_REASON": "Причина",
|
|
||||||
"WEBFRONT_ACTION_LOGIN_NAME": "Войти",
|
|
||||||
"WEBFRONT_ACTION_UNBAN_NAME": "Разбанить",
|
|
||||||
"WEBFRONT_CLIENT_META_FALSE": "не",
|
|
||||||
"WEBFRONT_CLIENT_META_JOINED": "Присоединился с именем",
|
|
||||||
"WEBFRONT_CLIENT_META_MASKED": "Замаскирован",
|
|
||||||
"WEBFRONT_CLIENT_META_TRUE": "Это",
|
|
||||||
"WEBFRONT_CLIENT_PRIVILEGED_TITLE": "Игроки с правами",
|
|
||||||
"WEBFRONT_CLIENT_PROFILE_TITLE": "Профиль",
|
|
||||||
"WEBFRONT_CLIENT_SEARCH_MATCHING": "Подходящие игроки",
|
|
||||||
"WEBFRONT_CONSOLE_EXECUTE": "Выполнить",
|
|
||||||
"WEBFRONT_CONSOLE_TITLE": "Веб-консоль",
|
|
||||||
"WEBFRONT_ERROR_DESC": "IW4MAdmin столкнулся с ошибкой",
|
|
||||||
"WEBFRONT_ERROR_GENERIC_DESC": "Произошла ошибка во время обработки вашего запроса",
|
|
||||||
"WEBFRONT_ERROR_GENERIC_TITLE": "Извините!",
|
|
||||||
"WEBFRONT_ERROR_TITLE": "Ошибка!",
|
|
||||||
"WEBFRONT_HOME_TITLE": "Обзор сервера",
|
|
||||||
"WEBFRONT_NAV_CONSOLE": "Консоль",
|
|
||||||
"WEBFRONT_NAV_DISCORD": "Дискорд ",
|
|
||||||
"WEBFRONT_NAV_HOME": "Обзор Серверов ",
|
|
||||||
"WEBFRONT_NAV_LOGOUT": "Выйти",
|
|
||||||
"WEBFRONT_NAV_PENALTIES": "Наказания",
|
|
||||||
"WEBFRONT_NAV_PRIVILEGED": "Админы",
|
|
||||||
"WEBFRONT_NAV_PROFILE": "Профиль игрока",
|
|
||||||
"WEBFRONT_NAV_SEARCH": "Найти игрока",
|
|
||||||
"WEBFRONT_NAV_SOCIAL": "Соц. сети",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_ADMIN": "Админ",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_AGO": "назад",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_NAME": "Имя",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_OFFENSE": "Нарушение",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_REMAINING": "осталось",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_SHOW": "Показывать",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_SHOWONLY": "Показывать только",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_TIME": "Время/Осталось",
|
|
||||||
"WEBFRONT_PENALTY_TEMPLATE_TYPE": "Тип",
|
|
||||||
"WEBFRONT_PENALTY_TITLE": "Наказания игроков",
|
|
||||||
"WEBFRONT_PROFILE_FSEEN": "Впервые заходил",
|
|
||||||
"WEBFRONT_PROFILE_LEVEL": "Уровень",
|
|
||||||
"WEBFRONT_PROFILE_LSEEN": "Последний раз заходил",
|
|
||||||
"WEBFRONT_PROFILE_PLAYER": "Наиграл",
|
|
||||||
"PLUGIN_STATS_SETUP_ENABLEAC": "Включить серверный античит (только IW4)",
|
|
||||||
"PLUGIN_STATS_ERROR_ADD": "Не удалось добавить сервер в статистику серверов",
|
|
||||||
"PLUGIN_STATS_CHEAT_DETECTED": "Кажется, вы читерите",
|
|
||||||
"PLUGINS_STATS_TEXT_KDR": "Вот так ..",
|
|
||||||
"PLUGINS_STATS_META_SPM": "Счёт за минуту",
|
|
||||||
"PLUGINS_WELCOME_USERANNOUNCE": "^5{{ClientName}} ^7из ^5{{ClientLocation}}",
|
|
||||||
"PLUGINS_WELCOME_USERWELCOME": "Добро пожаловать, ^5{{ClientName}}^7. Это ваше ^5{{TimesConnected}} ^7подключение по счёту!",
|
|
||||||
"PLUGINS_WELCOME_PRIVANNOUNCE": "{{ClientLevel}} {{ClientName}} присоединился к серверу",
|
|
||||||
"PLUGINS_LOGIN_AUTH": "Сперва Подключись",
|
|
||||||
"PLUGINS_PROFANITY_SETUP_ENABLE": "Включить сдерживание ненормативной лексики",
|
|
||||||
"PLUGINS_PROFANITY_WARNMSG": "Пожалуйта, не ругайтесь на этом сервере",
|
|
||||||
"PLUGINS_PROFANITY_KICKMSG": "Чрезмерное употребление ненормативной лексики",
|
|
||||||
"GLOBAL_DEBUG": "Отлаживание ",
|
|
||||||
"COMMANDS_UNFLAG_DESC": "Снять все подозрение с игрока !",
|
|
||||||
"COMMANDS_UNFLAG_FAIL": "Вы не можете снять подозрения..",
|
|
||||||
"COMMANDS_UNFLAG_NOTFLAGGED": "Игрок без подозрения !",
|
|
||||||
"COMMANDS_FLAG_ALREADYFLAGGED": "Игрок помечен ! ",
|
|
||||||
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT": "Самые популярные",
|
|
||||||
"PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC": "просмотр 5 лучших игроков на сервере",
|
|
||||||
"WEBFRONT_PROFILE_MESSAGES": "Сообщения",
|
|
||||||
"WEBFRONT_CLIENT_META_CONNECTIONS": "Подключения",
|
|
||||||
"PLUGINS_STATS_COMMANDS_TOPSTATS_RATING": "Рейтинг",
|
|
||||||
"PLUGINS_STATS_COMMANDS_PERFORMANCE": "Эффективность"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,6 @@
|
|||||||
using IW4MAdmin.Application.Migration;
|
using IW4MAdmin.Application.Migration;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Localization;
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -11,194 +9,226 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
static public double Version { get; private set; }
|
public static double Version { get; private set; } = Utilities.GetVersionAsDouble();
|
||||||
static public ApplicationManager ServerManager;
|
public static ApplicationManager ServerManager;
|
||||||
private static ManualResetEventSlim OnShutdownComplete = new ManualResetEventSlim();
|
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);
|
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
|
||||||
|
|
||||||
Console.OutputEncoding = Encoding.UTF8;
|
Console.OutputEncoding = Encoding.UTF8;
|
||||||
Console.ForegroundColor = ConsoleColor.Gray;
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
|
|
||||||
Version = Utilities.GetVersionAsDouble();
|
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
|
||||||
|
|
||||||
Console.WriteLine("=====================================================");
|
Console.WriteLine("=====================================================");
|
||||||
Console.WriteLine(" IW4M ADMIN");
|
Console.WriteLine(" IW4MAdmin");
|
||||||
Console.WriteLine(" by RaidMax ");
|
Console.WriteLine(" by RaidMax ");
|
||||||
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
|
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
|
||||||
Console.WriteLine("=====================================================");
|
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
|
try
|
||||||
{
|
{
|
||||||
ServerManager = ApplicationManager.GetInstance();
|
ServerManager = ApplicationManager.GetInstance();
|
||||||
|
var configuration = ServerManager.GetApplicationSettings().Configuration();
|
||||||
|
Localization.Configure.Initialize(configuration?.EnableCustomLocale ?? false ? (configuration.CustomLocale ?? "en-US") : "en-US");
|
||||||
|
|
||||||
if (ServerManager.GetApplicationSettings().Configuration() != null)
|
// do any needed housekeeping file/folder migrations
|
||||||
{
|
|
||||||
Localization.Configure.Initialize(ServerManager.GetApplicationSettings().Configuration().CustomLocale);
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Localization.Configure.Initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
loc = Utilities.CurrentLocalization.LocalizationIndex;
|
|
||||||
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
|
|
||||||
|
|
||||||
CheckDirectories();
|
|
||||||
// do any needed migrations
|
|
||||||
// todo: move out
|
|
||||||
ConfigurationMigration.MoveConfigFolder10518(null);
|
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();
|
await CheckVersion();
|
||||||
|
await ServerManager.Init();
|
||||||
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"]} [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"]} [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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
string failMessage = loc == null ? "Failed to initalize IW4MAdmin" : loc["MANAGER_INIT_FAIL"];
|
string failMessage = loc == null ? "Failed to initalize IW4MAdmin" : loc["MANAGER_INIT_FAIL"];
|
||||||
string exitMessage = loc == null ? "Press any key to exit..." : loc["MANAGER_EXIT"];
|
string exitMessage = loc == null ? "Press any key to exit..." : loc["MANAGER_EXIT"];
|
||||||
|
|
||||||
Console.WriteLine(failMessage);
|
Console.WriteLine(failMessage);
|
||||||
|
|
||||||
while (e.InnerException != null)
|
while (e.InnerException != null)
|
||||||
{
|
{
|
||||||
e = e.InnerException;
|
e = e.InnerException;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine(e.Message);
|
Console.WriteLine(e.Message);
|
||||||
Console.WriteLine(exitMessage);
|
Console.WriteLine(exitMessage);
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront)
|
try
|
||||||
{
|
{
|
||||||
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
|
ApplicationTask = RunApplicationTasksAsync();
|
||||||
|
await ApplicationTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
OnShutdownComplete.Reset();
|
catch { }
|
||||||
ServerManager.Start();
|
|
||||||
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
|
if (ServerManager.IsRestartRequested)
|
||||||
OnShutdownComplete.Set();
|
{
|
||||||
|
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();
|
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ?
|
||||||
OnShutdownComplete.Wait();
|
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>
|
/// </summary>
|
||||||
class ConfigurationMigration
|
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>
|
/// <summary>
|
||||||
/// moves existing configs from the root folder into a configs folder
|
/// moves existing configs from the root folder into a configs folder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -56,5 +77,14 @@ namespace IW4MAdmin.Application.Migration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ModifyLogPath020919(SharedLibraryCore.Configuration.ServerConfiguration config)
|
||||||
|
{
|
||||||
|
if (config.ManualLogPath.IsRemoteLog())
|
||||||
|
{
|
||||||
|
config.GameLogServerUrl = new Uri(config.ManualLogPath);
|
||||||
|
config.ManualLogPath = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
class Logger : SharedLibraryCore.Interfaces.ILogger
|
class Logger : ILogger
|
||||||
{
|
{
|
||||||
enum LogType
|
enum LogType
|
||||||
{
|
{
|
||||||
@ -72,11 +73,13 @@ namespace IW4MAdmin.Application
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
|
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
|
||||||
Console.WriteLine(LogLine);
|
Console.WriteLine(LogLine);
|
||||||
File.AppendAllText(FileName, LogLine + Environment.NewLine);
|
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
|
||||||
|
//Debug.WriteLine(msg);
|
||||||
#else
|
#else
|
||||||
if (type == LogType.Error || type == LogType.Verbose)
|
if (type == LogType.Error || type == LogType.Verbose)
|
||||||
|
{
|
||||||
Console.WriteLine(LogLine);
|
Console.WriteLine(LogLine);
|
||||||
//if (type != LogType.Debug)
|
}
|
||||||
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
|
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
100
Application/Misc/TokenAuthentication.cs
Normal file
100
Application/Misc/TokenAuthentication.cs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
using SharedLibraryCore.Helpers;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Misc
|
||||||
|
{
|
||||||
|
class TokenAuthentication : ITokenAuthentication
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<long, TokenState> _tokens;
|
||||||
|
private readonly RNGCryptoServiceProvider _random;
|
||||||
|
private readonly static TimeSpan _timeoutPeriod = new TimeSpan(0, 0, 120);
|
||||||
|
private const short TOKEN_LENGTH = 4;
|
||||||
|
|
||||||
|
public TokenAuthentication()
|
||||||
|
{
|
||||||
|
_tokens = new ConcurrentDictionary<long, TokenState>();
|
||||||
|
_random = new RNGCryptoServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AuthorizeToken(long networkId, string token)
|
||||||
|
{
|
||||||
|
bool authorizeSuccessful = _tokens.ContainsKey(networkId) && _tokens[networkId].Token == token;
|
||||||
|
|
||||||
|
if (authorizeSuccessful)
|
||||||
|
{
|
||||||
|
_tokens.TryRemove(networkId, out TokenState _);
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizeSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenState GenerateNextToken(long networkId)
|
||||||
|
{
|
||||||
|
TokenState state = null;
|
||||||
|
|
||||||
|
if (_tokens.ContainsKey(networkId))
|
||||||
|
{
|
||||||
|
state = _tokens[networkId];
|
||||||
|
|
||||||
|
if ((DateTime.Now - state.RequestTime) > _timeoutPeriod)
|
||||||
|
{
|
||||||
|
_tokens.TryRemove(networkId, out TokenState _);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state = new TokenState()
|
||||||
|
{
|
||||||
|
NetworkId = networkId,
|
||||||
|
Token = _generateToken(),
|
||||||
|
TokenDuration = _timeoutPeriod
|
||||||
|
};
|
||||||
|
|
||||||
|
_tokens.TryAdd(networkId, state);
|
||||||
|
|
||||||
|
// perform some housekeeping so we don't have built up tokens if they're not ever used
|
||||||
|
foreach (var (key, value) in _tokens)
|
||||||
|
{
|
||||||
|
if ((DateTime.Now - value.RequestTime) > _timeoutPeriod)
|
||||||
|
{
|
||||||
|
_tokens.TryRemove(key, out TokenState _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string _generateToken()
|
||||||
|
{
|
||||||
|
bool validCharacter(char c)
|
||||||
|
{
|
||||||
|
// this ensure that the characters are 0-9, A-Z, a-z
|
||||||
|
return (c > 47 && c < 58) || (c > 64 && c < 91) || (c > 96 && c < 123);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder token = new StringBuilder();
|
||||||
|
|
||||||
|
while (token.Length < TOKEN_LENGTH)
|
||||||
|
{
|
||||||
|
byte[] charSet = new byte[1];
|
||||||
|
_random.GetBytes(charSet);
|
||||||
|
|
||||||
|
if (validCharacter((char)charSet[0]))
|
||||||
|
{
|
||||||
|
token.Append((char)charSet[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_random.Dispose();
|
||||||
|
return token.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
185
Application/RconParsers/BaseRConParser.cs
Normal file
185
Application/RconParsers/BaseRConParser.cs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Exceptions;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using SharedLibraryCore.RCon;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static SharedLibraryCore.Server;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
public class BaseRConParser : IRConParser
|
||||||
|
#else
|
||||||
|
class BaseRConParser : IRConParser
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
public BaseRConParser()
|
||||||
|
{
|
||||||
|
Configuration = new DynamicRConParserConfiguration()
|
||||||
|
{
|
||||||
|
CommandPrefixes = new CommandPrefix()
|
||||||
|
{
|
||||||
|
Tell = "tell {0} {1}",
|
||||||
|
Say = "say {0}",
|
||||||
|
Kick = "clientkick {0} \"{1}\"",
|
||||||
|
Ban = "clientkick {0} \"{1}\"",
|
||||||
|
TempBan = "tempbanclient {0} \"{1}\"",
|
||||||
|
RConCommand = "ÿÿÿÿrcon {0} {1}",
|
||||||
|
RConGetDvar = "ÿÿÿÿrcon {0} {1}",
|
||||||
|
RConSetDvar = "ÿÿÿÿrcon {0} set {1}",
|
||||||
|
RConGetStatus = "ÿÿÿÿgetstatus",
|
||||||
|
RConGetInfo = "ÿÿÿÿgetinfo",
|
||||||
|
RConResponse = "ÿÿÿÿprint",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
Configuration.Status.AddMapping(ParserRegex.GroupType.RConNetworkId, 4);
|
||||||
|
Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5);
|
||||||
|
Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7);
|
||||||
|
|
||||||
|
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n(?:latched: \"(.+)?\"\n)? *(.+)$";
|
||||||
|
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1);
|
||||||
|
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
|
||||||
|
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
|
||||||
|
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4);
|
||||||
|
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRConParserConfiguration Configuration { get; set; }
|
||||||
|
|
||||||
|
public virtual string Version { get; set; } = "CoD";
|
||||||
|
public Game GameName { get; set; } = Game.COD;
|
||||||
|
public bool CanGenerateLogPath { get; set; } = true;
|
||||||
|
|
||||||
|
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
|
||||||
|
{
|
||||||
|
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
|
||||||
|
return response.Skip(1).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dvar<T>> GetDvarAsync<T>(Connection connection, string dvarName)
|
||||||
|
{
|
||||||
|
string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName);
|
||||||
|
string response = string.Join('\n', lineSplit.Skip(1));
|
||||||
|
var match = Regex.Match(response, Configuration.Dvar.Pattern);
|
||||||
|
|
||||||
|
if (!lineSplit[0].Contains(Configuration.CommandPrefixes.RConResponse) ||
|
||||||
|
response.Contains("Unknown command") ||
|
||||||
|
!match.Success)
|
||||||
|
{
|
||||||
|
throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName));
|
||||||
|
}
|
||||||
|
|
||||||
|
string value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value.StripColors();
|
||||||
|
string defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value.StripColors();
|
||||||
|
string latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value.StripColors();
|
||||||
|
|
||||||
|
return new Dvar<T>()
|
||||||
|
{
|
||||||
|
Name = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarName]].Value.StripColors(),
|
||||||
|
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 virtual async Task<List<EFClient>> GetStatusAsync(Connection connection)
|
||||||
|
{
|
||||||
|
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
|
||||||
|
return ClientsFromStatus(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
|
||||||
|
{
|
||||||
|
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, $"{dvarName} {dvarValue}")).Length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EFClient> ClientsFromStatus(string[] Status)
|
||||||
|
{
|
||||||
|
List<EFClient> StatusPlayers = new List<EFClient>();
|
||||||
|
|
||||||
|
if (Status.Length < 4)
|
||||||
|
{
|
||||||
|
throw new ServerException("Unexpected status response received");
|
||||||
|
}
|
||||||
|
|
||||||
|
int validMatches = 0;
|
||||||
|
foreach (string statusLine in Status)
|
||||||
|
{
|
||||||
|
string responseLine = statusLine.Trim();
|
||||||
|
|
||||||
|
var regex = Regex.Match(responseLine, Configuration.Status.Pattern, RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
if (regex.Success)
|
||||||
|
{
|
||||||
|
validMatches++;
|
||||||
|
int clientNumber = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]].Value);
|
||||||
|
int score = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]].Value);
|
||||||
|
|
||||||
|
int ping = 999;
|
||||||
|
|
||||||
|
// their state can be CNCT, ZMBI etc
|
||||||
|
if (regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value.Length <= 3)
|
||||||
|
{
|
||||||
|
ping = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
var client = new EFClient()
|
||||||
|
{
|
||||||
|
CurrentAlias = new EFAlias()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
IPAddress = ip
|
||||||
|
},
|
||||||
|
NetworkId = networkId,
|
||||||
|
ClientNumber = clientNumber,
|
||||||
|
Ping = ping,
|
||||||
|
Score = score,
|
||||||
|
State = EFClient.ClientState.Connecting
|
||||||
|
};
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
if (client.NetworkId < 1000 && client.NetworkId > 0)
|
||||||
|
{
|
||||||
|
client.IPAddress = 2147483646;
|
||||||
|
client.Ping = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
StatusPlayers.Add(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this happens if status is requested while map is rotating
|
||||||
|
if (Status.Length > 5 && validMatches == 0)
|
||||||
|
{
|
||||||
|
throw new ServerException("Server is rotating map");
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusPlayers;
|
||||||
|
}
|
||||||
|
}}
|
@ -1,12 +1,10 @@
|
|||||||
using SharedLibraryCore;
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
using SharedLibraryCore.RCon;
|
|
||||||
using System;
|
|
||||||
using static SharedLibraryCore.Server;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.RconParsers
|
|
||||||
{
|
{
|
||||||
sealed internal class DynamicRConParser : IW4RConParser
|
/// <summary>
|
||||||
|
/// empty implementation of the IW4RConParser
|
||||||
|
/// allows script plugins to generate dynamic RCon parsers
|
||||||
|
/// </summary>
|
||||||
|
sealed internal class DynamicRConParser : BaseRConParser
|
||||||
{
|
{
|
||||||
public string Version { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,15 @@ using SharedLibraryCore.RCon;
|
|||||||
|
|
||||||
namespace IW4MAdmin.Application.RconParsers
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
{
|
{
|
||||||
class DynamicRConParserConfiguration : IRConParserConfiguration
|
/// <summary>
|
||||||
|
/// generic implementation of the IRConParserConfiguration
|
||||||
|
/// allows script plugins to generate dynamic RCon configurations
|
||||||
|
/// </summary>
|
||||||
|
sealed internal class DynamicRConParserConfiguration : IRConParserConfiguration
|
||||||
{
|
{
|
||||||
public CommandPrefix CommandPrefixes { get; set; }
|
public CommandPrefix CommandPrefixes { get; set; }
|
||||||
public Server.Game GameName { get; set; }
|
|
||||||
public ParserRegex Status { get; set; } = new ParserRegex();
|
public ParserRegex Status { get; set; } = new ParserRegex();
|
||||||
|
public ParserRegex Dvar { get; set; } = new ParserRegex();
|
||||||
|
public bool WaitForResponse { get; set; } = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.RCon;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.RconParsers
|
|
||||||
{
|
|
||||||
class IW3RConParser : IW4RConParser
|
|
||||||
{
|
|
||||||
public IW3RConParser() : base()
|
|
||||||
{
|
|
||||||
Configuration.CommandPrefixes = new CommandPrefix()
|
|
||||||
{
|
|
||||||
Tell = "tell {0} {1}",
|
|
||||||
Say = "say {0}",
|
|
||||||
Kick = "clientkick {0} \"{1}\"",
|
|
||||||
Ban = "clientkick {0} \"{1}\"",
|
|
||||||
TempBan = "tempbanclient {0} \"{1}\""
|
|
||||||
};
|
|
||||||
|
|
||||||
Configuration.GameName = Server.Game.IW3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,158 +0,0 @@
|
|||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Database.Models;
|
|
||||||
using SharedLibraryCore.Exceptions;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using SharedLibraryCore.RCon;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.RconParsers
|
|
||||||
{
|
|
||||||
class IW4RConParser : IRConParser
|
|
||||||
{
|
|
||||||
public IW4RConParser()
|
|
||||||
{
|
|
||||||
Configuration = new DynamicRConParserConfiguration()
|
|
||||||
{
|
|
||||||
CommandPrefixes = new CommandPrefix()
|
|
||||||
{
|
|
||||||
Tell = "tellraw {0} {1}",
|
|
||||||
Say = "sayraw {0}",
|
|
||||||
Kick = "clientkick {0} \"{1}\"",
|
|
||||||
Ban = "clientkick {0} \"{1}\"",
|
|
||||||
TempBan = "tempbanclient {0} \"{1}\""
|
|
||||||
},
|
|
||||||
GameName = Server.Game.IW4
|
|
||||||
};
|
|
||||||
|
|
||||||
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]+) *$";
|
|
||||||
Configuration.Status.GroupMapping.Add(ParserRegex.GroupType.RConClientNumber, 1);
|
|
||||||
Configuration.Status.GroupMapping.Add(ParserRegex.GroupType.RConScore, 2);
|
|
||||||
Configuration.Status.GroupMapping.Add(ParserRegex.GroupType.RConPing, 3);
|
|
||||||
Configuration.Status.GroupMapping.Add(ParserRegex.GroupType.RConNetworkId, 4);
|
|
||||||
Configuration.Status.GroupMapping.Add(ParserRegex.GroupType.RConName, 5);
|
|
||||||
Configuration.Status.GroupMapping.Add(ParserRegex.GroupType.RConIpAddress, 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IRConParserConfiguration Configuration { get; set; }
|
|
||||||
|
|
||||||
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
|
|
||||||
{
|
|
||||||
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
|
|
||||||
return response.Skip(1).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Dvar<T>> GetDvarAsync<T>(Connection connection, string dvarName)
|
|
||||||
{
|
|
||||||
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.DVAR, dvarName);
|
|
||||||
|
|
||||||
if (LineSplit.Length < 3)
|
|
||||||
{
|
|
||||||
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
|
|
||||||
e.Data["dvar_name"] = dvarName;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: can this be made more portable and modifiable from plugin
|
|
||||||
string[] ValueSplit = LineSplit[1].Split(new char[] { '"' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
if (ValueSplit.Length < 5)
|
|
||||||
{
|
|
||||||
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
|
|
||||||
e.Data["dvar_name"] = dvarName;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
string DvarName = Regex.Replace(ValueSplit[0], @"\^[0-9]", "");
|
|
||||||
string DvarCurrentValue = Regex.Replace(ValueSplit[2], @"\^[0-9]", "");
|
|
||||||
string DvarDefaultValue = Regex.Replace(ValueSplit[4], @"\^[0-9]", "");
|
|
||||||
|
|
||||||
return new Dvar<T>(DvarName)
|
|
||||||
{
|
|
||||||
Value = (T)Convert.ChangeType(DvarCurrentValue, typeof(T))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<EFClient>> GetStatusAsync(Connection connection)
|
|
||||||
{
|
|
||||||
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status");
|
|
||||||
return ClientsFromStatus(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
|
|
||||||
{
|
|
||||||
return (await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"set {dvarName} {dvarValue}")).Length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<EFClient> ClientsFromStatus(string[] Status)
|
|
||||||
{
|
|
||||||
List<EFClient> StatusPlayers = new List<EFClient>();
|
|
||||||
|
|
||||||
if (Status.Length < 4)
|
|
||||||
{
|
|
||||||
throw new ServerException("Unexpected status response received");
|
|
||||||
}
|
|
||||||
|
|
||||||
int validMatches = 0;
|
|
||||||
foreach (String S in Status)
|
|
||||||
{
|
|
||||||
String responseLine = S.Trim();
|
|
||||||
|
|
||||||
var regex = Regex.Match(responseLine, Configuration.Status.Pattern, RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
if (regex.Success)
|
|
||||||
{
|
|
||||||
validMatches++;
|
|
||||||
int clientNumber = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]].Value);
|
|
||||||
int score = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]].Value);
|
|
||||||
|
|
||||||
int ping = 999;
|
|
||||||
|
|
||||||
// their state can be CNCT, ZMBI etc
|
|
||||||
if (regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value.Length <= 3)
|
|
||||||
{
|
|
||||||
ping = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
long networkId = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].Value.ConvertLong();
|
|
||||||
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();
|
|
||||||
|
|
||||||
var client = new EFClient()
|
|
||||||
{
|
|
||||||
CurrentAlias = new EFAlias()
|
|
||||||
{
|
|
||||||
Name = name
|
|
||||||
},
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusPlayers.Add(client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this happens if status is requested while map is rotating
|
|
||||||
if (Status.Length > 5 && validMatches == 0)
|
|
||||||
{
|
|
||||||
throw new ServerException("Server is rotating map");
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusPlayers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,131 +0,0 @@
|
|||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Database.Models;
|
|
||||||
using SharedLibraryCore.Exceptions;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using SharedLibraryCore.RCon;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.RconParsers
|
|
||||||
{
|
|
||||||
public class T6MRConParser : IRConParser
|
|
||||||
{
|
|
||||||
public IRConParserConfiguration Configuration { get; set; }
|
|
||||||
|
|
||||||
public T6MRConParser()
|
|
||||||
{
|
|
||||||
Configuration = new DynamicRConParserConfiguration()
|
|
||||||
{
|
|
||||||
CommandPrefixes = new CommandPrefix()
|
|
||||||
{
|
|
||||||
Tell = "tell {0} {1}",
|
|
||||||
Say = "say {0}",
|
|
||||||
Kick = "clientkick_for_reason {0} \"{1}\"",
|
|
||||||
Ban = "clientkick_for_reason {0} \"{1}\"",
|
|
||||||
TempBan = "clientkick_for_reason {0} \"{1}\""
|
|
||||||
},
|
|
||||||
GameName = Server.Game.T6M
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
|
|
||||||
{
|
|
||||||
await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, false);
|
|
||||||
return new string[] { "Command Executed" };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Dvar<T>> GetDvarAsync<T>(Connection connection, string dvarName)
|
|
||||||
{
|
|
||||||
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"get {dvarName}");
|
|
||||||
|
|
||||||
if (LineSplit.Length < 2)
|
|
||||||
{
|
|
||||||
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
|
|
||||||
e.Data["dvar_name"] = dvarName;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
string[] ValueSplit = LineSplit[1].Split(new char[] { '"' });
|
|
||||||
|
|
||||||
if (ValueSplit.Length == 0)
|
|
||||||
{
|
|
||||||
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
|
|
||||||
e.Data["dvar_name"] = dvarName;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
string DvarName = dvarName;
|
|
||||||
string DvarCurrentValue = Regex.Replace(ValueSplit[1], @"\^[0-9]", "");
|
|
||||||
|
|
||||||
return new Dvar<T>(DvarName)
|
|
||||||
{
|
|
||||||
Value = (T)Convert.ChangeType(DvarCurrentValue, typeof(T))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<EFClient>> GetStatusAsync(Connection connection)
|
|
||||||
{
|
|
||||||
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status");
|
|
||||||
return ClientsFromStatus(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
|
|
||||||
{
|
|
||||||
// T6M doesn't respond with anything when a value is set, so we can only hope for the best :c
|
|
||||||
await connection.SendQueryAsync(StaticHelpers.QueryType.DVAR, $"set {dvarName} {dvarValue}", false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<EFClient> ClientsFromStatus(string[] status)
|
|
||||||
{
|
|
||||||
List<EFClient> StatusPlayers = new List<EFClient>();
|
|
||||||
|
|
||||||
foreach (string statusLine in status)
|
|
||||||
{
|
|
||||||
String responseLine = statusLine;
|
|
||||||
|
|
||||||
if (Regex.Matches(responseLine, @"^ *\d+", RegexOptions.IgnoreCase).Count > 0) // its a client line!
|
|
||||||
{
|
|
||||||
String[] playerInfo = responseLine.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
int clientId = -1;
|
|
||||||
int Ping = -1;
|
|
||||||
|
|
||||||
Int32.TryParse(playerInfo[3], out Ping);
|
|
||||||
var regex = Regex.Match(responseLine, @"\^7.*\ +0 ");
|
|
||||||
string name = Encoding.UTF8.GetString(Encoding.Convert(Utilities.EncodingType, Encoding.UTF8, Utilities.EncodingType.GetBytes(regex.Value.Substring(0, regex.Value.Length - 2).StripColors().Trim())));
|
|
||||||
long networkId = playerInfo[4].ConvertLong();
|
|
||||||
int.TryParse(playerInfo[0], out clientId);
|
|
||||||
regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}");
|
|
||||||
#if DEBUG
|
|
||||||
Ping = 1;
|
|
||||||
#endif
|
|
||||||
int? ipAddress = regex.Value.Split(':')[0].ConvertToIP();
|
|
||||||
regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+");
|
|
||||||
var p = new EFClient()
|
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
NetworkId = networkId,
|
|
||||||
ClientNumber = clientId,
|
|
||||||
IPAddress = ipAddress,
|
|
||||||
Ping = Ping,
|
|
||||||
Score = 0,
|
|
||||||
State = EFClient.ClientState.Connecting,
|
|
||||||
IsBot = ipAddress == null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (p.IsBot)
|
|
||||||
{
|
|
||||||
p.NetworkId = -p.ClientNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusPlayers.Add(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusPlayers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
var plugin = {
|
|
||||||
author: 'RaidMax',
|
|
||||||
version: 1.1,
|
|
||||||
name: 'Shared GUID Kicker Plugin',
|
|
||||||
|
|
||||||
onEventAsync: function (gameEvent, server) {
|
|
||||||
// make sure we only check for IW4(x)
|
|
||||||
if (server.GameName !== 2) {
|
|
||||||
return false;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
gameEvent.Origin.Kick('Your GUID is generic. Delete players/guids.dat and rejoin', _IW4MAdminClient);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onLoadAsync: function (manager) {
|
|
||||||
},
|
|
||||||
|
|
||||||
onUnloadAsync: function () {
|
|
||||||
},
|
|
||||||
|
|
||||||
onTickAsync: function (server) {
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,62 +0,0 @@
|
|||||||
var plugin = {
|
|
||||||
author: 'RaidMax',
|
|
||||||
version: 1.0,
|
|
||||||
name: 'VPN Detection Plugin',
|
|
||||||
|
|
||||||
manager: null,
|
|
||||||
logger: null,
|
|
||||||
vpnExceptionIds: [],
|
|
||||||
|
|
||||||
checkForVpn: function (origin) {
|
|
||||||
var exempt = false;
|
|
||||||
// prevent players that are exempt from being kicked
|
|
||||||
this.vpnExceptionIds.forEach(function (id) {
|
|
||||||
if (id === origin.ClientId) {
|
|
||||||
exempt = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (exempt) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var usingVPN = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
var cl = new System.Net.Http.HttpClient();
|
|
||||||
var re = cl.GetAsync('https://api.xdefcon.com/proxy/check/?ip=' + origin.IPAddressString).Result;
|
|
||||||
var co = re.Content;
|
|
||||||
var parsedJSON = JSON.parse(co.ReadAsStringAsync().Result);
|
|
||||||
co.Dispose();
|
|
||||||
re.Dispose();
|
|
||||||
cl.Dispose();
|
|
||||||
usingVPN = parsedJSON.success && parsedJSON.proxy;
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.WriteError(e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (usingVPN) {
|
|
||||||
this.logger.WriteInfo(origin + ' is using a VPN (' + origin.IPAddressString + ')');
|
|
||||||
origin.Kick(_localization.LocalizationIndex["SERVER_KICK_VPNS_NOTALLOWED"], _IW4MAdminClient);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onEventAsync: function (gameEvent, server) {
|
|
||||||
// connect event
|
|
||||||
if (gameEvent.Type === 3) {
|
|
||||||
this.checkForVpn(gameEvent.Origin);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onLoadAsync: function (manager) {
|
|
||||||
this.manager = manager;
|
|
||||||
this.logger = manager.GetLogger(0);
|
|
||||||
},
|
|
||||||
|
|
||||||
onUnloadAsync: function () {
|
|
||||||
},
|
|
||||||
|
|
||||||
onTickAsync: function (server) {
|
|
||||||
}
|
|
||||||
};
|
|
@ -11,13 +11,15 @@
|
|||||||
<SearchPath>
|
<SearchPath>
|
||||||
</SearchPath>
|
</SearchPath>
|
||||||
<WorkingDirectory>.</WorkingDirectory>
|
<WorkingDirectory>.</WorkingDirectory>
|
||||||
<LaunchProvider>Web launcher</LaunchProvider>
|
<LaunchProvider>Standard Python launcher</LaunchProvider>
|
||||||
<WebBrowserUrl>http://localhost</WebBrowserUrl>
|
<WebBrowserUrl>http://localhost</WebBrowserUrl>
|
||||||
<OutputPath>.</OutputPath>
|
<OutputPath>.</OutputPath>
|
||||||
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
|
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
|
||||||
<Name>GameLogServer</Name>
|
<Name>GameLogServer</Name>
|
||||||
<RootNamespace>GameLogServer</RootNamespace>
|
<RootNamespace>GameLogServer</RootNamespace>
|
||||||
<InterpreterId>MSBuild|env|$(MSBuildProjectFullPath)</InterpreterId>
|
<InterpreterId>MSBuild|log_env|$(MSBuildProjectFullPath)</InterpreterId>
|
||||||
|
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
|
||||||
|
<Environment>DEBUG=True</Environment>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
@ -48,7 +50,6 @@
|
|||||||
<Folder Include="GameLogServer\" />
|
<Folder Include="GameLogServer\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="requirements.txt" />
|
|
||||||
<None Include="Stable.pubxml" />
|
<None Include="Stable.pubxml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -61,6 +62,18 @@
|
|||||||
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
|
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
|
||||||
<Architecture>X64</Architecture>
|
<Architecture>X64</Architecture>
|
||||||
</Interpreter>
|
</Interpreter>
|
||||||
|
<Interpreter Include="log_env\">
|
||||||
|
<Id>log_env</Id>
|
||||||
|
<Version>3.6</Version>
|
||||||
|
<Description>log_env (Python 3.6 (64-bit))</Description>
|
||||||
|
<InterpreterPath>Scripts\python.exe</InterpreterPath>
|
||||||
|
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
|
||||||
|
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
|
||||||
|
<Architecture>X64</Architecture>
|
||||||
|
</Interpreter>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="requirements.txt" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
|
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
|
||||||
<!-- Specify pre- and post-build commands in the BeforeBuild and
|
<!-- Specify pre- and post-build commands in the BeforeBuild and
|
||||||
|
@ -1,76 +1,118 @@
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
class LogReader(object):
|
class LogReader(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.log_file_sizes = {}
|
self.log_file_sizes = {}
|
||||||
# (if the file changes more than this, ignore ) - 0.125 MB
|
# (if the time between checks is greater, ignore ) - in seconds
|
||||||
self.max_file_size_change = 125000
|
self.max_file_time_change = 30
|
||||||
# (if the time between checks is greater, ignore ) - 5 minutes
|
|
||||||
self.max_file_time_change = 60
|
def read_file(self, path, retrieval_key):
|
||||||
|
# this removes old entries that are no longer valid
|
||||||
|
try:
|
||||||
|
self._clear_old_logs()
|
||||||
|
except Exception as e:
|
||||||
|
print('could not clear old logs')
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
if os.name != 'nt':
|
||||||
|
path = re.sub(r'^[A-Z]\:', '', path)
|
||||||
|
path = re.sub(r'\\+', '/', path)
|
||||||
|
|
||||||
def read_file(self, path):
|
|
||||||
# prevent traversing directories
|
# prevent traversing directories
|
||||||
if re.search('r^.+\.\.\\.+$', path):
|
if re.search('r^.+\.\.\\.+$', path):
|
||||||
return False
|
return self._generate_bad_response()
|
||||||
|
|
||||||
# must be a valid log path and log file
|
# must be a valid log path and log file
|
||||||
if not re.search(r'^.+[\\|\/](userraw|mods|main)[\\|\/].+.log$', path):
|
if not re.search(r'^.+[\\|\/](.+)[\\|\/].+.log$', path):
|
||||||
return False
|
return self._generate_bad_response()
|
||||||
# set the initialze size to the current file size
|
|
||||||
file_size = 0
|
|
||||||
|
|
||||||
if path not in self.log_file_sizes:
|
# get the new file size
|
||||||
self.log_file_sizes[path] = {
|
|
||||||
'length' : self.file_length(path),
|
|
||||||
'read': time.time()
|
|
||||||
}
|
|
||||||
return True
|
|
||||||
|
|
||||||
# grab the previous values
|
|
||||||
last_length = self.log_file_sizes[path]['length']
|
|
||||||
last_read = self.log_file_sizes[path]['read']
|
|
||||||
|
|
||||||
# the file is being tracked already
|
|
||||||
new_file_size = self.file_length(path)
|
new_file_size = self.file_length(path)
|
||||||
|
|
||||||
# the log size was unable to be read (probably the wrong path)
|
# the log size was unable to be read (probably the wrong path)
|
||||||
if new_file_size < 0:
|
if new_file_size < 0:
|
||||||
return False
|
return self._generate_bad_response()
|
||||||
|
|
||||||
now = time.time()
|
next_retrieval_key = self._generate_key()
|
||||||
|
|
||||||
file_size_difference = new_file_size - last_length
|
|
||||||
time_difference = now - last_read
|
|
||||||
|
|
||||||
# update the new size and actually read the data
|
# this is the first time the key has been requested, so we need to the next one
|
||||||
self.log_file_sizes[path] = {
|
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:
|
||||||
'length': new_file_size,
|
print('retrieval key "%s" does not exist or is outdated' % retrieval_key)
|
||||||
'read': now
|
last_log_info = {
|
||||||
|
'size' : new_file_size,
|
||||||
|
'previous_key' : None
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
last_log_info = self.log_file_sizes[retrieval_key]
|
||||||
|
|
||||||
|
print('next key is %s' % next_retrieval_key)
|
||||||
|
expired_key = last_log_info['previous_key']
|
||||||
|
print('expired key is %s' % expired_key)
|
||||||
|
|
||||||
|
# grab the previous value
|
||||||
|
last_size = last_log_info['size']
|
||||||
|
file_size_difference = new_file_size - last_size
|
||||||
|
|
||||||
|
#print('generating info for next key %s' % next_retrieval_key)
|
||||||
|
|
||||||
|
# update the new size
|
||||||
|
self.log_file_sizes[next_retrieval_key] = {
|
||||||
|
'size' : new_file_size,
|
||||||
|
'read': time.time(),
|
||||||
|
'next_key': next_retrieval_key,
|
||||||
|
'previous_key': retrieval_key
|
||||||
}
|
}
|
||||||
|
|
||||||
# if it's been too long since we read and the amount changed is too great, discard it
|
if expired_key in self.log_file_sizes:
|
||||||
# todo: do we really want old events? maybe make this an "or"
|
print('deleting expired key %s' % expired_key)
|
||||||
if file_size_difference > self.max_file_size_change or time_difference > self.max_file_time_change:
|
del self.log_file_sizes[expired_key]
|
||||||
return True
|
|
||||||
|
|
||||||
new_log_info = self.get_file_lines(path, file_size_difference)
|
#print('reading %i bytes starting at %i' % (file_size_difference, last_size))
|
||||||
return new_log_info
|
|
||||||
|
|
||||||
def get_file_lines(self, path, length):
|
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:
|
try:
|
||||||
file_handle = open(path, 'rb')
|
file_handle = open(path, 'rb')
|
||||||
file_handle.seek(-length, 2)
|
file_handle.seek(start_position)
|
||||||
file_data = file_handle.read(length)
|
file_data = file_handle.read(length_to_read)
|
||||||
file_handle.close()
|
file_handle.close()
|
||||||
return file_data.decode('utf-8')
|
# using ignore errors omits the pesky 0xb2 bytes we're reading in for some reason
|
||||||
except:
|
return file_data.decode('utf-8', errors='ignore')
|
||||||
|
except Exception as e:
|
||||||
|
print('could not read the log file at {0}, wanted to read {1} bytes'.format(path, length_to_read))
|
||||||
|
print(e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _clear_old_logs(self):
|
||||||
|
expired_logs = [path for path in self.log_file_sizes if int(time.time() - self.log_file_sizes[path]['read']) > self.max_file_time_change]
|
||||||
|
for key in expired_logs:
|
||||||
|
print('removing expired log with key {0}'.format(key))
|
||||||
|
del self.log_file_sizes[key]
|
||||||
|
|
||||||
|
def _generate_bad_response(self):
|
||||||
|
return {
|
||||||
|
'content': None,
|
||||||
|
'next_key': None
|
||||||
|
}
|
||||||
|
|
||||||
|
def _generate_key(self):
|
||||||
|
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||||||
|
|
||||||
def file_length(self, path):
|
def file_length(self, path):
|
||||||
try:
|
try:
|
||||||
return os.stat(path).st_size
|
return os.stat(path).st_size
|
||||||
except:
|
except Exception as e:
|
||||||
|
print('could not get the size of the log file at {0}'.format(path))
|
||||||
|
print(e)
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
reader = LogReader()
|
reader = LogReader()
|
||||||
|
@ -3,17 +3,14 @@ from GameLogServer.log_reader import reader
|
|||||||
from base64 import urlsafe_b64decode
|
from base64 import urlsafe_b64decode
|
||||||
|
|
||||||
class LogResource(Resource):
|
class LogResource(Resource):
|
||||||
def get(self, path):
|
def get(self, path, retrieval_key):
|
||||||
path = urlsafe_b64decode(path).decode('utf-8')
|
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']
|
||||||
if log_info is False:
|
|
||||||
print('could not read log file ' + path)
|
|
||||||
|
|
||||||
empty_read = (log_info == False) or (log_info == True)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success' : log_info is not False,
|
'success' : content is not None,
|
||||||
'length': -1 if empty_read else len(log_info),
|
'length': 0 if content is None else len(content),
|
||||||
'data': log_info
|
'data': content,
|
||||||
|
'next_key': log_info['next_key']
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,29 @@
|
|||||||
from flask_restful import Resource
|
#from flask_restful import Resource
|
||||||
from flask import request
|
#from flask import request
|
||||||
import requests
|
#import requests
|
||||||
import os
|
#import os
|
||||||
import subprocess
|
#import subprocess
|
||||||
import re
|
#import re
|
||||||
|
|
||||||
def get_pid_of_server_windows(port):
|
#def get_pid_of_server_windows(port):
|
||||||
process = subprocess.Popen('netstat -aon', shell=True, stdout=subprocess.PIPE)
|
# process = subprocess.Popen('netstat -aon', shell=True, stdout=subprocess.PIPE)
|
||||||
output = process.communicate()[0]
|
# output = process.communicate()[0]
|
||||||
matches = re.search(' *(UDP) +([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}):'+ str(port) + ' +[^\w]*([0-9]+)', output.decode('utf-8'))
|
# matches = re.search(' *(UDP) +([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}):'+ str(port) + ' +[^\w]*([0-9]+)', output.decode('utf-8'))
|
||||||
if matches is not None:
|
# if matches is not None:
|
||||||
return matches.group(3)
|
# return matches.group(3)
|
||||||
else:
|
# else:
|
||||||
return 0
|
# return 0
|
||||||
|
|
||||||
class RestartResource(Resource):
|
#class RestartResource(Resource):
|
||||||
def get(self):
|
# def get(self):
|
||||||
try:
|
# try:
|
||||||
response = requests.get('http://' + request.remote_addr + ':1624/api/restartapproved')
|
# response = requests.get('http://' + request.remote_addr + ':1624/api/restartapproved')
|
||||||
if response.status_code == 200:
|
# if response.status_code == 200:
|
||||||
pid = get_pid_of_server_windows(response.json()['port'])
|
# pid = get_pid_of_server_windows(response.json()['port'])
|
||||||
subprocess.check_output("Taskkill /PID %s /F" % pid)
|
# subprocess.check_output("Taskkill /PID %s /F" % pid)
|
||||||
else:
|
# else:
|
||||||
return {}, 400
|
# return {}, 400
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
print(e)
|
# print(e)
|
||||||
return {}, 500
|
# return {}, 500
|
||||||
return {}, 200
|
# return {}, 200
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_restful import Api
|
from flask_restful import Api
|
||||||
from .log_resource import LogResource
|
from .log_resource import LogResource
|
||||||
from .restart_resource import RestartResource
|
#from .restart_resource import RestartResource
|
||||||
|
import logging
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
def init():
|
def init():
|
||||||
|
log = logging.getLogger('werkzeug')
|
||||||
|
log.setLevel(logging.ERROR)
|
||||||
api = Api(app)
|
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')
|
#api.add_resource(RestartResource, '/restart')
|
||||||
|
@ -1,26 +1,12 @@
|
|||||||
aniso8601==3.0.2
|
aniso8601==6.0.0
|
||||||
APScheduler==3.5.3
|
Click==7.0
|
||||||
certifi==2018.10.15
|
|
||||||
chardet==3.0.4
|
|
||||||
click==6.7
|
|
||||||
Flask==1.0.2
|
Flask==1.0.2
|
||||||
Flask-JWT==0.3.2
|
Flask-RESTful==0.3.7
|
||||||
Flask-JWT-Extended==3.8.1
|
itsdangerous==1.1.0
|
||||||
Flask-RESTful==0.3.6
|
|
||||||
idna==2.7
|
|
||||||
itsdangerous==0.24
|
|
||||||
Jinja2==2.10
|
Jinja2==2.10
|
||||||
MarkupSafe==1.0
|
MarkupSafe==1.1.1
|
||||||
marshmallow==3.0.0b8
|
pip==10.0.1
|
||||||
pip==9.0.3
|
pytz==2018.9
|
||||||
psutil==5.4.8
|
setuptools==39.0.1
|
||||||
pygal==2.4.0
|
six==1.12.0
|
||||||
PyJWT==1.4.2
|
Werkzeug==0.15.2
|
||||||
pytz==2018.7
|
|
||||||
requests==2.20.0
|
|
||||||
setuptools==40.5.0
|
|
||||||
six==1.11.0
|
|
||||||
timeago==1.0.8
|
|
||||||
tzlocal==1.5.1
|
|
||||||
urllib3==1.24
|
|
||||||
Werkzeug==0.14.1
|
|
||||||
|
@ -12,4 +12,4 @@ if __name__ == '__main__':
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
PORT = 5555
|
PORT = 5555
|
||||||
init()
|
init()
|
||||||
app.run(HOST, PORT, debug=False)
|
app.run('0.0.0.0', PORT, debug=False)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio Version 16
|
||||||
VisualStudioVersion = 15.0.26730.16
|
VisualStudioVersion = 16.0.28803.352
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8B310-269E-46D4-A612-24601F16065F}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8B310-269E-46D4-A612-24601F16065F}"
|
||||||
EndProject
|
EndProject
|
||||||
@ -34,16 +34,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Test
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
|
||||||
EndProject
|
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}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js
|
||||||
|
Plugins\ScriptPlugins\ParserIW4x.js = Plugins\ScriptPlugins\ParserIW4x.js
|
||||||
|
Plugins\ScriptPlugins\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js
|
||||||
|
Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js
|
||||||
|
Plugins\ScriptPlugins\ParserTeknoMW3.js = Plugins\ScriptPlugins\ParserTeknoMW3.js
|
||||||
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
|
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
|
||||||
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
|
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "GameLogServer", "GameLogServer\GameLogServer.pyproj", "{42EFDA12-10D3-4C40-A210-9483520116BC}"
|
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "GameLogServer", "GameLogServer\GameLogServer.pyproj", "{42EFDA12-10D3-4C40-A210-9483520116BC}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{A848FCF1-8527-4AA8-A1AA-50D29695C678}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatsWeb", "Plugins\Web\StatsWeb\StatsWeb.csproj", "{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -282,8 +291,8 @@ Global
|
|||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x64.Build.0 = Debug|Any CPU
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x86.Build.0 = Debug|Any CPU
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Any CPU.Build.0 = Debug|Any CPU
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x64.ActiveCfg = Debug|Any CPU
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x64.ActiveCfg = Debug|Any CPU
|
||||||
@ -298,28 +307,14 @@ Global
|
|||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = Release|Any CPU
|
{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.ActiveCfg = Release|Any CPU
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = 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 = Release|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|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.Build.0 = Debug|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.Build.0 = Debug|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Any CPU.ActiveCfg = Release|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.ActiveCfg = Release|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.Build.0 = Release|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.ActiveCfg = Release|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.ActiveCfg = Release|Any CPU
|
||||||
@ -334,6 +329,54 @@ Global
|
|||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.Build.0 = Release|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.ActiveCfg = Release|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.Build.0 = Release|Any CPU
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x64.Build.0 = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x86.Build.0 = Debug|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x64.Build.0 = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x86.Build.0 = Debug|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -346,6 +389,9 @@ Global
|
|||||||
{B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F}
|
{B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F}
|
{6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {26E8B310-269E-46D4-A612-24601F16065F}
|
{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
|
{A848FCF1-8527-4AA8-A1AA-50D29695C678} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
|
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B} = {A848FCF1-8527-4AA8-A1AA-50D29695C678}
|
||||||
|
{F5815359-CFC7-44B4-9A3B-C04BACAD5836} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}
|
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<SearchPath>
|
<SearchPath>
|
||||||
</SearchPath>
|
</SearchPath>
|
||||||
<WorkingDirectory>.</WorkingDirectory>
|
<WorkingDirectory>.</WorkingDirectory>
|
||||||
<LaunchProvider>Web launcher</LaunchProvider>
|
<LaunchProvider>Standard Python launcher</LaunchProvider>
|
||||||
<WebBrowserUrl>http://localhost</WebBrowserUrl>
|
<WebBrowserUrl>http://localhost</WebBrowserUrl>
|
||||||
<OutputPath>.</OutputPath>
|
<OutputPath>.</OutputPath>
|
||||||
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
|
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
|
||||||
@ -25,6 +25,7 @@
|
|||||||
</PythonDebugWebServerCommand>
|
</PythonDebugWebServerCommand>
|
||||||
<PythonRunWebServerCommandType>script</PythonRunWebServerCommandType>
|
<PythonRunWebServerCommandType>script</PythonRunWebServerCommandType>
|
||||||
<PythonDebugWebServerCommandType>script</PythonDebugWebServerCommandType>
|
<PythonDebugWebServerCommandType>script</PythonDebugWebServerCommandType>
|
||||||
|
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
@ -5,6 +5,7 @@ from marshmallow import ValidationError
|
|||||||
from master.schema.instanceschema import InstanceSchema
|
from master.schema.instanceschema import InstanceSchema
|
||||||
from master import ctx
|
from master import ctx
|
||||||
import json
|
import json
|
||||||
|
from netaddr import IPAddress
|
||||||
|
|
||||||
class Instance(Resource):
|
class Instance(Resource):
|
||||||
def get(self, id=None):
|
def get(self, id=None):
|
||||||
@ -23,7 +24,7 @@ class Instance(Resource):
|
|||||||
def put(self, id):
|
def put(self, id):
|
||||||
try:
|
try:
|
||||||
for server in request.json['servers']:
|
for server in request.json['servers']:
|
||||||
if 'ip' not in server or server['ip'] == 'localhost':
|
if 'ip' not in server or IPAddress(server['ip']).is_private() or IPAddress(server['ip']).is_loopback():
|
||||||
server['ip'] = request.remote_addr
|
server['ip'] = request.remote_addr
|
||||||
if 'version' not in server:
|
if 'version' not in server:
|
||||||
server['version'] = 'Unknown'
|
server['version'] = 'Unknown'
|
||||||
|
@ -4,7 +4,7 @@ from master.models.servermodel import ServerModel
|
|||||||
class ServerSchema(Schema):
|
class ServerSchema(Schema):
|
||||||
id = fields.Int(
|
id = fields.Int(
|
||||||
required=True,
|
required=True,
|
||||||
validate=validate.Range(1, 25525525525565535, 'invalid id')
|
validate=validate.Range(0, 25525525525565535, 'invalid id')
|
||||||
)
|
)
|
||||||
ip = fields.Str(
|
ip = fields.Str(
|
||||||
required=True
|
required=True
|
||||||
@ -35,7 +35,7 @@ class ServerSchema(Schema):
|
|||||||
)
|
)
|
||||||
map = fields.String(
|
map = fields.String(
|
||||||
required=True,
|
required=True,
|
||||||
validate=validate.Length(1, 32, 'invalid map name')
|
validate=validate.Length(0, 64, 'invalid map name')
|
||||||
)
|
)
|
||||||
gametype = fields.String(
|
gametype = fields.String(
|
||||||
required=True,
|
required=True,
|
||||||
|
25
Plugins/AutomessageFeed/AutomessageFeed.csproj
Normal file
25
Plugins/AutomessageFeed/AutomessageFeed.csproj
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<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"" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
29
Plugins/AutomessageFeed/Configuration.cs
Normal file
29
Plugins/AutomessageFeed/Configuration.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
|
namespace AutomessageFeed
|
||||||
|
{
|
||||||
|
class Configuration : IBaseConfiguration
|
||||||
|
{
|
||||||
|
public bool EnableFeed { get; set; }
|
||||||
|
public string FeedUrl { get; set; }
|
||||||
|
public int MaxFeedItems { get; set; }
|
||||||
|
|
||||||
|
public IBaseConfiguration Generate()
|
||||||
|
{
|
||||||
|
EnableFeed = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_ENABLE"]);
|
||||||
|
|
||||||
|
if (EnableFeed)
|
||||||
|
{
|
||||||
|
FeedUrl = Utilities.PromptString(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_URL"]);
|
||||||
|
MaxFeedItems = Utilities.PromptInt(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_MAXITEMS"],
|
||||||
|
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_MAXITEMS_DESC"],
|
||||||
|
0, int.MaxValue, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name() => "AutomessageFeedConfiguration";
|
||||||
|
}
|
||||||
|
}
|
85
Plugins/AutomessageFeed/Plugin.cs
Normal file
85
Plugins/AutomessageFeed/Plugin.cs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.SyndicationFeed.Rss;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using System.Xml;
|
||||||
|
using Microsoft.SyndicationFeed;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using SharedLibraryCore.Helpers;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace AutomessageFeed
|
||||||
|
{
|
||||||
|
public class Plugin : IPlugin
|
||||||
|
{
|
||||||
|
public string Name => "Automessage Feed";
|
||||||
|
|
||||||
|
public float Version => (float)Utilities.GetVersionAsDouble();
|
||||||
|
|
||||||
|
public string Author => "RaidMax";
|
||||||
|
|
||||||
|
private Configuration _configuration;
|
||||||
|
private int _currentFeedItem;
|
||||||
|
|
||||||
|
private async Task<string> GetNextFeedItem(Server server)
|
||||||
|
{
|
||||||
|
var items = new List<string>();
|
||||||
|
|
||||||
|
using (var reader = XmlReader.Create(_configuration.FeedUrl, new XmlReaderSettings() { Async = true }))
|
||||||
|
{
|
||||||
|
var feedReader = new RssFeedReader(reader);
|
||||||
|
|
||||||
|
while (await feedReader.Read())
|
||||||
|
{
|
||||||
|
switch (feedReader.ElementType)
|
||||||
|
{
|
||||||
|
case SyndicationElementType.Item:
|
||||||
|
var item = await feedReader.ReadItem();
|
||||||
|
items.Add(Regex.Replace(item.Title, @"\<.+\>.*\</.+\>", ""));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentFeedItem < items.Count && (_configuration.MaxFeedItems == 0 || _currentFeedItem < _configuration.MaxFeedItems))
|
||||||
|
{
|
||||||
|
_currentFeedItem++;
|
||||||
|
return items[_currentFeedItem - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentFeedItem = 0;
|
||||||
|
return Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_NO_ITEMS"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnEventAsync(GameEvent E, Server S)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnLoadAsync(IManager manager)
|
||||||
|
{
|
||||||
|
var cfg = new BaseConfigurationHandler<Configuration>("AutomessageFeedPluginSettings");
|
||||||
|
if (cfg.Configuration() == null)
|
||||||
|
{
|
||||||
|
cfg.Set((Configuration)new Configuration().Generate());
|
||||||
|
await cfg.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
_configuration = cfg.Configuration();
|
||||||
|
|
||||||
|
manager.GetMessageTokens().Add(new MessageToken("FEED", GetNextFeedItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnTickAsync(Server S)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnUnloadAsync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Database.Models;
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using IW4ScriptCommands.Commands;
|
using IW4ScriptCommands.Commands;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -17,7 +16,7 @@ namespace WebfrontCore.Controllers.API
|
|||||||
public IActionResult ClientInfo(string networkId)
|
public IActionResult ClientInfo(string networkId)
|
||||||
{
|
{
|
||||||
var clientInfo = Manager.GetActiveClients()
|
var clientInfo = Manager.GetActiveClients()
|
||||||
.FirstOrDefault(c => c.NetworkId == networkId.ConvertLong());
|
.FirstOrDefault(c => c.NetworkId == networkId.ConvertGuidToLong());
|
||||||
|
|
||||||
if (clientInfo != null)
|
if (clientInfo != null)
|
||||||
{
|
{
|
||||||
@ -40,7 +39,7 @@ namespace WebfrontCore.Controllers.API
|
|||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var client = Manager.GetActiveClients()
|
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);
|
var server = Manager.GetServers().First(c => c.EndPoint == serverId);
|
||||||
|
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
<RuntimeFrameworkVersion>2.1.5</RuntimeFrameworkVersion>
|
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||||
<ApplicationIcon />
|
<ApplicationIcon />
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||||
|
<LangVersion>7.1</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
@ -14,12 +15,11 @@
|
|||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
|
||||||
<ProjectReference Include="..\Stats\Stats.csproj" />
|
<Private>false</Private>
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\Stats\Stats.csproj">
|
||||||
|
<Private>false</Private>
|
||||||
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.1.5" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Database.Models;
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Login.Commands
|
namespace IW4MAdmin.Plugins.Login.Commands
|
||||||
@ -21,19 +18,22 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
|||||||
|
|
||||||
public override async Task ExecuteAsync(GameEvent E)
|
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);
|
||||||
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, client.PasswordSalt));
|
|
||||||
|
|
||||||
if (hashedPassword[0] == client.Password)
|
if (!success)
|
||||||
|
{
|
||||||
|
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, E.Origin.PasswordSalt));
|
||||||
|
success = hashedPassword[0] == E.Origin.Password;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success)
|
||||||
{
|
{
|
||||||
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
|
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
|
||||||
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
_ = success ?
|
||||||
{
|
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
|
||||||
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
<RuntimeFrameworkVersion>2.1.5</RuntimeFrameworkVersion>
|
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||||
<ApplicationIcon />
|
<ApplicationIcon />
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
<PackageId>RaidMax.IW4MAdmin.Plugins.Login</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Plugins.Login</PackageId>
|
||||||
@ -11,6 +11,7 @@
|
|||||||
<Company>Forever None</Company>
|
<Company>Forever None</Company>
|
||||||
<Product>Login Plugin for IW4MAdmin</Product>
|
<Product>Login Plugin for IW4MAdmin</Product>
|
||||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||||
|
<LangVersion>7.1</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
@ -18,13 +19,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
|
||||||
|
<Private>false</Private>
|
||||||
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.1.5" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||||
</Target>
|
</Target>
|
||||||
|
@ -42,10 +42,8 @@ namespace IW4MAdmin.Plugins.Login
|
|||||||
E.Origin.Level == EFClient.Permission.Console)
|
E.Origin.Level == EFClient.Permission.Console)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
E.Owner.Manager.GetPrivilegedClients().TryGetValue(E.Origin.ClientId, out EFClient client);
|
|
||||||
|
|
||||||
if (((Command)E.Extra).Name == new SharedLibraryCore.Commands.CSetPassword().Name &&
|
if (((Command)E.Extra).Name == new SharedLibraryCore.Commands.CSetPassword().Name &&
|
||||||
client?.Password == null)
|
E.Origin?.Password == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
if (((Command)E.Extra).Name == new Commands.CLogin().Name)
|
if (((Command)E.Extra).Name == new Commands.CLogin().Name)
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Linq;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
using SharedLibraryCore.Database.Models;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.ProfanityDeterment
|
namespace IW4MAdmin.Plugins.ProfanityDeterment
|
||||||
{
|
{
|
||||||
@ -20,8 +17,6 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
public string Author => "RaidMax";
|
public string Author => "RaidMax";
|
||||||
|
|
||||||
BaseConfigurationHandler<Configuration> Settings;
|
BaseConfigurationHandler<Configuration> Settings;
|
||||||
ConcurrentDictionary<int, Tracking> ProfanityCounts;
|
|
||||||
IManager Manager;
|
|
||||||
|
|
||||||
public Task OnEventAsync(GameEvent E, Server S)
|
public Task OnEventAsync(GameEvent E, Server S)
|
||||||
{
|
{
|
||||||
@ -30,10 +25,7 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Connect)
|
if (E.Type == GameEvent.EventType.Connect)
|
||||||
{
|
{
|
||||||
if (!ProfanityCounts.TryAdd(E.Origin.ClientId, new Tracking(E.Origin)))
|
E.Origin.SetAdditionalProperty("_profanityInfringements", 0);
|
||||||
{
|
|
||||||
S.Logger.WriteWarning("Could not add client to profanity tracking");
|
|
||||||
}
|
|
||||||
|
|
||||||
var objectionalWords = Settings.Configuration().OffensiveWords;
|
var objectionalWords = Settings.Configuration().OffensiveWords;
|
||||||
bool containsObjectionalWord = objectionalWords.FirstOrDefault(w => E.Origin.Name.ToLower().Contains(w)) != null;
|
bool containsObjectionalWord = objectionalWords.FirstOrDefault(w => E.Origin.Name.ToLower().Contains(w)) != null;
|
||||||
@ -49,20 +41,13 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
|
|
||||||
if (containsObjectionalWord)
|
if (containsObjectionalWord)
|
||||||
{
|
{
|
||||||
E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, new EFClient()
|
E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, Utilities.IW4MAdminClient(E.Owner));
|
||||||
{
|
|
||||||
ClientId = 1,
|
|
||||||
CurrentServer = E.Owner
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Disconnect)
|
if (E.Type == GameEvent.EventType.Disconnect)
|
||||||
{
|
{
|
||||||
if (!ProfanityCounts.TryRemove(E.Origin.ClientId, out Tracking old))
|
E.Origin.SetAdditionalProperty("_profanityInfringements", 0);
|
||||||
{
|
|
||||||
S.Logger.WriteWarning("Could not remove client from profanity tracking");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Say)
|
if (E.Type == GameEvent.EventType.Say)
|
||||||
@ -83,17 +68,17 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
|
|
||||||
if (containsObjectionalWord)
|
if (containsObjectionalWord)
|
||||||
{
|
{
|
||||||
var clientProfanity = ProfanityCounts[E.Origin.ClientId];
|
int profanityInfringments = E.Origin.GetAdditionalProperty<int>("_profanityInfringements");
|
||||||
if (clientProfanity.Infringements >= Settings.Configuration().KickAfterInfringementCount)
|
|
||||||
|
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++;
|
E.Origin.SetAdditionalProperty("_profanityInfringements", profanityInfringments + 1);
|
||||||
|
E.Origin.Warn(Settings.Configuration().ProfanityWarningMessage, Utilities.IW4MAdminClient(E.Owner));
|
||||||
clientProfanity.Client.Warn(Settings.Configuration().ProfanityWarningMessage, Utilities.IW4MAdminClient(E.Owner));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,9 +94,6 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
Settings.Set((Configuration)new Configuration().Generate());
|
Settings.Set((Configuration)new Configuration().Generate());
|
||||||
await Settings.Save();
|
await Settings.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfanityCounts = new ConcurrentDictionary<int, Tracking>();
|
|
||||||
Manager = manager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task OnTickAsync(Server S) => Task.CompletedTask;
|
public Task OnTickAsync(Server S) => Task.CompletedTask;
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
<RuntimeFrameworkVersion>2.1.5</RuntimeFrameworkVersion>
|
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||||
<ApplicationIcon />
|
<ApplicationIcon />
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
<PackageId>RaidMax.IW4MAdmin.Plugins.ProfanityDeterment</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Plugins.ProfanityDeterment</PackageId>
|
||||||
@ -13,14 +13,13 @@
|
|||||||
<Description>Warns and kicks players for using profanity</Description>
|
<Description>Warns and kicks players for using profanity</Description>
|
||||||
<Copyright>2018</Copyright>
|
<Copyright>2018</Copyright>
|
||||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||||
|
<LangVersion>7.1</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
|
||||||
</ItemGroup>
|
<Private>false</Private>
|
||||||
|
</ProjectReference>
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.1.5" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
38
Plugins/ScriptPlugins/ParserCoD4x.js
Normal file
38
Plugins/ScriptPlugins/ParserCoD4x.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
var rconParser;
|
||||||
|
var eventParser;
|
||||||
|
|
||||||
|
var plugin = {
|
||||||
|
author: 'FrenchFry, RaidMax',
|
||||||
|
version: 0.5,
|
||||||
|
name: 'CoD4x Parser',
|
||||||
|
isParser: true,
|
||||||
|
|
||||||
|
onEventAsync: function (gameEvent, server) {
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoadAsync: function (manager) {
|
||||||
|
rconParser = manager.GenerateDynamicRConParser();
|
||||||
|
eventParser = manager.GenerateDynamicEventParser();
|
||||||
|
|
||||||
|
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 - win_mingw-x86 build 963 Mar 12 2019';
|
||||||
|
rconParser.GameName = 1; // IW3
|
||||||
|
|
||||||
|
eventParser.Configuration.GameDirectory = 'main';
|
||||||
|
eventParser.Version = 'CoD4 X - win_mingw-x86 build 963 Mar 12 2019';
|
||||||
|
eventParser.GameName = 1; // IW3
|
||||||
|
eventParser.URLProtocolFormat = 'cod4://{{ip}}:{{port}}';
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnloadAsync: function () {
|
||||||
|
},
|
||||||
|
|
||||||
|
onTickAsync: function (server) {
|
||||||
|
}
|
||||||
|
};
|
36
Plugins/ScriptPlugins/ParserIW4x.js
Normal file
36
Plugins/ScriptPlugins/ParserIW4x.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
var rconParser;
|
||||||
|
var eventParser;
|
||||||
|
|
||||||
|
var plugin = {
|
||||||
|
author: 'RaidMax',
|
||||||
|
version: 0.3,
|
||||||
|
name: 'IW4 Parser',
|
||||||
|
isParser: true,
|
||||||
|
|
||||||
|
onEventAsync: function (gameEvent, server) {
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoadAsync: function (manager) {
|
||||||
|
rconParser = manager.GenerateDynamicRConParser();
|
||||||
|
eventParser = manager.GenerateDynamicEventParser();
|
||||||
|
|
||||||
|
rconParser.Configuration.CommandPrefixes.Tell = 'tellraw {0} {1}';
|
||||||
|
rconParser.Configuration.CommandPrefixes.Say = 'sayraw {0}';
|
||||||
|
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';
|
||||||
|
|
||||||
|
rconParser.Version = 'IW4x (v0.6.0)';
|
||||||
|
rconParser.GameName = 2; // IW4x
|
||||||
|
eventParser.Version = 'IW4x (v0.6.0)';
|
||||||
|
eventParser.GameName = 2; // IW4x
|
||||||
|
eventParser.URLProtocolFormat = 'iw4x://{{ip}}:{{port}}';
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnloadAsync: function () {
|
||||||
|
},
|
||||||
|
|
||||||
|
onTickAsync: function (server) {
|
||||||
|
}
|
||||||
|
};
|
50
Plugins/ScriptPlugins/ParserPT6.js
Normal file
50
Plugins/ScriptPlugins/ParserPT6.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
var rconParser;
|
||||||
|
var eventParser;
|
||||||
|
|
||||||
|
var plugin = {
|
||||||
|
author: 'RaidMax, Xerxes',
|
||||||
|
version: 0.4,
|
||||||
|
name: 'Plutonium T6 Parser',
|
||||||
|
isParser: true,
|
||||||
|
|
||||||
|
onEventAsync: function (gameEvent, server) {
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoadAsync: function (manager) {
|
||||||
|
rconParser = manager.GenerateDynamicRConParser();
|
||||||
|
eventParser = manager.GenerateDynamicEventParser();
|
||||||
|
|
||||||
|
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
|
||||||
|
rconParser.Configuration.CommandPrefixes.Say = 'say {0}';
|
||||||
|
rconParser.Configuration.CommandPrefixes.Kick = 'clientkick_for_reason {0} "{1}"';
|
||||||
|
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick_for_reason {0} "{1}"';
|
||||||
|
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick_for_reason {0} "{1}"';
|
||||||
|
rconParser.Configuration.CommandPrefixes.RConGetDvar = '\xff\xff\xff\xffrcon {0} get {1}';
|
||||||
|
|
||||||
|
rconParser.Configuration.Dvar.Pattern = '^(.+) is "(.+)?"$';
|
||||||
|
rconParser.Configuration.Dvar.AddMapping(106, 1);
|
||||||
|
rconParser.Configuration.Dvar.AddMapping(107, 2);
|
||||||
|
rconParser.Configuration.WaitForResponse = false;
|
||||||
|
|
||||||
|
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, 3);
|
||||||
|
rconParser.Configuration.Status.AddMapping(103, 4);
|
||||||
|
rconParser.Configuration.Status.AddMapping(104, 5);
|
||||||
|
rconParser.Configuration.Status.AddMapping(105, 6);
|
||||||
|
|
||||||
|
eventParser.Configuration.GameDirectory = 't6r\\data';
|
||||||
|
|
||||||
|
rconParser.Version = 'Call of Duty Multiplayer - Ship COD_T6_S MP build 1.0.44 CL(1759941) CODPCAB2 CEG Fri May 9 19:19:19 2014 win-x86 813e66d5';
|
||||||
|
rconParser.GameName = 7; // T6
|
||||||
|
eventParser.Version = 'Call of Duty Multiplayer - Ship COD_T6_S MP build 1.0.44 CL(1759941) CODPCAB2 CEG Fri May 9 19:19:19 2014 win-x86 813e66d5';
|
||||||
|
eventParser.GameName = 7; // T6
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnloadAsync: function () {
|
||||||
|
},
|
||||||
|
|
||||||
|
onTickAsync: function (server) {
|
||||||
|
}
|
||||||
|
};
|
34
Plugins/ScriptPlugins/ParserRektT5M.js
Normal file
34
Plugins/ScriptPlugins/ParserRektT5M.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
var rconParser;
|
||||||
|
var eventParser;
|
||||||
|
|
||||||
|
var plugin = {
|
||||||
|
author: 'RaidMax',
|
||||||
|
version: 0.1,
|
||||||
|
name: 'RektT5m Parser',
|
||||||
|
isParser: true,
|
||||||
|
|
||||||
|
onEventAsync: function (gameEvent, server) {
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoadAsync: function (manager) {
|
||||||
|
rconParser = manager.GenerateDynamicRConParser();
|
||||||
|
eventParser = manager.GenerateDynamicEventParser();
|
||||||
|
|
||||||
|
eventParser.Configuration.GameDirectory = 'data';
|
||||||
|
|
||||||
|
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff\1print';
|
||||||
|
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
|
||||||
|
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
|
||||||
|
|
||||||
|
rconParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
|
||||||
|
rconParser.GameName = 6; // T5
|
||||||
|
eventParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
|
||||||
|
eventParser.GameName = 6; // T5
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnloadAsync: function () {
|
||||||
|
},
|
||||||
|
|
||||||
|
onTickAsync: function (server) {
|
||||||
|
}
|
||||||
|
};
|
43
Plugins/ScriptPlugins/ParserTeknoMW3.js
Normal file
43
Plugins/ScriptPlugins/ParserTeknoMW3.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
var rconParser;
|
||||||
|
var eventParser;
|
||||||
|
|
||||||
|
var plugin = {
|
||||||
|
author: 'RaidMax',
|
||||||
|
version: 0.2,
|
||||||
|
name: 'Tekno MW3 Parser',
|
||||||
|
isParser: true,
|
||||||
|
|
||||||
|
onEventAsync: function (gameEvent, server) {
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoadAsync: function (manager) {
|
||||||
|
rconParser = manager.GenerateDynamicRConParser();
|
||||||
|
eventParser = manager.GenerateDynamicEventParser();
|
||||||
|
|
||||||
|
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[A-Z]|[0-9]){16,32})\t +(.{0,16}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+\\:-?\\d{1,5}|loopback) *$';
|
||||||
|
rconParser.Configuration.Status.AddMapping(104, 5); // RConName
|
||||||
|
rconParser.Configuration.Status.AddMapping(103, 4); // RConNetworkId
|
||||||
|
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
|
||||||
|
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff';
|
||||||
|
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
|
||||||
|
rconParser.Configuration.CommandPrefixes.Say = 'say {0}';
|
||||||
|
rconParser.Configuration.CommandPrefixes.Kick = 'dropclient {0} "{1}"';
|
||||||
|
rconParser.Configuration.CommandPrefixes.Ban = 'dropclient {0} "{1}"';
|
||||||
|
rconParser.Configuration.CommandPrefixes.TempBan = 'tempbanclient {0} "{1}"';
|
||||||
|
rconParser.Configuration.Dvar.AddMapping(107, 1); // RCon DvarValue
|
||||||
|
rconParser.Configuration.Dvar.Pattern = '^(.*)$';
|
||||||
|
rconParser.Version = 'IW5 MP 1.4 build 382 latest Thu Jan 19 2012 11:09:49AM win-x86';
|
||||||
|
rconParser.GameName = 3; // IW5
|
||||||
|
rconParser.CanGenerateLogPath = false;
|
||||||
|
|
||||||
|
eventParser.Configuration.GameDirectory = 'scripts';
|
||||||
|
eventParser.Version = 'IW5 MP 1.4 build 382 latest Thu Jan 19 2012 11:09:49AM win-x86';
|
||||||
|
eventParser.GameName = 3; // IW5
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnloadAsync: function () {
|
||||||
|
},
|
||||||
|
|
||||||
|
onTickAsync: function (server) {
|
||||||
|
}
|
||||||
|
};
|
@ -11,13 +11,22 @@ var plugin = {
|
|||||||
|
|
||||||
// connect or join event
|
// connect or join event
|
||||||
if (gameEvent.Type === 3) {
|
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
|
// 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) {
|
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);
|
gameEvent.Origin.Kick('Your GUID is generic. Delete players/guids.dat and rejoin', _IW4MAdminClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoadAsync: function (manager) {
|
onLoadAsync: function (manager) {
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
var plugin = {
|
var plugin = {
|
||||||
author: 'RaidMax',
|
author: 'RaidMax',
|
||||||
version: 1.0,
|
version: 1.2,
|
||||||
name: 'VPN Detection Plugin',
|
name: 'VPN Detection Plugin',
|
||||||
|
|
||||||
manager: null,
|
manager: null,
|
||||||
@ -33,7 +33,7 @@ var plugin = {
|
|||||||
cl.Dispose();
|
cl.Dispose();
|
||||||
usingVPN = parsedJSON.success && parsedJSON.proxy;
|
usingVPN = parsedJSON.success && parsedJSON.proxy;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.WriteError(e.message);
|
this.logger.WriteWarning('There was a problem checking client IP for VPN ' + e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usingVPN) {
|
if (usingVPN) {
|
||||||
@ -43,8 +43,8 @@ var plugin = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onEventAsync: function (gameEvent, server) {
|
onEventAsync: function (gameEvent, server) {
|
||||||
// connect event
|
// join event
|
||||||
if (gameEvent.Type === 3) {
|
if (gameEvent.Type === 4) {
|
||||||
this.checkForVpn(gameEvent.Origin);
|
this.checkForVpn(gameEvent.Origin);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
using IW4MAdmin.Plugins.Stats.Models;
|
using IW4MAdmin.Plugins.Stats.Models;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Helpers;
|
using SharedLibraryCore.Helpers;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||||
{
|
{
|
||||||
@ -16,16 +18,18 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
Bone,
|
Bone,
|
||||||
Chest,
|
Chest,
|
||||||
Offset,
|
Offset,
|
||||||
Strain
|
Strain,
|
||||||
|
Recoil
|
||||||
};
|
};
|
||||||
|
|
||||||
public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
|
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 Kills;
|
||||||
int HitCount;
|
int HitCount;
|
||||||
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
|
Dictionary<IW4Info.HitLocation, HitInfo> HitLocationCount;
|
||||||
double AngleDifferenceAverage;
|
double AngleDifferenceAverage;
|
||||||
EFClientStatistics ClientStats;
|
EFClientStatistics ClientStats;
|
||||||
long LastOffset;
|
long LastOffset;
|
||||||
@ -33,20 +37,29 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
ILogger Log;
|
ILogger Log;
|
||||||
Strain Strain;
|
Strain Strain;
|
||||||
readonly DateTime ConnectionTime = DateTime.UtcNow;
|
readonly DateTime ConnectionTime = DateTime.UtcNow;
|
||||||
|
private double sessionAverageRecoilAmount;
|
||||||
|
private EFClientKill lastHit;
|
||||||
|
private int validRecoilHitCount;
|
||||||
|
|
||||||
|
private class HitInfo
|
||||||
|
{
|
||||||
|
public int Count { get; set; }
|
||||||
|
public double Offset { get; set; }
|
||||||
|
};
|
||||||
|
|
||||||
public Detection(ILogger log, EFClientStatistics clientStats)
|
public Detection(ILogger log, EFClientStatistics clientStats)
|
||||||
{
|
{
|
||||||
Log = log;
|
Log = log;
|
||||||
HitLocationCount = new Dictionary<IW4Info.HitLocation, int>();
|
HitLocationCount = new Dictionary<IW4Info.HitLocation, HitInfo>();
|
||||||
foreach (var loc in Enum.GetValues(typeof(IW4Info.HitLocation)))
|
foreach (var loc in Enum.GetValues(typeof(IW4Info.HitLocation)))
|
||||||
{
|
{
|
||||||
HitLocationCount.Add((IW4Info.HitLocation)loc, 0);
|
HitLocationCount.Add((IW4Info.HitLocation)loc, new HitInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientStats = clientStats;
|
ClientStats = clientStats;
|
||||||
Strain = new Strain();
|
Strain = new Strain();
|
||||||
Tracker = new ChangeTracking<EFACSnapshot>();
|
Tracker = new ChangeTracking<EFACSnapshot>();
|
||||||
QueuedHits = new List<EFClientKill>();
|
TrackedHits = new List<EFClientKill>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -56,6 +69,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
/// <returns>true if detection reached thresholds, false otherwise</returns>
|
/// <returns>true if detection reached thresholds, false otherwise</returns>
|
||||||
public DetectionPenaltyResult ProcessHit(EFClientKill hit, bool isDamage)
|
public DetectionPenaltyResult ProcessHit(EFClientKill hit, bool isDamage)
|
||||||
{
|
{
|
||||||
|
var results = new List<DetectionPenaltyResult>();
|
||||||
|
|
||||||
if ((hit.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
|
if ((hit.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
|
||||||
hit.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
|
hit.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
|
||||||
hit.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
|
hit.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
|
||||||
@ -65,16 +80,16 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
{
|
{
|
||||||
return new DetectionPenaltyResult()
|
return new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Any,
|
ClientPenalty = EFPenalty.PenaltyType.Any,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
DetectionPenaltyResult result = null;
|
|
||||||
LastWeapon = hit.Weapon;
|
LastWeapon = hit.Weapon;
|
||||||
|
|
||||||
HitLocationCount[hit.HitLoc]++;
|
HitLocationCount[hit.HitLoc].Count++;
|
||||||
HitCount++;
|
HitCount++;
|
||||||
|
|
||||||
|
|
||||||
if (!isDamage)
|
if (!isDamage)
|
||||||
{
|
{
|
||||||
Kills++;
|
Kills++;
|
||||||
@ -93,52 +108,64 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + realAgainstPredict) / hitLoc.HitCount;
|
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + realAgainstPredict) / hitLoc.HitCount;
|
||||||
hitLoc.HitOffsetAverage = (float)newAverage;
|
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)
|
hitLoc.HitCount > 100)
|
||||||
{
|
{
|
||||||
//Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***");
|
Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***");
|
||||||
//Log.WriteDebug($"Lifetime Average = {newAverage}");
|
Log.WriteDebug($"Lifetime Average = {newAverage}");
|
||||||
//Log.WriteDebug($"Bone = {hitLoc.Location}");
|
Log.WriteDebug($"Bone = {hitLoc.Location}");
|
||||||
//Log.WriteDebug($"HitCount = {hitLoc.HitCount}");
|
Log.WriteDebug($"HitCount = {hitLoc.HitCount}");
|
||||||
//Log.WriteDebug($"ID = {hit.AttackerId}");
|
Log.WriteDebug($"ID = {hit.AttackerId}");
|
||||||
|
|
||||||
result = new DetectionPenaltyResult()
|
results.Add(new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
ClientPenalty = EFPenalty.PenaltyType.Ban,
|
||||||
Value = hitLoc.HitOffsetAverage,
|
Value = hitLoc.HitOffsetAverage,
|
||||||
HitCount = hitLoc.HitCount,
|
HitCount = hitLoc.HitCount,
|
||||||
Type = DetectionType.Offset
|
Type = DetectionType.Offset
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// SESSION
|
// SESSION
|
||||||
double sessAverage = (AngleDifferenceAverage * (HitCount - 1) + realAgainstPredict) / HitCount;
|
var sessionHitLoc = HitLocationCount[hit.HitLoc];
|
||||||
AngleDifferenceAverage = sessAverage;
|
sessionHitLoc.Offset = (sessionHitLoc.Offset * (sessionHitLoc.Count - 1) + realAgainstPredict) / sessionHitLoc.Count;
|
||||||
|
|
||||||
if (sessAverage > Thresholds.MaxOffset(HitCount) &&
|
int totalSessionHits = HitLocationCount.Sum(_hit => _hit.Value.Count);
|
||||||
HitCount > 30)
|
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("*** Reached Max Session Average for Angle Difference ***");
|
||||||
//Log.WriteDebug($"Session Average = {sessAverage}");
|
Log.WriteDebug($"Session Average = {weightedSessionAverage}");
|
||||||
//Log.WriteDebug($"HitCount = {HitCount}");
|
Log.WriteDebug($"HitCount = {HitCount}");
|
||||||
//Log.WriteDebug($"ID = {hit.AttackerId}");
|
Log.WriteDebug($"ID = {hit.AttackerId}");
|
||||||
|
|
||||||
result = new DetectionPenaltyResult()
|
results.Add(new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
ClientPenalty = EFPenalty.PenaltyType.Ban,
|
||||||
Value = sessAverage,
|
Value = weightedSessionAverage,
|
||||||
HitCount = HitCount,
|
HitCount = HitCount,
|
||||||
Type = DetectionType.Offset,
|
Type = DetectionType.Offset,
|
||||||
Location = hitLoc.Location
|
Location = hitLoc.Location
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Log.WriteDebug($"PredictVsReal={realAgainstPredict}");
|
Log.WriteDebug($"PredictVsReal={realAgainstPredict}");
|
||||||
#endif
|
#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
|
#if DEBUG == true
|
||||||
Log.WriteDebug($"Current Strain: {currentStrain}");
|
Log.WriteDebug($"Current Strain: {currentStrain}");
|
||||||
#endif
|
#endif
|
||||||
@ -152,26 +179,47 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
// flag
|
// flag
|
||||||
if (currentStrain > Thresholds.MaxStrainFlag)
|
if (currentStrain > Thresholds.MaxStrainFlag)
|
||||||
{
|
{
|
||||||
result = new DetectionPenaltyResult()
|
results.Add(new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
ClientPenalty = EFPenalty.PenaltyType.Flag,
|
||||||
Value = currentStrain,
|
Value = currentStrain,
|
||||||
HitCount = HitCount,
|
HitCount = HitCount,
|
||||||
Type = DetectionType.Strain
|
Type = DetectionType.Strain
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ban
|
// ban
|
||||||
if (currentStrain > Thresholds.MaxStrainBan &&
|
if (currentStrain > Thresholds.MaxStrainBan &&
|
||||||
HitCount >= 5)
|
HitCount >= 5)
|
||||||
{
|
{
|
||||||
result = new DetectionPenaltyResult()
|
results.Add(new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
ClientPenalty = EFPenalty.PenaltyType.Ban,
|
||||||
Value = currentStrain,
|
Value = currentStrain,
|
||||||
HitCount = HitCount,
|
HitCount = HitCount,
|
||||||
Type = DetectionType.Strain
|
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
|
#endregion
|
||||||
|
|
||||||
@ -188,11 +236,11 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
double maxBoneRatioLerpValueForBan = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(3.25), Thresholds.BoneRatioThresholdHighSample(3.25), lerpAmount) + marginOfError;
|
double maxBoneRatioLerpValueForBan = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(3.25), Thresholds.BoneRatioThresholdHighSample(3.25), lerpAmount) + marginOfError;
|
||||||
|
|
||||||
// calculate headshot ratio
|
// 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
|
// calculate maximum bone
|
||||||
double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v / (double)HitCount).Max());
|
double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v.Count / (double)HitCount).Max());
|
||||||
var bone = HitLocationCount.FirstOrDefault(b => b.Value == HitLocationCount.Values.Max()).Key;
|
var bone = HitLocationCount.FirstOrDefault(b => b.Value.Count == HitLocationCount.Values.Max(_hit => _hit.Count)).Key;
|
||||||
|
|
||||||
#region HEADSHOT_RATIO
|
#region HEADSHOT_RATIO
|
||||||
// flag on headshot
|
// flag on headshot
|
||||||
@ -201,51 +249,25 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
// ban on headshot
|
// ban on headshot
|
||||||
if (currentHeadshotRatio > maxHeadshotLerpValueForBan)
|
if (currentHeadshotRatio > maxHeadshotLerpValueForBan)
|
||||||
{
|
{
|
||||||
Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**");
|
results.Add(new DetectionPenaltyResult()
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
ClientPenalty = EFPenalty.PenaltyType.Ban,
|
||||||
}
|
|
||||||
|
|
||||||
Log.WriteDebug(sb.ToString());
|
|
||||||
|
|
||||||
result = new DetectionPenaltyResult()
|
|
||||||
{
|
|
||||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
|
||||||
Value = currentHeadshotRatio,
|
Value = currentHeadshotRatio,
|
||||||
Location = IW4Info.HitLocation.head,
|
Location = IW4Info.HitLocation.head,
|
||||||
HitCount = HitCount,
|
HitCount = HitCount,
|
||||||
Type = DetectionType.Bone
|
Type = DetectionType.Bone
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.WriteDebug("**Maximum Headshot Ratio Reached For Flag**");
|
results.Add(new DetectionPenaltyResult()
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
ClientPenalty = EFPenalty.PenaltyType.Flag,
|
||||||
}
|
|
||||||
|
|
||||||
Log.WriteDebug(sb.ToString());
|
|
||||||
|
|
||||||
result = new DetectionPenaltyResult()
|
|
||||||
{
|
|
||||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
|
||||||
Value = currentHeadshotRatio,
|
Value = currentHeadshotRatio,
|
||||||
Location = IW4Info.HitLocation.head,
|
Location = IW4Info.HitLocation.head,
|
||||||
HitCount = HitCount,
|
HitCount = HitCount,
|
||||||
Type = DetectionType.Bone
|
Type = DetectionType.Bone
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@ -257,58 +279,32 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
// ban on bone ratio
|
// ban on bone ratio
|
||||||
if (currentMaxBoneRatio > maxBoneRatioLerpValueForBan)
|
if (currentMaxBoneRatio > maxBoneRatioLerpValueForBan)
|
||||||
{
|
{
|
||||||
Log.WriteDebug("**Maximum Bone Ratio Reached For Ban**");
|
results.Add(new DetectionPenaltyResult()
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
ClientPenalty = EFPenalty.PenaltyType.Ban,
|
||||||
}
|
|
||||||
|
|
||||||
Log.WriteDebug(sb.ToString());
|
|
||||||
|
|
||||||
result = new DetectionPenaltyResult()
|
|
||||||
{
|
|
||||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
|
||||||
Value = currentMaxBoneRatio,
|
Value = currentMaxBoneRatio,
|
||||||
Location = bone,
|
Location = bone,
|
||||||
HitCount = HitCount,
|
HitCount = HitCount,
|
||||||
Type = DetectionType.Bone
|
Type = DetectionType.Bone
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.WriteDebug("**Maximum Bone Ratio Reached For Flag**");
|
results.Add(new DetectionPenaltyResult()
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
ClientPenalty = EFPenalty.PenaltyType.Flag,
|
||||||
}
|
|
||||||
|
|
||||||
Log.WriteDebug(sb.ToString());
|
|
||||||
|
|
||||||
result = new DetectionPenaltyResult()
|
|
||||||
{
|
|
||||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
|
||||||
Value = currentMaxBoneRatio,
|
Value = currentMaxBoneRatio,
|
||||||
Location = bone,
|
Location = bone,
|
||||||
HitCount = HitCount,
|
HitCount = HitCount,
|
||||||
Type = DetectionType.Bone
|
Type = DetectionType.Bone
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
#region CHEST_ABDOMEN_RATIO_SESSION
|
#region CHEST_ABDOMEN_RATIO_SESSION
|
||||||
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper];
|
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count;
|
||||||
|
|
||||||
if (chestHits >= Thresholds.MediumSampleMinKills)
|
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 chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(3), Thresholds.ChestAbdomenRatioThresholdHighSample(3), lerpAmount) + marginOfError;
|
||||||
double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(4), Thresholds.ChestAbdomenRatioThresholdHighSample(4), 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 > chestAbdomenRatioLerpValueForFlag)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan && chestHits >= Thresholds.MediumSampleMinKills + 30)
|
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan && chestHits >= Thresholds.MediumSampleMinKills * 2)
|
||||||
{
|
{
|
||||||
//Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Ban**");
|
results.Add(new DetectionPenaltyResult()
|
||||||
//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()
|
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
ClientPenalty = EFPenalty.PenaltyType.Ban,
|
||||||
Value = currentChestAbdomenRatio,
|
Value = currentChestAbdomenRatio,
|
||||||
Location = IW4Info.HitLocation.torso_upper,
|
Location = IW4Info.HitLocation.torso_upper,
|
||||||
Type = DetectionType.Chest,
|
Type = DetectionType.Chest,
|
||||||
HitCount = chestHits
|
HitCount = chestHits
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Flag**");
|
results.Add(new DetectionPenaltyResult()
|
||||||
//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()
|
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
ClientPenalty = EFPenalty.PenaltyType.Flag,
|
||||||
Value = currentChestAbdomenRatio,
|
Value = currentChestAbdomenRatio,
|
||||||
Location = IW4Info.HitLocation.torso_upper,
|
Location = IW4Info.HitLocation.torso_upper,
|
||||||
Type = DetectionType.Chest,
|
Type = DetectionType.Chest,
|
||||||
HitCount = chestHits
|
HitCount = chestHits
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
Tracker.OnChange(new EFACSnapshot()
|
var snapshot = new EFACSnapshot()
|
||||||
{
|
{
|
||||||
When = hit.When,
|
When = hit.When,
|
||||||
ClientId = ClientStats.ClientId,
|
ClientId = ClientStats.ClientId,
|
||||||
SessionAngleOffset = AngleDifferenceAverage,
|
SessionAngleOffset = AngleDifferenceAverage,
|
||||||
|
RecoilOffset = hitRecoilAverage,
|
||||||
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds,
|
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds,
|
||||||
CurrentStrain = currentStrain,
|
CurrentStrain = currentStrain,
|
||||||
CurrentViewAngle = hit.ViewAngles,
|
CurrentViewAngle = hit.ViewAngles,
|
||||||
@ -397,81 +374,16 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
StrainAngleBetween = Strain.LastDistance,
|
StrainAngleBetween = Strain.LastDistance,
|
||||||
TimeSinceLastEvent = (int)Strain.LastDeltaTime,
|
TimeSinceLastEvent = (int)Strain.LastDeltaTime,
|
||||||
WeaponId = hit.Weapon
|
WeaponId = hit.Weapon
|
||||||
});
|
|
||||||
|
|
||||||
return result ?? new DetectionPenaltyResult()
|
|
||||||
{
|
|
||||||
ClientPenalty = Penalty.PenaltyType.Any,
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
public DetectionPenaltyResult ProcessTotalRatio(EFClientStatistics stats)
|
Tracker.OnChange(snapshot);
|
||||||
{
|
|
||||||
int totalChestHits = stats.HitLocations.Single(c => c.Location == IW4Info.HitLocation.torso_upper).HitCount;
|
|
||||||
|
|
||||||
if (totalChestHits >= 60)
|
return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ??
|
||||||
{
|
results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
|
||||||
double marginOfError = Thresholds.GetMarginOfError(totalChestHits);
|
new DetectionPenaltyResult()
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
|
ClientPenalty = EFPenalty.PenaltyType.Any,
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Database.Models;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||||
{
|
{
|
||||||
class DetectionPenaltyResult
|
class DetectionPenaltyResult
|
||||||
{
|
{
|
||||||
public Detection.DetectionType Type { get; set; }
|
public Detection.DetectionType Type { get; set; }
|
||||||
public Penalty.PenaltyType ClientPenalty { get; set; }
|
public EFPenalty.PenaltyType ClientPenalty { get; set; }
|
||||||
public double Value { get; set; }
|
public double Value { get; set; }
|
||||||
public IW4Info.HitLocation Location { get; set; }
|
public IW4Info.HitLocation Location { get; set; }
|
||||||
public int HitCount { 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 Vector3 LastAngle { get; private set; }
|
||||||
public double LastDeltaTime { 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)
|
if (LastAngle == null)
|
||||||
LastAngle = newAngle;
|
LastAngle = newAngle;
|
||||||
|
@ -26,10 +26,14 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
public const int MediumSampleMinKills = 30;
|
public const int MediumSampleMinKills = 30;
|
||||||
public const int HighSampleMinKills = 100;
|
public const int HighSampleMinKills = 100;
|
||||||
public const double KillTimeThreshold = 0.2;
|
public const double KillTimeThreshold = 0.2;
|
||||||
|
public const int LowSampleMinKillsRecoil = 5;
|
||||||
|
|
||||||
public const double MaxStrainBan = 0.9;
|
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 const double MaxStrainFlag = 0.36;
|
||||||
|
|
||||||
public static double GetMarginOfError(int numKills) => 1.6455 / Math.Sqrt(numKills);
|
public static double GetMarginOfError(int numKills) => 1.6455 / Math.Sqrt(numKills);
|
||||||
|
@ -4,7 +4,6 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using IW4MAdmin.Plugins.Stats.Models;
|
using IW4MAdmin.Plugins.Stats.Models;
|
||||||
using SharedLibraryCore.Database;
|
using SharedLibraryCore.Database;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -17,14 +16,14 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
{
|
{
|
||||||
public static async Task<List<string>> GetMostPlayed(Server s)
|
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>()
|
List<string> mostPlayed = new List<string>()
|
||||||
{
|
{
|
||||||
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--"
|
$"^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.AutoDetectChangesEnabled = false;
|
||||||
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||||
@ -39,7 +38,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
where stats.ServerId == serverId
|
where stats.ServerId == serverId
|
||||||
where client.Level != EFClient.Permission.Banned
|
where client.Level != EFClient.Permission.Banned
|
||||||
where client.LastConnection >= thirtyDaysAgo
|
where client.LastConnection >= thirtyDaysAgo
|
||||||
orderby stats.Kills descending
|
orderby stats.TimePlayed descending
|
||||||
select new
|
select new
|
||||||
{
|
{
|
||||||
alias.Name,
|
alias.Name,
|
||||||
|
@ -17,7 +17,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
if (E.Origin.ClientNumber >= 0)
|
if (E.Origin.ClientNumber >= 0)
|
||||||
{
|
{
|
||||||
|
|
||||||
long serverId = await Helpers.StatManager.GetIdForServer(E.Owner);
|
long serverId = Helpers.StatManager.GetIdForServer(E.Owner);
|
||||||
|
|
||||||
EFClientStatistics clientStats;
|
EFClientStatistics clientStats;
|
||||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||||
|
@ -4,7 +4,6 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using SharedLibraryCore.Services;
|
using SharedLibraryCore.Services;
|
||||||
using IW4MAdmin.Plugins.Stats.Models;
|
using IW4MAdmin.Plugins.Stats.Models;
|
||||||
using SharedLibraryCore.Database;
|
using SharedLibraryCore.Database;
|
||||||
@ -18,7 +17,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
{
|
{
|
||||||
public static async Task<List<string>> GetTopStats(Server s)
|
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>()
|
List<string> topStatsText = new List<string>()
|
||||||
{
|
{
|
||||||
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
|
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using SharedLibraryCore.Services;
|
using SharedLibraryCore.Services;
|
||||||
using IW4MAdmin.Plugins.Stats.Models;
|
using IW4MAdmin.Plugins.Stats.Models;
|
||||||
using System;
|
using System;
|
||||||
@ -43,33 +42,53 @@ 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));
|
if (E.Owner.GetClientsAsList().Any(_client => _client.Equals(E.Target)))
|
||||||
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}";
|
{
|
||||||
|
pStats = Plugin.Manager.GetClientStats(E.Target.ClientId, serverId);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
int performanceRanking = await StatManager.GetClientOverallRanking(E.Origin.ClientId);
|
using (var ctx = new DatabaseContext(true))
|
||||||
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));
|
||||||
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}";
|
|
||||||
}
|
}
|
||||||
|
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())
|
if (E.Message.IsBroadcastCommand())
|
||||||
{
|
{
|
||||||
string name = E.Target == null ? E.Origin.Name : E.Target.Name;
|
string name = E.Target == null ? E.Origin.Name : E.Target.Name;
|
||||||
E.Owner.Broadcast($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{name}^7");
|
E.Owner.Broadcast(loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name));
|
||||||
E.Owner.Broadcast(statLine);
|
E.Owner.Broadcast(statLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +96,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
{
|
{
|
||||||
if (E.Target != null)
|
if (E.Target != null)
|
||||||
{
|
{
|
||||||
E.Origin.Tell($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{E.Target.Name}^7");
|
E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(E.Target.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
E.Origin.Tell(statLine);
|
E.Origin.Tell(statLine);
|
||||||
|
@ -9,6 +9,7 @@ namespace IW4MAdmin.Plugins.Stats.Config
|
|||||||
public bool EnableAntiCheat { get; set; }
|
public bool EnableAntiCheat { get; set; }
|
||||||
public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
|
public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
|
||||||
public List<StreakMessageConfiguration> DeathstreakMessages { get; set; }
|
public List<StreakMessageConfiguration> DeathstreakMessages { get; set; }
|
||||||
|
public List<string> RecoilessWeapons { get; set; }
|
||||||
public int TopPlayersMinPlayTime { get; set; }
|
public int TopPlayersMinPlayTime { get; set; }
|
||||||
public bool StoreClientKills { get; set; }
|
public bool StoreClientKills { get; set; }
|
||||||
public string Name() => "Stats";
|
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;
|
TopPlayersMinPlayTime = 3600 * 3;
|
||||||
StoreClientKills = false;
|
StoreClientKills = false;
|
||||||
|
|
||||||
|
138
Plugins/Stats/Controllers/StatsController.cs
Normal file
138
Plugins/Stats/Controllers/StatsController.cs
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Dtos;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using WebfrontCore.Controllers;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Plugins.Stats.Web.Controllers
|
||||||
|
{
|
||||||
|
public class StatsController : BaseController
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult TopPlayersAsync()
|
||||||
|
{
|
||||||
|
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"];
|
||||||
|
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"];
|
||||||
|
ViewBag.Servers = Manager.GetServers().Select(_server => new ServerInfo() { Name = _server.Hostname, ID = _server.EndPoint });
|
||||||
|
|
||||||
|
return View("Index");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetTopPlayersAsync(int count, int offset, long? serverId = null)
|
||||||
|
{
|
||||||
|
// this prevents empty results when we really want aggregate
|
||||||
|
if (serverId == 0)
|
||||||
|
{
|
||||||
|
serverId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var server = Manager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId);
|
||||||
|
|
||||||
|
if (server != null)
|
||||||
|
{
|
||||||
|
serverId = StatManager.GetIdForServer(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = await Plugin.Manager.GetTopStats(offset, count, serverId);
|
||||||
|
|
||||||
|
// this returns an empty result so we know to stale the loader
|
||||||
|
if (results.Count == 0 && offset > 0)
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return View("Components/TopPlayers/_List", results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetMessageAsync(int serverId, long when)
|
||||||
|
{
|
||||||
|
var whenTime = DateTime.FromFileTimeUtc(when);
|
||||||
|
var whenUpper = whenTime.AddMinutes(5);
|
||||||
|
var whenLower = whenTime.AddMinutes(-5);
|
||||||
|
|
||||||
|
using (var ctx = new SharedLibraryCore.Database.DatabaseContext(true))
|
||||||
|
{
|
||||||
|
var iqMessages = from message in ctx.Set<Models.EFClientMessage>()
|
||||||
|
where message.ServerId == serverId
|
||||||
|
where message.TimeSent >= whenLower
|
||||||
|
where message.TimeSent <= whenUpper
|
||||||
|
select new ChatInfo()
|
||||||
|
{
|
||||||
|
ClientId = message.ClientId,
|
||||||
|
Message = message.Message,
|
||||||
|
Name = message.Client.CurrentAlias.Name,
|
||||||
|
Time = message.TimeSent,
|
||||||
|
ServerGame = message.Server.GameName ?? Server.Game.IW4
|
||||||
|
};
|
||||||
|
|
||||||
|
#if DEBUG == true
|
||||||
|
var messagesSql = iqMessages.ToSql();
|
||||||
|
#endif
|
||||||
|
var messages = await iqMessages.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
if (message.Message.IsQuickMessage())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var quickMessages = Manager.GetApplicationSettings().Configuration()
|
||||||
|
.QuickMessages
|
||||||
|
.First(_qm => _qm.Game == message.ServerGame);
|
||||||
|
message.Message = quickMessages.Messages[message.Message.Substring(1)];
|
||||||
|
message.IsQuickMessage = true;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return View("_MessageContext", messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> GetAutomatedPenaltyInfoAsync(int clientId)
|
||||||
|
{
|
||||||
|
using (var ctx = new SharedLibraryCore.Database.DatabaseContext(true))
|
||||||
|
{
|
||||||
|
int linkId = await ctx.Clients
|
||||||
|
.Where(_client => _client.ClientId == clientId)
|
||||||
|
.Select(_client => _client.AliasLinkId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
var clientIds = await ctx.Clients.Where(_client => _client.AliasLinkId == linkId)
|
||||||
|
.Select(_client => _client.ClientId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var iqPenaltyInfo = ctx.Set<Models.EFACSnapshot>()
|
||||||
|
.Where(s => clientIds.Contains(s.ClientId))
|
||||||
|
.Include(s => s.LastStrainAngle)
|
||||||
|
.Include(s => s.HitOrigin)
|
||||||
|
.Include(s => s.HitDestination)
|
||||||
|
.Include(s => s.CurrentViewAngle)
|
||||||
|
.Include(s => s.PredictedViewAngles)
|
||||||
|
.OrderBy(s => s.When)
|
||||||
|
.ThenBy(s => s.Hits);
|
||||||
|
|
||||||
|
#if DEBUG == true
|
||||||
|
var sql = iqPenaltyInfo.ToSql();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var penaltyInfo = await iqPenaltyInfo.ToListAsync();
|
||||||
|
|
||||||
|
return View("_PenaltyInfo", penaltyInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,9 +14,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
{
|
{
|
||||||
float X = vector.X >= 0 ? vector.X : 360.0f + vector.X;
|
float X = vector.X >= 0 ? vector.X : 360.0f + vector.X;
|
||||||
float Y = vector.Y >= 0 ? vector.Y : 360.0f + vector.Y;
|
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;
|
public static float ToRadians(this float value) => (float)Math.PI * value / 180.0f;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using IW4MAdmin.Plugins.Stats.Models;
|
using IW4MAdmin.Plugins.Stats.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Helpers
|
namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||||
@ -9,6 +10,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
class ServerStats {
|
class ServerStats {
|
||||||
public ConcurrentDictionary<int, EFClientStatistics> PlayerStats { get; set; }
|
public ConcurrentDictionary<int, EFClientStatistics> PlayerStats { get; set; }
|
||||||
public ConcurrentDictionary<int, Detection> PlayerDetections { get; set; }
|
public ConcurrentDictionary<int, Detection> PlayerDetections { get; set; }
|
||||||
|
public IList<EFClientKill> HitCache { get; private set; }
|
||||||
public EFServerStatistics ServerStatistics { get; private set; }
|
public EFServerStatistics ServerStatistics { get; private set; }
|
||||||
public EFServer Server { get; private set; }
|
public EFServer Server { get; private set; }
|
||||||
public bool IsTeamBased { get; set; }
|
public bool IsTeamBased { get; set; }
|
||||||
@ -17,6 +19,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
{
|
{
|
||||||
PlayerStats = new ConcurrentDictionary<int, EFClientStatistics>();
|
PlayerStats = new ConcurrentDictionary<int, EFClientStatistics>();
|
||||||
PlayerDetections = new ConcurrentDictionary<int, Detection>();
|
PlayerDetections = new ConcurrentDictionary<int, Detection>();
|
||||||
|
HitCache = new List<EFClientKill>();
|
||||||
ServerStatistics = st;
|
ServerStatistics = st;
|
||||||
Server = sv;
|
Server = sv;
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,11 @@ using SharedLibraryCore.Database;
|
|||||||
using SharedLibraryCore.Database.Models;
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Helpers;
|
using SharedLibraryCore.Helpers;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -21,25 +19,28 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
{
|
{
|
||||||
public class StatManager
|
public class StatManager
|
||||||
{
|
{
|
||||||
private ConcurrentDictionary<long, ServerStats> Servers;
|
private const int MAX_CACHED_HITS = 100;
|
||||||
private ILogger Log;
|
private readonly ConcurrentDictionary<long, ServerStats> _servers;
|
||||||
private readonly IManager Manager;
|
private readonly ILogger _log;
|
||||||
|
private static List<EFServer> serverModels;
|
||||||
private readonly SemaphoreSlim OnProcessingPenalty;
|
|
||||||
private readonly SemaphoreSlim OnProcessingSensitive;
|
|
||||||
|
|
||||||
public StatManager(IManager mgr)
|
public StatManager(IManager mgr)
|
||||||
{
|
{
|
||||||
Servers = new ConcurrentDictionary<long, ServerStats>();
|
_servers = new ConcurrentDictionary<long, ServerStats>();
|
||||||
Log = mgr.GetLogger(0);
|
_log = mgr.GetLogger(0);
|
||||||
Manager = mgr;
|
}
|
||||||
OnProcessingPenalty = new SemaphoreSlim(1, 1);
|
|
||||||
OnProcessingSensitive = new SemaphoreSlim(1, 1);
|
private void SetupServerIds()
|
||||||
|
{
|
||||||
|
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||||
|
{
|
||||||
|
serverModels = ctx.Set<EFServer>().ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public EFClientStatistics GetClientStats(int clientId, long serverId)
|
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)
|
public static Expression<Func<EFRating, bool>> GetRankingFunc(long? serverId = null)
|
||||||
@ -82,13 +83,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count)
|
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
|
||||||
{
|
{
|
||||||
using (var context = new DatabaseContext(true))
|
using (var context = new DatabaseContext(true))
|
||||||
{
|
{
|
||||||
// setup the query for the clients within the given rating range
|
// setup the query for the clients within the given rating range
|
||||||
var iqClientRatings = (from rating in context.Set<EFRating>()
|
var iqClientRatings = (from rating in context.Set<EFRating>()
|
||||||
.Where(GetRankingFunc())
|
.Where(GetRankingFunc(serverId))
|
||||||
select new
|
select new
|
||||||
{
|
{
|
||||||
rating.RatingHistory.ClientId,
|
rating.RatingHistory.ClientId,
|
||||||
@ -113,7 +114,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
var iqRatingInfo = from rating in context.Set<EFRating>()
|
var iqRatingInfo = from rating in context.Set<EFRating>()
|
||||||
where clientIds.Contains(rating.RatingHistory.ClientId)
|
where clientIds.Contains(rating.RatingHistory.ClientId)
|
||||||
where rating.ServerId == null
|
where rating.ServerId == serverId
|
||||||
select new
|
select new
|
||||||
{
|
{
|
||||||
rating.Ranking,
|
rating.Ranking,
|
||||||
@ -136,6 +137,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
|
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
|
||||||
where clientIds.Contains(stat.ClientId)
|
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
|
group stat by stat.ClientId into s
|
||||||
select new
|
select new
|
||||||
{
|
{
|
||||||
@ -143,7 +146,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
Kills = s.Sum(c => c.Kills),
|
Kills = s.Sum(c => c.Kills),
|
||||||
Deaths = s.Sum(c => c.Deaths),
|
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),
|
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
|
#if DEBUG == true
|
||||||
@ -155,6 +158,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
var finished = topPlayers.Select(s => new TopStatsInfo()
|
var finished = topPlayers.Select(s => new TopStatsInfo()
|
||||||
{
|
{
|
||||||
ClientId = s.ClientId,
|
ClientId = s.ClientId,
|
||||||
|
Id = (int?)serverId ?? 0,
|
||||||
Deaths = s.Deaths,
|
Deaths = s.Deaths,
|
||||||
Kills = s.Kills,
|
Kills = s.Kills,
|
||||||
KDR = Math.Round(s.KDR, 2),
|
KDR = Math.Round(s.KDR, 2),
|
||||||
@ -191,7 +195,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
// insert the server if it does not exist
|
// insert the server if it does not exist
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
long serverId = GetIdForServer(sv).Result;
|
if (serverModels == null)
|
||||||
|
{
|
||||||
|
SetupServerIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
long serverId = GetIdForServer(sv);
|
||||||
EFServer server;
|
EFServer server;
|
||||||
|
|
||||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||||
@ -219,21 +228,30 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
{
|
{
|
||||||
server = new EFServer()
|
server = new EFServer()
|
||||||
{
|
{
|
||||||
Port = sv.GetPort(),
|
Port = sv.Port,
|
||||||
EndPoint = sv.ToString(),
|
EndPoint = sv.ToString(),
|
||||||
ServerId = serverId
|
ServerId = serverId,
|
||||||
|
GameName = sv.GameName
|
||||||
};
|
};
|
||||||
|
|
||||||
server = serverSet.Add(server).Entity;
|
server = serverSet.Add(server).Entity;
|
||||||
// this doesn't need to be async as it's during initialization
|
// this doesn't need to be async as it's during initialization
|
||||||
ctx.SaveChanges();
|
ctx.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we want to set the gamename up if it's never been set, or it changed
|
||||||
|
else if (!server.GameName.HasValue || server.GameName.HasValue && server.GameName.Value != sv.GameName)
|
||||||
|
{
|
||||||
|
server.GameName = sv.GameName;
|
||||||
|
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
|
||||||
|
ctx.SaveChanges();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check to see if the stats have ever been initialized
|
// check to see if the stats have ever been initialized
|
||||||
var serverStats = InitializeServerStats(server.ServerId);
|
var serverStats = InitializeServerStats(server.ServerId);
|
||||||
|
|
||||||
Servers.TryAdd(serverId, new ServerStats(server, serverStats)
|
_servers.TryAdd(serverId, new ServerStats(server, serverStats)
|
||||||
{
|
{
|
||||||
IsTeamBased = sv.Gametype != "dm"
|
IsTeamBased = sv.Gametype != "dm"
|
||||||
});
|
});
|
||||||
@ -241,8 +259,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]} - {e.Message}");
|
_log.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]} - {e.Message}");
|
||||||
Log.WriteDebug(e.GetExceptionInfo());
|
_log.WriteDebug(e.GetExceptionInfo());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,24 +271,22 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
/// <returns>EFClientStatistic of specified player</returns>
|
/// <returns>EFClientStatistic of specified player</returns>
|
||||||
public async Task<EFClientStatistics> AddPlayer(EFClient pl)
|
public async Task<EFClientStatistics> AddPlayer(EFClient pl)
|
||||||
{
|
{
|
||||||
await OnProcessingSensitive.WaitAsync();
|
|
||||||
|
|
||||||
try
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerStats = Servers[serverId].PlayerStats;
|
var playerStats = _servers[serverId].PlayerStats;
|
||||||
var detectionStats = Servers[serverId].PlayerDetections;
|
var detectionStats = _servers[serverId].PlayerDetections;
|
||||||
|
|
||||||
if (playerStats.ContainsKey(pl.ClientId))
|
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];
|
return playerStats[pl.ClientId];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,7 +328,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
if (!playerStats.TryAdd(clientStats.ClientId, clientStats))
|
if (!playerStats.TryAdd(clientStats.ClientId, clientStats))
|
||||||
{
|
{
|
||||||
Log.WriteWarning("Adding new client to stats failed");
|
_log.WriteWarning("Adding new client to stats failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,7 +336,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
{
|
{
|
||||||
if (!playerStats.TryAdd(clientStats.ClientId, clientStats))
|
if (!playerStats.TryAdd(clientStats.ClientId, clientStats))
|
||||||
{
|
{
|
||||||
Log.WriteWarning("Adding pre-existing client to stats failed");
|
_log.WriteWarning("Adding pre-existing client to stats failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,9 +373,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
clientStats.SessionScore = pl.Score;
|
clientStats.SessionScore = pl.Score;
|
||||||
clientStats.LastScore = 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");
|
pl.CurrentServer.Logger.WriteInfo($"Adding {pl} to stats");
|
||||||
@ -370,13 +386,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.WriteWarning("Could not add client to stats");
|
_log.WriteWarning("Could not add client to stats");
|
||||||
Log.WriteDebug(ex.GetExceptionInfo());
|
_log.WriteDebug(ex.GetExceptionInfo());
|
||||||
}
|
|
||||||
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
OnProcessingSensitive.Release(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -391,55 +402,46 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
{
|
{
|
||||||
pl.CurrentServer.Logger.WriteInfo($"Removing {pl} from stats");
|
pl.CurrentServer.Logger.WriteInfo($"Removing {pl} from stats");
|
||||||
|
|
||||||
long serverId = await GetIdForServer(pl.CurrentServer);
|
long serverId = GetIdForServer(pl.CurrentServer);
|
||||||
var playerStats = Servers[serverId].PlayerStats;
|
var playerStats = _servers[serverId].PlayerStats;
|
||||||
var detectionStats = Servers[serverId].PlayerDetections;
|
var detectionStats = _servers[serverId].PlayerDetections;
|
||||||
var serverStats = Servers[serverId].ServerStatistics;
|
var serverStats = _servers[serverId].ServerStatistics;
|
||||||
|
|
||||||
if (!playerStats.ContainsKey(pl.ClientId))
|
if (!playerStats.ContainsKey(pl.ClientId))
|
||||||
{
|
{
|
||||||
pl.CurrentServer.Logger.WriteWarning($"Client disconnecting not in stats {pl}");
|
pl.CurrentServer.Logger.WriteWarning($"Client disconnecting not in stats {pl}");
|
||||||
// remove the client from the stats dictionary as they're leaving
|
// remove the client from the stats dictionary as they're leaving
|
||||||
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue1);
|
playerStats.TryRemove(pl.ClientId, out _);
|
||||||
detectionStats.TryRemove(pl.ClientId, out Detection removedValue2);
|
detectionStats.TryRemove(pl.ClientId, out _);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get individual client's stats
|
// get individual client's stats
|
||||||
var clientStats = playerStats[pl.ClientId];
|
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
|
// sync their stats before they leave
|
||||||
clientStats = UpdateStats(clientStats);
|
clientStats = UpdateStats(clientStats);
|
||||||
|
await SaveClientStats(clientStats);
|
||||||
|
|
||||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
// remove the client from the stats dictionary as they're leaving
|
||||||
{
|
playerStats.TryRemove(pl.ClientId, out _);
|
||||||
ctx.Update(clientStats);
|
detectionStats.TryRemove(pl.ClientId, out _);
|
||||||
await ctx.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
// increment the total play time
|
// increment the total play time
|
||||||
serverStats.TotalPlayTime += pl.ConnectionLength;
|
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)
|
public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, long serverId)
|
||||||
{
|
{
|
||||||
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());
|
|
||||||
IW4Info.Team attackerTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[7].ToString());
|
|
||||||
attackerStats.Team = attackerTeam;
|
|
||||||
victimStats.Team = victimTeam;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -453,180 +455,194 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
Vector3 vDeathOrigin = null;
|
Vector3 vDeathOrigin = null;
|
||||||
Vector3 vKillOrigin = null;
|
Vector3 vKillOrigin = null;
|
||||||
Vector3 vViewAngles = null;
|
Vector3 vViewAngles = null;
|
||||||
|
SemaphoreSlim waiter = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
vDeathOrigin = Vector3.Parse(deathOrigin);
|
try
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
snapshotAngles.Add(Vector3.Parse(angle).FixIW4Angles());
|
vDeathOrigin = Vector3.Parse(deathOrigin);
|
||||||
|
vKillOrigin = Vector3.Parse(killOrigin);
|
||||||
|
vViewAngles = Vector3.Parse(viewAngles).FixIW4Angles();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
catch (FormatException)
|
catch (FormatException)
|
||||||
{
|
{
|
||||||
Log.WriteWarning("Could not parse snapshot angles");
|
_log.WriteWarning("Could not parse kill or death origin or viewangle vectors");
|
||||||
return;
|
_log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAngle - {viewAngles}");
|
||||||
}
|
await AddStandardKill(attacker, victim);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var hit = new EFClientKill()
|
var snapshotAngles = new List<Vector3>();
|
||||||
{
|
|
||||||
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 palyer 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();
|
|
||||||
|
|
||||||
try
|
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)
|
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();
|
await SaveTrackedSnapshots(clientDetection);
|
||||||
var oldestHit = clientDetection.QueuedHits.First();
|
|
||||||
clientDetection.QueuedHits.RemoveAt(0);
|
if (result.ClientPenalty == EFPenalty.PenaltyType.Ban)
|
||||||
ApplyPenalty(clientDetection.ProcessHit(oldestHit, isDamage), clientDetection, attacker, ctx);
|
{
|
||||||
|
// we don't care about any additional hits now that they're banned
|
||||||
|
clientDetection.TrackedHits.Clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
clientDetection.QueuedHits.Add(hit);
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyPenalty(clientDetection.ProcessTotalRatio(clientStats), clientDetection, attacker, ctx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Set<EFHitLocationCount>().UpdateRange(clientStats.HitLocations);
|
|
||||||
|
|
||||||
await ctx.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.WriteError("Could not save hit or AC info");
|
_log.WriteError("Could not save hit or AC info");
|
||||||
Log.WriteDebug(ex.GetExceptionInfo());
|
_log.WriteDebug(ex.GetExceptionInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
OnProcessingPenalty.Release(1);
|
finally
|
||||||
|
{
|
||||||
|
waiter?.Release(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void 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)
|
switch (penalty.ClientPenalty)
|
||||||
{
|
{
|
||||||
case Penalty.PenaltyType.Ban:
|
case EFPenalty.PenaltyType.Ban:
|
||||||
if (attacker.Level == EFClient.Permission.Banned)
|
if (attacker.Level == EFClient.Permission.Banned)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new EFClient()
|
|
||||||
{
|
|
||||||
ClientId = 1,
|
|
||||||
AdministeredPenalties = new List<EFPenalty>()
|
|
||||||
{
|
|
||||||
new EFPenalty()
|
|
||||||
{
|
|
||||||
AutomatedOffense = penalty.Type == Cheat.Detection.DetectionType.Bone ?
|
|
||||||
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
|
|
||||||
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Level = EFClient.Permission.Console,
|
|
||||||
CurrentServer = attacker.CurrentServer,
|
|
||||||
|
|
||||||
}, false);
|
penaltyClient.AdministeredPenalties = new List<EFPenalty>()
|
||||||
if (clientDetection.Tracker.HasChanges)
|
|
||||||
{
|
{
|
||||||
SaveTrackedSnapshots(clientDetection, ctx);
|
new EFPenalty()
|
||||||
}
|
{
|
||||||
|
AutomatedOffense = penalty.Type == Detection.DetectionType.Bone ?
|
||||||
|
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
|
||||||
|
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], penaltyClient, false).WaitAsync(Utilities.DefaultCommandTimeout, attacker.CurrentServer.Manager.CancellationToken);
|
||||||
break;
|
break;
|
||||||
case Penalty.PenaltyType.Flag:
|
case EFPenalty.PenaltyType.Flag:
|
||||||
if (attacker.Level != EFClient.Permission.User)
|
if (attacker.Level != EFClient.Permission.User)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@ -636,110 +652,57 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
|
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
|
||||||
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}";
|
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}";
|
||||||
|
|
||||||
attacker.Flag(flagReason, new EFClient()
|
await attacker.Flag(flagReason, penaltyClient, new TimeSpan(168, 0, 0)).WaitAsync(Utilities.DefaultCommandTimeout, attacker.CurrentServer.Manager.CancellationToken);
|
||||||
{
|
|
||||||
ClientId = 1,
|
|
||||||
Level = EFClient.Permission.Console,
|
|
||||||
CurrentServer = attacker.CurrentServer,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (clientDetection.Tracker.HasChanges)
|
|
||||||
{
|
|
||||||
SaveTrackedSnapshots(clientDetection, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SaveTrackedSnapshots(Cheat.Detection clientDetection, DatabaseContext ctx)
|
async Task SaveTrackedSnapshots(Detection clientDetection)
|
||||||
{
|
{
|
||||||
// todo: why does this cause duplicate primary key
|
EFACSnapshot change;
|
||||||
var change = clientDetection.Tracker.GetNextChange();
|
|
||||||
while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot))
|
using (var ctx = new DatabaseContext(true))
|
||||||
{
|
{
|
||||||
|
while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot))
|
||||||
if (change.HitOrigin.Vector3Id > 0)
|
|
||||||
{
|
{
|
||||||
change.HitOriginId = change.HitOrigin.Vector3Id;
|
ctx.Add(change);
|
||||||
ctx.Attach(change.HitOrigin);
|
|
||||||
}
|
}
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddStandardKill(EFClient attacker, EFClient victim)
|
public async Task AddStandardKill(EFClient attacker, EFClient victim)
|
||||||
{
|
{
|
||||||
long serverId = await GetIdForServer(attacker.CurrentServer);
|
long serverId = GetIdForServer(attacker.CurrentServer);
|
||||||
|
|
||||||
EFClientStatistics attackerStats = null;
|
EFClientStatistics attackerStats = null;
|
||||||
if (!Servers[serverId].PlayerStats.ContainsKey(attacker.ClientId))
|
if (!_servers[serverId].PlayerStats.ContainsKey(attacker.ClientId))
|
||||||
{
|
{
|
||||||
attackerStats = await AddPlayer(attacker);
|
attackerStats = await AddPlayer(attacker);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
attackerStats = Servers[serverId].PlayerStats[attacker.ClientId];
|
attackerStats = _servers[serverId].PlayerStats[attacker.ClientId];
|
||||||
}
|
}
|
||||||
|
|
||||||
EFClientStatistics victimStats = null;
|
EFClientStatistics victimStats = null;
|
||||||
if (!Servers[serverId].PlayerStats.ContainsKey(victim.ClientId))
|
if (!_servers[serverId].PlayerStats.ContainsKey(victim.ClientId))
|
||||||
{
|
{
|
||||||
victimStats = await AddPlayer(victim);
|
victimStats = await AddPlayer(victim);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
victimStats = Servers[serverId].PlayerStats[victim.ClientId];
|
victimStats = _servers[serverId].PlayerStats[victim.ClientId];
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Log.WriteDebug("Calculating standard kill");
|
_log.WriteDebug("Calculating standard kill");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// update the total stats
|
// update the total stats
|
||||||
Servers[serverId].ServerStatistics.TotalKills += 1;
|
_servers[serverId].ServerStatistics.TotalKills += 1;
|
||||||
await Sync(attacker.CurrentServer);
|
|
||||||
|
|
||||||
// this happens when the round has changed
|
// this happens when the round has changed
|
||||||
if (attackerStats.SessionScore == 0)
|
if (attackerStats.SessionScore == 0)
|
||||||
@ -775,38 +738,24 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
// fixme: why?
|
// fixme: why?
|
||||||
if (double.IsNaN(victimStats.SPM) || double.IsNaN(victimStats.Skill))
|
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.SPM = 0.0;
|
||||||
victimStats.Skill = 0.0;
|
victimStats.Skill = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (double.IsNaN(attackerStats.SPM) || double.IsNaN(attackerStats.Skill))
|
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.SPM = 0.0;
|
||||||
attackerStats.Skill = 0.0;
|
attackerStats.Skill = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update their performance
|
// update their performance
|
||||||
#if !DEBUG
|
|
||||||
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 2.5)
|
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 2.5)
|
||||||
#else
|
|
||||||
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 0.1)
|
|
||||||
#endif
|
|
||||||
{
|
{
|
||||||
attackerStats.LastStatHistoryUpdate = DateTime.UtcNow;
|
attackerStats.LastStatHistoryUpdate = DateTime.UtcNow;
|
||||||
await UpdateStatHistory(attacker, attackerStats);
|
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>
|
/// <summary>
|
||||||
@ -820,12 +769,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds;
|
int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds;
|
||||||
|
|
||||||
// don't update their stat history if they haven't played long
|
// don't update their stat history if they haven't played long
|
||||||
#if DEBUG == false
|
|
||||||
if (currentSessionTime < 60)
|
if (currentSessionTime < 60)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime;
|
int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime;
|
||||||
|
|
||||||
@ -855,11 +802,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
#region INDIVIDUAL_SERVER_PERFORMANCE
|
#region INDIVIDUAL_SERVER_PERFORMANCE
|
||||||
// get the client ranking for the current server
|
// get the client ranking for the current server
|
||||||
int individualClientRanking = await ctx.Set<EFRating>()
|
int individualClientRanking = await ctx.Set<EFRating>()
|
||||||
.Where(GetRankingFunc(clientStats.ServerId))
|
.Where(GetRankingFunc(clientStats.ServerId))
|
||||||
// ignore themselves in the query
|
// ignore themselves in the query
|
||||||
.Where(c => c.RatingHistory.ClientId != client.ClientId)
|
.Where(c => c.RatingHistory.ClientId != client.ClientId)
|
||||||
.Where(c => c.Performance > clientStats.Performance)
|
.Where(c => c.Performance > clientStats.Performance)
|
||||||
.CountAsync() + 1;
|
.CountAsync() + 1;
|
||||||
|
|
||||||
// limit max history per server to 40
|
// limit max history per server to 40
|
||||||
if (clientHistory.Ratings.Count(r => r.ServerId == clientStats.ServerId) >= 40)
|
if (clientHistory.Ratings.Count(r => r.ServerId == clientStats.ServerId) >= 40)
|
||||||
@ -1012,7 +959,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
attackerStats = UpdateStats(attackerStats);
|
attackerStats = UpdateStats(attackerStats);
|
||||||
|
|
||||||
// calulate elo
|
// calulate elo
|
||||||
if (Servers[attackerStats.ServerId].PlayerStats.Count > 1)
|
if (_servers[attackerStats.ServerId].PlayerStats.Count > 1)
|
||||||
{
|
{
|
||||||
#region DEPRECATED
|
#region DEPRECATED
|
||||||
/* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats
|
/* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats
|
||||||
@ -1093,7 +1040,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
double killSPM = scoreDifference / timeSinceLastCalc;
|
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);
|
killSPM *= Math.Max(1, spmMultiplier);
|
||||||
|
|
||||||
// update this for ac tracking
|
// update this for ac tracking
|
||||||
@ -1118,8 +1065,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
if (clientStats.SPM < 0)
|
if (clientStats.SPM < 0)
|
||||||
{
|
{
|
||||||
Log.WriteWarning("[StatManager:UpdateStats] clientStats SPM < 0");
|
_log.WriteWarning("[StatManager:UpdateStats] clientStats SPM < 0");
|
||||||
Log.WriteDebug($"{scoreDifference}-{clientStats.RoundScore} - {clientStats.LastScore} - {clientStats.SessionScore}");
|
_log.WriteDebug($"{scoreDifference}-{clientStats.RoundScore} - {clientStats.LastScore} - {clientStats.SessionScore}");
|
||||||
clientStats.SPM = 0;
|
clientStats.SPM = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1129,8 +1076,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
// fixme: how does this happen?
|
// fixme: how does this happen?
|
||||||
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
|
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
|
||||||
{
|
{
|
||||||
Log.WriteWarning("[StatManager::UpdateStats] clientStats SPM/Skill NaN");
|
_log.WriteWarning("[StatManager::UpdateStats] clientStats SPM/Skill NaN");
|
||||||
Log.WriteDebug($"{killSPM}-{KDRWeight}-{totalPlayTime}-{SPMAgainstPlayWeight}-{clientStats.SPM}-{clientStats.Skill}-{scoreDifference}");
|
_log.WriteDebug($"{killSPM}-{KDRWeight}-{totalPlayTime}-{SPMAgainstPlayWeight}-{clientStats.SPM}-{clientStats.Skill}-{scoreDifference}");
|
||||||
clientStats.SPM = 0;
|
clientStats.SPM = 0;
|
||||||
clientStats.Skill = 0;
|
clientStats.Skill = 0;
|
||||||
}
|
}
|
||||||
@ -1152,7 +1099,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
if (serverStats == null)
|
if (serverStats == null)
|
||||||
{
|
{
|
||||||
Log.WriteDebug($"Initializing server stats for {serverId}");
|
_log.WriteDebug($"Initializing server stats for {serverId}");
|
||||||
// server stats have never been generated before
|
// server stats have never been generated before
|
||||||
serverStats = new EFServerStatistics()
|
serverStats = new EFServerStatistics()
|
||||||
{
|
{
|
||||||
@ -1171,7 +1118,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
public void ResetKillstreaks(long serverId)
|
public void ResetKillstreaks(long serverId)
|
||||||
{
|
{
|
||||||
var serverStats = Servers[serverId];
|
var serverStats = _servers[serverId];
|
||||||
|
|
||||||
foreach (var stat in serverStats.PlayerStats.Values)
|
foreach (var stat in serverStats.PlayerStats.Values)
|
||||||
{
|
{
|
||||||
@ -1181,7 +1128,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
public void ResetStats(int clientId, long serverId)
|
public void ResetStats(int clientId, long serverId)
|
||||||
{
|
{
|
||||||
var stats = Servers[serverId].PlayerStats[clientId];
|
var stats = _servers[serverId].PlayerStats[clientId];
|
||||||
stats.Kills = 0;
|
stats.Kills = 0;
|
||||||
stats.Deaths = 0;
|
stats.Deaths = 0;
|
||||||
stats.SPM = 0;
|
stats.SPM = 0;
|
||||||
@ -1214,64 +1161,55 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
public async Task Sync(Server sv)
|
public async Task Sync(Server sv)
|
||||||
{
|
{
|
||||||
long serverId = await GetIdForServer(sv);
|
long serverId = GetIdForServer(sv);
|
||||||
|
|
||||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||||
{
|
{
|
||||||
var serverSet = ctx.Set<EFServer>();
|
var serverSet = ctx.Set<EFServer>();
|
||||||
serverSet.Update(Servers[serverId].Server);
|
serverSet.Update(_servers[serverId].Server);
|
||||||
|
|
||||||
var serverStatsSet = ctx.Set<EFServerStatistics>();
|
var serverStatsSet = ctx.Set<EFServerStatistics>();
|
||||||
serverStatsSet.Update(Servers[serverId].ServerStatistics);
|
serverStatsSet.Update(_servers[serverId].ServerStatistics);
|
||||||
|
|
||||||
await ctx.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
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.Port.ToString()}" == "66.150.121.184:28965")
|
||||||
if ($"{server.IP}:{server.GetPort().ToString()}" == "66.150.121.184:28965")
|
|
||||||
{
|
{
|
||||||
return 886229536;
|
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 serverId.Value;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 CurrentStrain { get; set; }
|
||||||
public double StrainAngleBetween { get; set; }
|
public double StrainAngleBetween { get; set; }
|
||||||
public double SessionAngleOffset { get; set; }
|
public double SessionAngleOffset { get; set; }
|
||||||
|
public double RecoilOffset { get; set; }
|
||||||
public int LastStrainAngleId { get; set; }
|
public int LastStrainAngleId { get; set; }
|
||||||
[ForeignKey("LastStrainAngleId")]
|
[ForeignKey("LastStrainAngleId")]
|
||||||
public Vector3 LastStrainAngle { get; set; }
|
public Vector3 LastStrainAngle { get; set; }
|
||||||
|
@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using SharedLibraryCore.Database.Models;
|
using SharedLibraryCore.Database.Models;
|
||||||
@ -12,6 +13,16 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
|||||||
{
|
{
|
||||||
public class EFClientStatistics : SharedEntity
|
public class EFClientStatistics : SharedEntity
|
||||||
{
|
{
|
||||||
|
public EFClientStatistics()
|
||||||
|
{
|
||||||
|
ProcessingHit = new SemaphoreSlim(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
~EFClientStatistics()
|
||||||
|
{
|
||||||
|
ProcessingHit.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
public int ClientId { get; set; }
|
public int ClientId { get; set; }
|
||||||
[ForeignKey("ClientId")]
|
[ForeignKey("ClientId")]
|
||||||
public virtual EFClient Client { get; set; }
|
public virtual EFClient Client { get; set; }
|
||||||
@ -26,6 +37,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
|||||||
public virtual ICollection<EFHitLocationCount> HitLocations { get; set; }
|
public virtual ICollection<EFHitLocationCount> HitLocations { get; set; }
|
||||||
public double RollingWeightedKDR { get; set; }
|
public double RollingWeightedKDR { get; set; }
|
||||||
public double VisionAverage { get; set; }
|
public double VisionAverage { get; set; }
|
||||||
|
public double AverageRecoilOffset { get; set; }
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public double Performance
|
public double Performance
|
||||||
{
|
{
|
||||||
@ -95,12 +107,14 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
private List<int> SessionScores = new List<int>() { 0 };
|
private readonly List<int> SessionScores = new List<int>() { 0 };
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public IW4Info.Team Team { get; set; }
|
public IW4Info.Team Team { get; set; }
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public DateTime LastStatHistoryUpdate { get; set; } = DateTime.UtcNow;
|
public DateTime LastStatHistoryUpdate { get; set; } = DateTime.UtcNow;
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public double SessionSPM { get; set; }
|
public double SessionSPM { get; set; }
|
||||||
|
[NotMapped]
|
||||||
|
public SemaphoreSlim ProcessingHit { get; private set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using SharedLibraryCore.Database.Models;
|
using SharedLibraryCore.Database.Models;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using static SharedLibraryCore.Server;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Models
|
namespace IW4MAdmin.Plugins.Stats.Models
|
||||||
{
|
{
|
||||||
@ -13,5 +14,6 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
|||||||
[Required]
|
[Required]
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
public string EndPoint { get; set; }
|
public string EndPoint { get; set; }
|
||||||
|
public Game? GameName { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,13 +21,7 @@ namespace Stats.Models
|
|||||||
.HasColumnName("EFClientStatistics_ServerId");
|
.HasColumnName("EFClientStatistics_ServerId");
|
||||||
|
|
||||||
builder.Entity<EFRating>()
|
builder.Entity<EFRating>()
|
||||||
.HasIndex(p => p.Performance);
|
.HasIndex(p => new { p.Performance, p.Ranking, p.When });
|
||||||
|
|
||||||
builder.Entity<EFRating>()
|
|
||||||
.HasIndex(p => p.Ranking);
|
|
||||||
|
|
||||||
builder.Entity<EFRating>()
|
|
||||||
.HasIndex(p => p.When);
|
|
||||||
|
|
||||||
builder.Entity<EFClientMessage>()
|
builder.Entity<EFClientMessage>()
|
||||||
.HasIndex(p => p.TimeSent);
|
.HasIndex(p => p.TimeSent);
|
||||||
|
@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
using SharedLibraryCore.Database;
|
using SharedLibraryCore.Database;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Dtos;
|
using SharedLibraryCore.Dtos;
|
||||||
using SharedLibraryCore.Helpers;
|
using SharedLibraryCore.Helpers;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
@ -26,8 +27,12 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
public string Author => "RaidMax";
|
public string Author => "RaidMax";
|
||||||
|
|
||||||
public static StatManager Manager { get; private set; }
|
public static StatManager Manager { get; private set; }
|
||||||
private IManager ServerManager;
|
public static IManager ServerManager;
|
||||||
public static BaseConfigurationHandler<StatsConfiguration> Config { get; private set; }
|
public static BaseConfigurationHandler<StatsConfiguration> Config { get; private set; }
|
||||||
|
#if DEBUG
|
||||||
|
int scriptDamageCount;
|
||||||
|
int scriptKillCount;
|
||||||
|
#endif
|
||||||
|
|
||||||
public async Task OnEventAsync(GameEvent E, Server S)
|
public async Task OnEventAsync(GameEvent E, Server S)
|
||||||
{
|
{
|
||||||
@ -43,21 +48,21 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
break;
|
break;
|
||||||
case GameEvent.EventType.Disconnect:
|
case GameEvent.EventType.Disconnect:
|
||||||
await Manager.RemovePlayer(E.Origin);
|
await Manager.RemovePlayer(E.Origin);
|
||||||
await Manager.Sync(S);
|
|
||||||
break;
|
break;
|
||||||
case GameEvent.EventType.Say:
|
case GameEvent.EventType.Say:
|
||||||
if (!string.IsNullOrEmpty(E.Data) &&
|
if (!string.IsNullOrEmpty(E.Data) &&
|
||||||
E.Origin.ClientId > 1)
|
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;
|
break;
|
||||||
case GameEvent.EventType.MapChange:
|
case GameEvent.EventType.MapChange:
|
||||||
Manager.SetTeamBased(await StatManager.GetIdForServer(E.Owner), E.Owner.Gametype != "dm");
|
Manager.SetTeamBased(StatManager.GetIdForServer(E.Owner), E.Owner.Gametype != "dm");
|
||||||
Manager.ResetKillstreaks(await StatManager.GetIdForServer(E.Owner));
|
Manager.ResetKillstreaks(StatManager.GetIdForServer(E.Owner));
|
||||||
|
await Manager.Sync(E.Owner);
|
||||||
break;
|
break;
|
||||||
case GameEvent.EventType.MapEnd:
|
case GameEvent.EventType.MapEnd:
|
||||||
|
await Manager.Sync(E.Owner);
|
||||||
break;
|
break;
|
||||||
case GameEvent.EventType.JoinTeam:
|
case GameEvent.EventType.JoinTeam:
|
||||||
break;
|
break;
|
||||||
@ -77,30 +82,76 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
break;
|
break;
|
||||||
case GameEvent.EventType.ScriptKill:
|
case GameEvent.EventType.ScriptKill:
|
||||||
string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
|
string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
|
||||||
if (killInfo.Length >= 14)
|
if (E.Owner.CustomCallback && killInfo.Length >= 14 && !ShouldIgnoreEvent(E.Origin, E.Target))
|
||||||
{
|
{
|
||||||
await Manager.AddScriptHit(false, E.Time, E.Origin, E.Target, await StatManager.GetIdForServer(E.Owner), S.CurrentMap.Name, killInfo[7], killInfo[8],
|
// this treats "world" damage as self damage
|
||||||
|
if (IsWorldDamage(E.Origin))
|
||||||
|
{
|
||||||
|
E.Origin = E.Target;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
scriptKillCount++;
|
||||||
|
S.Logger.WriteInfo($"Start ScriptKill {scriptKillCount}");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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]);
|
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;
|
break;
|
||||||
case GameEvent.EventType.Kill:
|
case GameEvent.EventType.Kill:
|
||||||
if (!E.Owner.CustomCallback)
|
if (!E.Owner.CustomCallback && !ShouldIgnoreEvent(E.Origin, E.Target))
|
||||||
{
|
{
|
||||||
|
// this treats "world" damage as self damage
|
||||||
|
if (IsWorldDamage(E.Origin))
|
||||||
|
{
|
||||||
|
E.Origin = E.Target;
|
||||||
|
}
|
||||||
|
|
||||||
await Manager.AddStandardKill(E.Origin, E.Target);
|
await Manager.AddStandardKill(E.Origin, E.Target);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case GameEvent.EventType.Damage:
|
case GameEvent.EventType.Damage:
|
||||||
if (!E.Owner.CustomCallback)
|
if (!E.Owner.CustomCallback && !ShouldIgnoreEvent(E.Origin, E.Target))
|
||||||
{
|
{
|
||||||
Manager.AddDamageEvent(E.Data, E.Origin.ClientId, E.Target.ClientId, await StatManager.GetIdForServer(E.Owner));
|
// this treats "world" damage as self damage
|
||||||
|
if (IsWorldDamage(E.Origin))
|
||||||
|
{
|
||||||
|
E.Origin = E.Target;
|
||||||
|
}
|
||||||
|
|
||||||
|
Manager.AddDamageEvent(E.Data, E.Origin.ClientId, E.Target.ClientId, StatManager.GetIdForServer(E.Owner));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case GameEvent.EventType.ScriptDamage:
|
case GameEvent.EventType.ScriptDamage:
|
||||||
killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
|
killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
|
||||||
if (killInfo.Length >= 14)
|
if (E.Owner.CustomCallback && killInfo.Length >= 14 && !ShouldIgnoreEvent(E.Origin, E.Target))
|
||||||
{
|
{
|
||||||
await Manager.AddScriptHit(true, E.Time, E.Origin, E.Target, await StatManager.GetIdForServer(E.Owner), S.CurrentMap.Name, killInfo[7], killInfo[8],
|
// this treats "world" damage as self damage
|
||||||
|
if (IsWorldDamage(E.Origin))
|
||||||
|
{
|
||||||
|
E.Origin = E.Target;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
scriptDamageCount++;
|
||||||
|
S.Logger.WriteInfo($"Start ScriptDamage {scriptDamageCount}");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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]);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
@ -124,8 +175,13 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
"/Stats/TopPlayersAsync");
|
"/Stats/TopPlayersAsync");
|
||||||
|
|
||||||
// meta data info
|
// meta data info
|
||||||
async Task<List<ProfileMeta>> getStats(int clientId)
|
async Task<List<ProfileMeta>> getStats(int clientId, int offset, int count, DateTime? startAt)
|
||||||
{
|
{
|
||||||
|
if (count > 1)
|
||||||
|
{
|
||||||
|
return new List<ProfileMeta>();
|
||||||
|
}
|
||||||
|
|
||||||
IList<EFClientStatistics> clientStats;
|
IList<EFClientStatistics> clientStats;
|
||||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||||
{
|
{
|
||||||
@ -145,39 +201,63 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
|
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
|
||||||
Value = "#" + await StatManager.GetClientOverallRanking(clientId),
|
Value = "#" + (await StatManager.GetClientOverallRanking(clientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||||
|
Column = 0,
|
||||||
|
Order = 0,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
},
|
},
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"],
|
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"],
|
||||||
Value = kills
|
Value = kills.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||||
|
Column = 0,
|
||||||
|
Order = 1,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
},
|
},
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"],
|
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"],
|
||||||
Value = deaths
|
Value = deaths.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||||
|
Column = 0,
|
||||||
|
Order = 2,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
},
|
},
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"],
|
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"],
|
||||||
Value = kdr
|
Value = kdr.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||||
|
Column = 0,
|
||||||
|
Order = 3,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
},
|
},
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
|
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
|
||||||
Value = performance
|
Value = performance.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||||
|
Column = 0,
|
||||||
|
Order = 4,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
},
|
},
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_META_SPM"],
|
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_META_SPM"],
|
||||||
Value = spm
|
Value = spm.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||||
|
Column = 0,
|
||||||
|
Order = 5,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<List<ProfileMeta>> getAnticheatInfo(int clientId)
|
async Task<List<ProfileMeta>> getAnticheatInfo(int clientId, int offset, int count, DateTime? startAt)
|
||||||
{
|
{
|
||||||
|
if (count > 1)
|
||||||
|
{
|
||||||
|
return new List<ProfileMeta>();
|
||||||
|
}
|
||||||
|
|
||||||
IList<EFClientStatistics> clientStats;
|
IList<EFClientStatistics> clientStats;
|
||||||
|
|
||||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||||
{
|
{
|
||||||
clientStats = await ctx.Set<EFClientStatistics>()
|
clientStats = await ctx.Set<EFClientStatistics>()
|
||||||
@ -191,132 +271,212 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
double abdomenRatio = 0;
|
double abdomenRatio = 0;
|
||||||
double chestAbdomenRatio = 0;
|
double chestAbdomenRatio = 0;
|
||||||
double hitOffsetAverage = 0;
|
double hitOffsetAverage = 0;
|
||||||
|
double averageRecoilAmount = 0;
|
||||||
double maxStrain = clientStats.Count(c => c.MaxStrain > 0) == 0 ? 0 : clientStats.Max(cs => cs.MaxStrain);
|
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)
|
if (clientStats.Where(cs => cs.HitLocations.Count > 0).FirstOrDefault() != null)
|
||||||
{
|
{
|
||||||
chestRatio = Math.Round(clientStats.Where(c => c.HitLocations.Count > 0).Sum(c =>
|
chestRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(c =>
|
||||||
c.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_upper).HitCount) /
|
c.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_upper).HitCount) /
|
||||||
(double)clientStats.Where(c => c.HitLocations.Count > 0)
|
(double)clientStats.Where(c => c.HitLocations.Count > 0)
|
||||||
.Sum(c => c.HitLocations.Where(hl => hl.Location != IW4Info.HitLocation.none).Sum(f => f.HitCount)), 2);
|
.Sum(c => c.HitLocations.Where(hl => hl.Location != IW4Info.HitLocation.none).Sum(f => f.HitCount))) * 100.0, 0);
|
||||||
|
|
||||||
abdomenRatio = Math.Round(clientStats.Where(c => c.HitLocations.Count > 0).Sum(c =>
|
abdomenRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(c =>
|
||||||
c.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount) /
|
c.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount) /
|
||||||
(double)clientStats.Where(c => c.HitLocations.Count > 0).Sum(c => c.HitLocations.Where(hl => hl.Location != IW4Info.HitLocation.none).Sum(f => f.HitCount)), 2);
|
(double)clientStats.Where(c => c.HitLocations.Count > 0).Sum(c => c.HitLocations.Where(hl => hl.Location != IW4Info.HitLocation.none).Sum(f => f.HitCount))) * 100.0, 0);
|
||||||
|
|
||||||
chestAbdomenRatio = Math.Round(clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_upper).HitCount) /
|
chestAbdomenRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_upper).HitCount) /
|
||||||
(double)clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount), 2);
|
(double)clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount)) * 100.0, 0);
|
||||||
|
|
||||||
headRatio = Math.Round(clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.head).HitCount) /
|
headRatio = Math.Round((clientStats.Where(c => c.HitLocations.Count > 0).Sum(cs => cs.HitLocations.First(hl => hl.Location == IW4Info.HitLocation.head).HitCount) /
|
||||||
(double)clientStats.Where(c => c.HitLocations.Count > 0)
|
(double)clientStats.Where(c => c.HitLocations.Count > 0)
|
||||||
.Sum(c => c.HitLocations.Where(hl => hl.Location != IW4Info.HitLocation.none).Sum(f => f.HitCount)), 2);
|
.Sum(c => c.HitLocations.Where(hl => hl.Location != IW4Info.HitLocation.none).Sum(f => f.HitCount))) * 100.0, 0);
|
||||||
|
|
||||||
var validOffsets = clientStats.Where(c => c.HitLocations.Count(hl => hl.HitCount > 0) > 0).SelectMany(hl => hl.HitLocations);
|
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);
|
hitOffsetAverage = validOffsets.Sum(o => o.HitCount * o.HitOffsetAverage) / (double)validOffsets.Sum(o => o.HitCount);
|
||||||
|
averageRecoilAmount = clientStats.Average(_stat => _stat.AverageRecoilOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new List<ProfileMeta>()
|
return new List<ProfileMeta>()
|
||||||
{
|
{
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = "Chest Ratio",
|
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 1",
|
||||||
Value = chestRatio,
|
Value = chestRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||||
|
Type = ProfileMeta.MetaType.Information,
|
||||||
|
Column = 2,
|
||||||
|
Order = 0,
|
||||||
|
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM1"],
|
||||||
Sensitive = true
|
Sensitive = true
|
||||||
},
|
},
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = "Abdomen Ratio",
|
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 2",
|
||||||
Value = abdomenRatio,
|
Value = abdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||||
|
Type = ProfileMeta.MetaType.Information,
|
||||||
|
Column = 2,
|
||||||
|
Order = 1,
|
||||||
|
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM2"],
|
||||||
Sensitive = true
|
Sensitive = true
|
||||||
},
|
},
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = "Chest To Abdomen Ratio",
|
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 3",
|
||||||
Value = chestAbdomenRatio,
|
Value = chestAbdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||||
|
Type = ProfileMeta.MetaType.Information,
|
||||||
|
Column = 2,
|
||||||
|
Order = 2,
|
||||||
|
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM3"],
|
||||||
Sensitive = true
|
Sensitive = true
|
||||||
},
|
},
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = "Headshot Ratio",
|
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 4",
|
||||||
Value = headRatio,
|
Value = headRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||||
|
Type = ProfileMeta.MetaType.Information,
|
||||||
|
Column = 2,
|
||||||
|
Order = 3,
|
||||||
|
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM4"],
|
||||||
Sensitive = true
|
Sensitive = true
|
||||||
},
|
},
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = "Hit Offset Average",
|
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 5",
|
||||||
// todo: make sure this is wrapped somewhere else
|
// todo: make sure this is wrapped somewhere else
|
||||||
Value = $"{Math.Round(((float)hitOffsetAverage), 4).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))}°",
|
Value = $"{Math.Round(((float)hitOffsetAverage), 4).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))}°",
|
||||||
|
Type = ProfileMeta.MetaType.Information,
|
||||||
|
Column = 2,
|
||||||
|
Order = 4,
|
||||||
|
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM5"],
|
||||||
Sensitive = true
|
Sensitive = true
|
||||||
},
|
},
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = "Max Strain",
|
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 6",
|
||||||
Value = Math.Round(maxStrain, 3),
|
Value = Math.Round(maxStrain, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||||
|
Type = ProfileMeta.MetaType.Information,
|
||||||
|
Column = 2,
|
||||||
|
Order = 5,
|
||||||
|
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM6"],
|
||||||
Sensitive = true
|
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
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<List<ProfileMeta>> getMessages(int clientId)
|
async Task<List<ProfileMeta>> getMessages(int clientId, int offset, int count, DateTime? startAt)
|
||||||
{
|
{
|
||||||
|
if (count <= 1)
|
||||||
|
{
|
||||||
|
using (var ctx = new DatabaseContext(true))
|
||||||
|
{
|
||||||
|
return new List<ProfileMeta>
|
||||||
|
{
|
||||||
|
new ProfileMeta()
|
||||||
|
{
|
||||||
|
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGES"],
|
||||||
|
Value = (await ctx.Set<EFClientMessage>()
|
||||||
|
.CountAsync(_message => _message.ClientId == clientId))
|
||||||
|
.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||||
|
Column = 1,
|
||||||
|
Order= 4,
|
||||||
|
Type = ProfileMeta.MetaType.Information
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<ProfileMeta> messageMeta;
|
List<ProfileMeta> messageMeta;
|
||||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||||
{
|
{
|
||||||
var messages = ctx.Set<EFClientMessage>().Where(m => m.ClientId == clientId);
|
var messages = ctx.Set<EFClientMessage>()
|
||||||
|
.Where(m => m.ClientId == clientId)
|
||||||
|
.Where(_message => _message.TimeSent < startAt)
|
||||||
|
.OrderByDescending(_message => _message.TimeSent)
|
||||||
|
.Skip(offset)
|
||||||
|
.Take(count);
|
||||||
|
|
||||||
messageMeta = await messages.Select(m => new ProfileMeta()
|
messageMeta = await messages.Select(m => new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = "EventMessage",
|
Key = null,
|
||||||
Value = m.Message,
|
Value = new { m.Message, m.Server.GameName },
|
||||||
When = m.TimeSent,
|
When = m.TimeSent,
|
||||||
Extra = m.ServerId.ToString()
|
Extra = m.ServerId.ToString(),
|
||||||
|
Type = ProfileMeta.MetaType.ChatMessage
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
}
|
|
||||||
|
|
||||||
messageMeta.Add(new ProfileMeta()
|
foreach (var message in messageMeta)
|
||||||
{
|
{
|
||||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGES"],
|
if ((message.Value.Message as string).IsQuickMessage())
|
||||||
Value = messageMeta.Count
|
{
|
||||||
});
|
try
|
||||||
|
{
|
||||||
|
var quickMessages = ServerManager.GetApplicationSettings().Configuration()
|
||||||
|
.QuickMessages
|
||||||
|
.First(_qm => _qm.Game == message.Value.GameName);
|
||||||
|
message.Value = quickMessages.Messages[(message.Value.Message as string).Substring(1)];
|
||||||
|
message.Type = ProfileMeta.MetaType.QuickMessage;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
message.Value = message.Value.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message.Value = message.Value.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return messageMeta;
|
return messageMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
MetaService.AddMeta(getStats);
|
|
||||||
|
|
||||||
if (Config.Configuration().EnableAntiCheat)
|
if (Config.Configuration().EnableAntiCheat)
|
||||||
{
|
{
|
||||||
MetaService.AddMeta(getAnticheatInfo);
|
MetaService.AddRuntimeMeta(getAnticheatInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
MetaService.AddMeta(getMessages);
|
MetaService.AddRuntimeMeta(getStats);
|
||||||
|
MetaService.AddRuntimeMeta(getMessages);
|
||||||
|
|
||||||
string totalKills(Server server)
|
async Task<string> totalKills(Server server)
|
||||||
{
|
{
|
||||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||||
{
|
{
|
||||||
long kills = ctx.Set<EFServerStatistics>().Where(s => s.Active).Sum(s => s.TotalKills);
|
long kills = await ctx.Set<EFServerStatistics>().Where(s => s.Active).SumAsync(s => s.TotalKills);
|
||||||
return kills.ToString("#,##0");
|
return kills.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string totalPlayTime(Server server)
|
async Task<string> totalPlayTime(Server server)
|
||||||
{
|
{
|
||||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||||
{
|
{
|
||||||
long playTime = ctx.Set<EFServerStatistics>().Where(s => s.Active).Sum(s => s.TotalPlayTime);
|
long playTime = await ctx.Set<EFServerStatistics>().Where(s => s.Active).SumAsync(s => s.TotalPlayTime);
|
||||||
return (playTime / 3600.0).ToString("#,##0");
|
return (playTime / 3600.0).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string topStats(Server s)
|
async Task<string> topStats(Server s)
|
||||||
{
|
{
|
||||||
return String.Join(Environment.NewLine, Commands.TopStats.GetTopStats(s).Result);
|
return string.Join(Environment.NewLine, await Commands.TopStats.GetTopStats(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
string mostPlayed(Server s)
|
async Task<string> mostPlayed(Server s)
|
||||||
{
|
{
|
||||||
return String.Join(Environment.NewLine, Commands.MostPlayed.GetMostPlayed(s).Result);
|
return string.Join(Environment.NewLine, await Commands.MostPlayed.GetMostPlayed(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.GetMessageTokens().Add(new MessageToken("TOTALKILLS", totalKills));
|
manager.GetMessageTokens().Add(new MessageToken("TOTALKILLS", totalKills));
|
||||||
@ -325,7 +485,6 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
manager.GetMessageTokens().Add(new MessageToken("MOSTPLAYED", mostPlayed));
|
manager.GetMessageTokens().Add(new MessageToken("MOSTPLAYED", mostPlayed));
|
||||||
|
|
||||||
ServerManager = manager;
|
ServerManager = manager;
|
||||||
|
|
||||||
Manager = new StatManager(manager);
|
Manager = new StatManager(manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,5 +500,24 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
await Manager.Sync(sv);
|
await Manager.Sync(sv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if the event should be ignored
|
||||||
|
/// (If the client id or target id is not a real client or the target/origin is a bot and ignore bots is turned on)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="origin"></param>
|
||||||
|
/// <param name="target"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private bool ShouldIgnoreEvent(EFClient origin, EFClient target)
|
||||||
|
{
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
<RuntimeFrameworkVersion>2.1.5</RuntimeFrameworkVersion>
|
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||||
<ApplicationIcon />
|
<ApplicationIcon />
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
<PackageId>RaidMax.IW4MAdmin.Plugins.Stats</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Plugins.Stats</PackageId>
|
||||||
@ -13,29 +13,19 @@
|
|||||||
<Description>Client Statistics Plugin for IW4MAdmin</Description>
|
<Description>Client Statistics Plugin for IW4MAdmin</Description>
|
||||||
<Copyright>2018</Copyright>
|
<Copyright>2018</Copyright>
|
||||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||||
|
<LangVersion>7.1</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="Web\Views\Stats\_MessageContext.cshtml">
|
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
|
||||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
<Private>false</Private>
|
||||||
</Content>
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\..\WebfrontCore\WebfrontCore.csproj">
|
||||||
|
<Private>false</Private>
|
||||||
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
|
||||||
<ProjectReference Include="..\..\WebfrontCore\WebfrontCore.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.1.5" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
|
||||||
<Exec Command="xcopy /E /K /Y /C /I "$(ProjectDir)Web\Views" "$(SolutionDir)WebfrontCore\Views\Plugins"
xcopy /E /K /Y /C /I "$(ProjectDir)Web\wwwroot\images" "$(SolutionDir)WebfrontCore\wwwroot\images"" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
28
Plugins/Stats/ViewComponents/TopPlayersViewComponent.cs
Normal file
28
Plugins/Stats/ViewComponents/TopPlayersViewComponent.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using IW4MAdmin.Plugins.Stats;
|
||||||
|
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Stats.ViewComponents
|
||||||
|
{
|
||||||
|
public class TopPlayersViewComponent : ViewComponent
|
||||||
|
{
|
||||||
|
public async Task<IViewComponentResult> InvokeAsync(int count, int offset, long? serverId = null)
|
||||||
|
{
|
||||||
|
if (serverId == 0)
|
||||||
|
{
|
||||||
|
serverId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var server = Plugin.ServerManager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId);
|
||||||
|
|
||||||
|
if (server != null)
|
||||||
|
{
|
||||||
|
serverId = StatManager.GetIdForServer(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
return View("_List", await Plugin.Manager.GetTopStats(offset, count, serverId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,80 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using SharedLibraryCore;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using WebfrontCore.Controllers;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Web.Controllers
|
|
||||||
{
|
|
||||||
public class StatsController : BaseController
|
|
||||||
{
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> TopPlayersAsync()
|
|
||||||
{
|
|
||||||
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex.Set["WEBFRONT_STATS_INDEX_TITLE"];
|
|
||||||
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex.Set["WEBFRONT_STATS_INDEX_DESC"];
|
|
||||||
|
|
||||||
return View("Index", await Plugin.Manager.GetTopStats(0, 50));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> GetTopPlayersAsync(int count, int offset)
|
|
||||||
{
|
|
||||||
return View("_List", await Plugin.Manager.GetTopStats(offset, count));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> GetMessageAsync(int serverId, DateTime when)
|
|
||||||
{
|
|
||||||
var whenUpper = when.AddMinutes(5);
|
|
||||||
var whenLower = when.AddMinutes(-5);
|
|
||||||
|
|
||||||
using (var ctx = new SharedLibraryCore.Database.DatabaseContext(true))
|
|
||||||
{
|
|
||||||
var iqMessages = from message in ctx.Set<Models.EFClientMessage>()
|
|
||||||
where message.ServerId == serverId
|
|
||||||
where message.TimeSent >= whenLower
|
|
||||||
where message.TimeSent <= whenUpper
|
|
||||||
select new SharedLibraryCore.Dtos.ChatInfo()
|
|
||||||
{
|
|
||||||
ClientId = message.ClientId,
|
|
||||||
Message = message.Message,
|
|
||||||
Name = message.Client.CurrentAlias.Name,
|
|
||||||
Time = message.TimeSent
|
|
||||||
};
|
|
||||||
|
|
||||||
#if DEBUG == true
|
|
||||||
var messagesSql = iqMessages.ToSql();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
var messages = await iqMessages.ToListAsync();
|
|
||||||
|
|
||||||
return View("_MessageContext", messages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<IActionResult> GetAutomatedPenaltyInfoAsync(int clientId)
|
|
||||||
{
|
|
||||||
using (var ctx = new SharedLibraryCore.Database.DatabaseContext(true))
|
|
||||||
{
|
|
||||||
var penaltyInfo = await ctx.Set<Models.EFACSnapshot>()
|
|
||||||
.Where(s => s.ClientId == clientId)
|
|
||||||
.Include(s => s.LastStrainAngle)
|
|
||||||
.Include(s => s.HitOrigin)
|
|
||||||
.Include(s => s.HitDestination)
|
|
||||||
.Include(s => s.CurrentViewAngle)
|
|
||||||
.Include(s => s.PredictedViewAngles)
|
|
||||||
.OrderBy(s => s.When)
|
|
||||||
.ThenBy(s => s.Hits)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
return View("_PenaltyInfo", penaltyInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
|
|
||||||
<h4 class="pb-2 text-center ">@ViewBag.Title</h4>
|
|
||||||
|
|
||||||
<div id="stats_top_players" class="striped border-top border-bottom">
|
|
||||||
@await Html.PartialAsync("_List", Model)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@section scripts {
|
|
||||||
<environment include="Development">
|
|
||||||
<script type="text/javascript" src="~/js/loader.js"></script>
|
|
||||||
<script type="text/javascript" src="~/js/stats.js"></script>
|
|
||||||
</environment>
|
|
||||||
<script>initLoader('/Stats/GetTopPlayersAsync', '#stats_top_players', 50);</script>
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
@model IEnumerable<SharedLibraryCore.Dtos.ChatInfo>
|
|
||||||
@{
|
|
||||||
Layout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="client-message-context bg-dark p-2 mt-2 mb-2 border-top border-bottom">
|
|
||||||
<h5>@Model.First().Time.ToString()</h5>
|
|
||||||
@foreach (var message in Model)
|
|
||||||
{
|
|
||||||
<span class="text-white">@Html.ActionLink(@message.Name, "ProfileAsync", "Client", new { id = message.ClientId})</span><span> — @message.Message</span><br />
|
|
||||||
}
|
|
||||||
</div>
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user