Compare commits
107 Commits
2.0
...
2.3-prerel
Author | SHA1 | Date | |
---|---|---|---|
08c883e0ff | |||
aaf9eb09b6 | |||
7b75c35c9b | |||
07ec5cf52f | |||
cf5ee8765d | |||
9494a17997 | |||
5f4171ccf4 | |||
a10746d5ff | |||
8dca05a442 | |||
8aa0d204f4 | |||
12cf2e8247 | |||
b77bdbe793 | |||
4522992c0e | |||
9d6cbee69c | |||
abf0609e2e | |||
5ac8a55c72 | |||
9bdd7d1b8a | |||
ed83c4c011 | |||
d9d548ea18 | |||
1779bf821d | |||
d50e6c8030 | |||
a58726d872 | |||
dded60a6ef | |||
305817d00c | |||
65cf3566db | |||
e91d076b41 | |||
b5e9519f0c | |||
7caee55a7e | |||
b289917319 | |||
c8366a22e5 | |||
de902a58ac | |||
c7547f1ad5 | |||
9d946d1bad | |||
f4ac815d07 | |||
7fa0b52543 | |||
d45729d7e1 | |||
5d93e7ac57 | |||
0f9d2e92e1 | |||
4a46abc46d | |||
7c708f06f3 | |||
98adfb12d2 | |||
a786541484 | |||
b9086fd145 | |||
3d8108f339 | |||
39596db56e | |||
ba5b1e19a6 | |||
385879618d | |||
0c90d02e44 | |||
cfbacabb4a | |||
672d45df7c | |||
20d4ab27d3 | |||
e77ef69ee8 | |||
cc7628d058 | |||
46bdc2ac33 | |||
bbefd53db4 | |||
56cb8c50e7 | |||
0538d9f479 | |||
1343d4959e | |||
ac64d8d3c1 | |||
b5939bbdaf | |||
a0fafe5797 | |||
bbade07646 | |||
3c0e101f14 | |||
d0be08629d | |||
9d00d5a16a | |||
396e5c9215 | |||
4ec16d3aa2 | |||
f40bcce44f | |||
6071ad8653 | |||
16d7ccd590 | |||
87541c4a5a | |||
af6361144e | |||
454238a192 | |||
e7c7145da1 | |||
5be6b75ccf | |||
e60f612f95 | |||
ba023ceeb5 | |||
e3dba96d72 | |||
6d0f859a93 | |||
696e2d12c9 | |||
bf68e5672f | |||
2204686b08 | |||
897ec0d0c1 | |||
d9a601328c | |||
36d493f05b | |||
be68335f70 | |||
4d585e6ab2 | |||
4006c09045 | |||
699c19cd4b | |||
6e5501b32d | |||
e964013700 | |||
9ff7f39e8d | |||
a54ea3913d | |||
e8dff01c41 | |||
3092a529e9 | |||
f442f251f6 | |||
3a463be7f8 | |||
35e7f57156 | |||
8071fb37bc | |||
bb90a807b7 | |||
2c2c442ba7 | |||
b6c979beba | |||
99390f1f35 | |||
ece519251a | |||
0e3d280595 | |||
5dfaa4ebd6 | |||
02ef5a0bf8 |
13
.gitignore
vendored
13
.gitignore
vendored
@ -220,7 +220,16 @@ Thumbs.db
|
|||||||
DEPLOY
|
DEPLOY
|
||||||
global.min.css
|
global.min.css
|
||||||
global.min.js
|
global.min.js
|
||||||
bootstrap-custom.css
|
|
||||||
bootstrap-custom.min.css
|
bootstrap-custom.min.css
|
||||||
**/Master/static
|
**/Master/static
|
||||||
**/Master/dev_env
|
**/Master/dev_env
|
||||||
|
/WebfrontCore/Views/Plugins/Stats
|
||||||
|
/WebfrontCore/wwwroot/**/dds
|
||||||
|
|
||||||
|
/DiscordWebhook/env
|
||||||
|
/DiscordWebhook/config.dev.json
|
||||||
|
/GameLogServer/env
|
||||||
|
launchSettings.json
|
||||||
|
/VpnDetectionPrivate.js
|
||||||
|
/Plugins/ScriptPlugins/VpnDetectionPrivate.js
|
||||||
|
**/Master/env_master
|
@ -1,75 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Dtos;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.API
|
|
||||||
{
|
|
||||||
class EventApi : IEventApi
|
|
||||||
{
|
|
||||||
Queue<EventInfo> Events = new Queue<EventInfo>();
|
|
||||||
DateTime LastFlagEvent;
|
|
||||||
static string[] FlaggedMessageContains =
|
|
||||||
{
|
|
||||||
" wh ",
|
|
||||||
"hax",
|
|
||||||
"cheat",
|
|
||||||
" hack ",
|
|
||||||
"aim",
|
|
||||||
"wall",
|
|
||||||
"cheto",
|
|
||||||
"hak",
|
|
||||||
"bot"
|
|
||||||
};
|
|
||||||
int FlaggedMessageCount;
|
|
||||||
|
|
||||||
public Queue<EventInfo> GetEvents() => Events;
|
|
||||||
|
|
||||||
public void OnServerEvent(object sender, GameEvent E)
|
|
||||||
{
|
|
||||||
if (E.Type == GameEvent.EventType.Say && E.Origin.Level < Player.Permission.Trusted)
|
|
||||||
{
|
|
||||||
bool flaggedMessage = false;
|
|
||||||
foreach (string msg in FlaggedMessageContains)
|
|
||||||
flaggedMessage = flaggedMessage ? flaggedMessage : E.Data.ToLower().Contains(msg);
|
|
||||||
|
|
||||||
if (flaggedMessage)
|
|
||||||
FlaggedMessageCount++;
|
|
||||||
|
|
||||||
if (FlaggedMessageCount > 3)
|
|
||||||
{
|
|
||||||
if (Events.Count > 20)
|
|
||||||
Events.Dequeue();
|
|
||||||
|
|
||||||
FlaggedMessageCount = 0;
|
|
||||||
|
|
||||||
E.Owner.Broadcast("If you suspect someone of ^5CHEATING ^7use the ^5!report ^7command").Wait();
|
|
||||||
Events.Enqueue(new EventInfo(
|
|
||||||
EventInfo.EventType.ALERT,
|
|
||||||
EventInfo.EventVersion.IW4MAdmin,
|
|
||||||
"Chat indicates there may be a cheater",
|
|
||||||
"Alert",
|
|
||||||
E.Owner.Hostname, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((DateTime.UtcNow - LastFlagEvent).Minutes >= 3)
|
|
||||||
{
|
|
||||||
FlaggedMessageCount = 0;
|
|
||||||
LastFlagEvent = DateTime.Now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Report)
|
|
||||||
{
|
|
||||||
Events.Enqueue(new EventInfo(
|
|
||||||
EventInfo.EventType.ALERT,
|
|
||||||
EventInfo.EventVersion.IW4MAdmin,
|
|
||||||
$"**{E.Origin.Name}** has reported **{E.Target.Name}** for: {E.Data.Trim()}",
|
|
||||||
E.Target.Name, E.Origin.Name, ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
12
Application/API/GameLogServer/IGameLogServer.cs
Normal file
12
Application/API/GameLogServer/IGameLogServer.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using RestEase;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.API.GameLogServer
|
||||||
|
{
|
||||||
|
[Header("User-Agent", "IW4MAdmin-RestEase")]
|
||||||
|
public interface IGameLogServer
|
||||||
|
{
|
||||||
|
[Get("log/{path}")]
|
||||||
|
Task<LogInfo> Log([Path] string path);
|
||||||
|
}
|
||||||
|
}
|
17
Application/API/GameLogServer/LogInfo.cs
Normal file
17
Application/API/GameLogServer/LogInfo.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.API.GameLogServer
|
||||||
|
{
|
||||||
|
public class LogInfo
|
||||||
|
{
|
||||||
|
[JsonProperty("success")]
|
||||||
|
public bool Success { get; set; }
|
||||||
|
[JsonProperty("length")]
|
||||||
|
public int Length { get; set; }
|
||||||
|
[JsonProperty("data")]
|
||||||
|
public string Data { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -8,9 +8,13 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
public class ApiServer
|
public class ApiServer
|
||||||
{
|
{
|
||||||
[JsonProperty("id")]
|
[JsonProperty("id")]
|
||||||
public int Id { get; set; }
|
public long Id { get; set; }
|
||||||
|
[JsonProperty("ip")]
|
||||||
|
public string IPAddress { get; set; }
|
||||||
[JsonProperty("port")]
|
[JsonProperty("port")]
|
||||||
public short Port { get; set; }
|
public short Port { get; set; }
|
||||||
|
[JsonProperty("version")]
|
||||||
|
public string Version { get; set; }
|
||||||
[JsonProperty("gametype")]
|
[JsonProperty("gametype")]
|
||||||
public string Gametype { get; set; }
|
public string Gametype { get; set; }
|
||||||
[JsonProperty("map")]
|
[JsonProperty("map")]
|
||||||
|
@ -8,9 +8,13 @@ using SharedLibraryCore;
|
|||||||
|
|
||||||
namespace IW4MAdmin.Application.API.Master
|
namespace IW4MAdmin.Application.API.Master
|
||||||
{
|
{
|
||||||
|
public class HeartbeatState
|
||||||
|
{
|
||||||
|
public bool Connected { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class Heartbeat
|
public class Heartbeat
|
||||||
{
|
{
|
||||||
|
|
||||||
public static async Task Send(ApplicationManager mgr, bool firstHeartbeat = false)
|
public static async Task Send(ApplicationManager mgr, bool firstHeartbeat = false)
|
||||||
{
|
{
|
||||||
var api = Endpoint.Get();
|
var api = Endpoint.Get();
|
||||||
@ -35,12 +39,14 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
{
|
{
|
||||||
ClientNum = s.ClientNum,
|
ClientNum = s.ClientNum,
|
||||||
Game = s.GameName.ToString(),
|
Game = s.GameName.ToString(),
|
||||||
|
Version = s.Version,
|
||||||
Gametype = s.Gametype,
|
Gametype = s.Gametype,
|
||||||
Hostname = s.Hostname,
|
Hostname = s.Hostname,
|
||||||
Map = s.CurrentMap.Name,
|
Map = s.CurrentMap.Name,
|
||||||
MaxClientNum = s.MaxClients,
|
MaxClientNum = s.MaxClients,
|
||||||
Id = s.GetHashCode(),
|
Id = s.EndPoint,
|
||||||
Port = (short)s.GetPort()
|
Port = (short)s.GetPort(),
|
||||||
|
IPAddress = s.IP
|
||||||
}).ToList()
|
}).ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ using RestEase;
|
|||||||
namespace IW4MAdmin.Application.API.Master
|
namespace IW4MAdmin.Application.API.Master
|
||||||
{
|
{
|
||||||
public class AuthenticationId
|
public class AuthenticationId
|
||||||
{
|
{
|
||||||
[JsonProperty("id")]
|
[JsonProperty("id")]
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
public class Endpoint
|
public class Endpoint
|
||||||
{
|
{
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
private static IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
|
private static readonly IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
|
||||||
#else
|
#else
|
||||||
private static IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
|
private static IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
|
||||||
#endif
|
#endif
|
||||||
@ -60,5 +60,11 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
|
|
||||||
[Get("version")]
|
[Get("version")]
|
||||||
Task<VersionInfo> GetVersion();
|
Task<VersionInfo> GetVersion();
|
||||||
|
|
||||||
|
[Get("localization")]
|
||||||
|
Task<List<SharedLibraryCore.Localization.Layout>> GetLocalization();
|
||||||
|
|
||||||
|
[Get("localization/{languageTag}")]
|
||||||
|
Task<SharedLibraryCore.Localization.Layout> GetLocalization([Path("languageTag")] string languageTag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,16 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<RuntimeFrameworkVersion>2.1.5</RuntimeFrameworkVersion>
|
||||||
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
|
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
|
||||||
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
|
||||||
<Version>2.1.0</Version>
|
<Version>2.2.3.2</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 server</Description>
|
||||||
<Copyright>2018</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>
|
||||||
<RepositoryUrl>https://github.com/RaidMax/IW4M-Admin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/RaidMax/IW4M-Admin</RepositoryUrl>
|
||||||
@ -19,13 +20,21 @@
|
|||||||
<AssemblyName>IW4MAdmin</AssemblyName>
|
<AssemblyName>IW4MAdmin</AssemblyName>
|
||||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||||
<Win32Resource />
|
<Win32Resource />
|
||||||
|
<RootNamespace>IW4MAdmin.Application</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="RestEase" Version="1.4.5" />
|
<PackageReference Include="RestEase" Version="1.4.7" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.4.0" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||||
|
<TieredCompilation>true</TieredCompilation>
|
||||||
|
<AssemblyVersion>2.2.3.2</AssemblyVersion>
|
||||||
|
<FileVersion>2.2.3.2</FileVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj">
|
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj">
|
||||||
<Private>true</Private>
|
<Private>true</Private>
|
||||||
@ -58,18 +67,33 @@
|
|||||||
<None Update="Localization\IW4MAdmin.en-US.json">
|
<None Update="Localization\IW4MAdmin.en-US.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</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">
|
||||||
<Exec Command="call $(ProjectDir)BuildScripts\PreBuild.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
|
<Exec Command="call $(ProjectDir)BuildScripts\PreBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
<Exec Command="call $(ProjectDir)BuildScripts\PostBuild.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
|
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
|
||||||
|
<Output TaskParameter="Assemblies" ItemName="CurrentAssembly" />
|
||||||
|
</GetAssemblyIdentity>
|
||||||
|
<Exec Command="call $(ProjectDir)BuildScripts\PostBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir) %(CurrentAssembly.Version)" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="PostPublish" AfterTargets="Publish">
|
<Target Name="PostPublish" AfterTargets="Publish">
|
||||||
<Exec Command="call $(ProjectDir)BuildScripts\PostPublish.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
|
<Exec Command="call $(ProjectDir)BuildScripts\PostPublish.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(ConfigurationName)" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -2,6 +2,9 @@ set SolutionDir=%1
|
|||||||
set ProjectDir=%2
|
set ProjectDir=%2
|
||||||
set TargetDir=%3
|
set TargetDir=%3
|
||||||
set OutDir=%4
|
set OutDir=%4
|
||||||
|
set Version=%5
|
||||||
|
|
||||||
|
echo %Version% > "%SolutionDir%DEPLOY\version.txt"
|
||||||
|
|
||||||
echo Copying dependency configs
|
echo Copying dependency configs
|
||||||
copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%"
|
copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%"
|
||||||
@ -15,4 +18,17 @@ if not exist "%TargetDir%Plugins" (
|
|||||||
xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
|
xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
|
||||||
|
|
||||||
echo Copying plugins for publish
|
echo Copying plugins for publish
|
||||||
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\Windows\Plugins\"
|
del %SolutionDir%BUILD\Plugins\Tests.dll
|
||||||
|
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\Windows\Plugins\"
|
||||||
|
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
|
||||||
|
|
||||||
|
echo Copying script plugins for publish
|
||||||
|
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\Windows\Plugins\"
|
||||||
|
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
|
||||||
|
|
||||||
|
echo Copying GSC files for publish
|
||||||
|
xcopy /Y "%SolutionDir%_customcallbacks.gsc" "%SolutionDir%Publish\Windows\userraw\scripts\"
|
||||||
|
xcopy /Y "%SolutionDir%_customcallbacks.gsc" "%SolutionDir%Publish\WindowsPrerelease\userraw\scripts\"
|
||||||
|
|
||||||
|
xcopy /Y "%SolutionDir%_commands.gsc" "%SolutionDir%Publish\Windows\userraw\scripts\"
|
||||||
|
xcopy /Y "%SolutionDir%_commands.gsc" "%SolutionDir%Publish\WindowsPrerelease\userraw\scripts\"
|
@ -1,8 +1,12 @@
|
|||||||
set SolutionDir=%1
|
set SolutionDir=%1
|
||||||
set ProjectDir=%2
|
set ProjectDir=%2
|
||||||
set TargetDir=%3
|
set TargetDir=%3
|
||||||
|
set CurrentConfiguration=%4
|
||||||
|
SET COPYCMD=/Y
|
||||||
|
|
||||||
echo Deleting extra language files
|
echo Deleting extra language files
|
||||||
|
|
||||||
|
if exist "%SolutionDir%Publish\Windows\en-US\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\en-US'
|
||||||
if exist "%SolutionDir%Publish\Windows\de\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\de'
|
if exist "%SolutionDir%Publish\Windows\de\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\de'
|
||||||
if exist "%SolutionDir%Publish\Windows\es\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\es'
|
if exist "%SolutionDir%Publish\Windows\es\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\es'
|
||||||
if exist "%SolutionDir%Publish\Windows\fr\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\fr'
|
if exist "%SolutionDir%Publish\Windows\fr\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\fr'
|
||||||
@ -13,6 +17,17 @@ if exist "%SolutionDir%Publish\Windows\ru\" powershell Remove-Item -Force -Recur
|
|||||||
if exist "%SolutionDir%Publish\Windows\zh-Hans\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\zh-Hans'
|
if exist "%SolutionDir%Publish\Windows\zh-Hans\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\zh-Hans'
|
||||||
if exist "%SolutionDir%Publish\Windows\zh-Hant\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\zh-Hant'
|
if exist "%SolutionDir%Publish\Windows\zh-Hant\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\zh-Hant'
|
||||||
|
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\en-US\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\en-US'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\de\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\de'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\es\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\es'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\fr\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\fr'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\it\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\it'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\ja\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ja'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\ko\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ko'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\ru\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\ru'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\zh-Hans\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\zh-Hans'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\zh-Hant\" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\zh-Hant'
|
||||||
|
|
||||||
echo Deleting extra runtime files
|
echo Deleting extra runtime files
|
||||||
if exist "%SolutionDir%Publish\Windows\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-arm'
|
if exist "%SolutionDir%Publish\Windows\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-arm'
|
||||||
if exist "%SolutionDir%Publish\Windows\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-arm64'
|
if exist "%SolutionDir%Publish\Windows\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\linux-arm64'
|
||||||
@ -24,6 +39,69 @@ if exist "%SolutionDir%Publish\Windows\runtimes\osx-x64" powershell Remove-Item
|
|||||||
if exist "%SolutionDir%Publish\Windows\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\win-arm'
|
if exist "%SolutionDir%Publish\Windows\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\win-arm'
|
||||||
if exist "%SolutionDir%Publish\Windows\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\win-arm64'
|
if exist "%SolutionDir%Publish\Windows\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\Windows\runtimes\win-arm64'
|
||||||
|
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-arm64'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-armel" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\linux-armel'
|
||||||
|
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\osx" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\osx'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\osx-x64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\osx-x64'
|
||||||
|
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm'
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm64" powershell Remove-Item -Force -Recurse '%SolutionDir%Publish\WindowsPrerelease\runtimes\win-arm64'
|
||||||
|
|
||||||
echo Deleting misc files
|
echo Deleting misc files
|
||||||
if exist "%SolutionDir%Publish\Windows\web.config" del "%SolutionDir%Publish\Windows\web.config"
|
if exist "%SolutionDir%Publish\Windows\web.config" del "%SolutionDir%Publish\Windows\web.config"
|
||||||
del "%SolutionDir%Publish\Windows\*pdb"
|
del "%SolutionDir%Publish\Windows\*pdb"
|
||||||
|
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\web.config" del "%SolutionDir%Publish\WindowsPrerelease\web.config"
|
||||||
|
del "%SolutionDir%Publish\WindowsPrerelease\*pdb"
|
||||||
|
|
||||||
|
echo setting up library folders
|
||||||
|
|
||||||
|
if "%CurrentConfiguration%" == "Prerelease" (
|
||||||
|
echo PR-Config
|
||||||
|
if not exist "%SolutionDir%Publish\WindowsPrerelease\Configuration" md "%SolutionDir%Publish\WindowsPrerelease\Configuration"
|
||||||
|
move "%SolutionDir%Publish\WindowsPrerelease\DefaultSettings.json" "%SolutionDir%Publish\WindowsPrerelease\Configuration\"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%CurrentConfiguration%" == "Release" (
|
||||||
|
echo R-Config
|
||||||
|
if not exist "%SolutionDir%Publish\Windows\Configuration" md "%SolutionDir%Publish\Windows\Configuration"
|
||||||
|
if exist "%SolutionDir%Publish\Windows\DefaultSettings.json" move "%SolutionDir%Publish\Windows\DefaultSettings.json" "%SolutionDir%Publish\Windows\Configuration\DefaultSettings.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%CurrentConfiguration%" == "Prerelease" (
|
||||||
|
echo PR-LIB
|
||||||
|
if not exist "%SolutionDir%Publish\WindowsPrerelease\Lib\" md "%SolutionDir%Publish\WindowsPrerelease\Lib\"
|
||||||
|
move "%SolutionDir%Publish\WindowsPrerelease\*.dll" "%SolutionDir%Publish\WindowsPrerelease\Lib\"
|
||||||
|
move "%SolutionDir%Publish\WindowsPrerelease\*.json" "%SolutionDir%Publish\WindowsPrerelease\Lib\"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%CurrentConfiguration%" == "Release" (
|
||||||
|
echo R-LIB
|
||||||
|
if not exist "%SolutionDir%Publish\Windows\Lib\" md "%SolutionDir%Publish\Windows\Lib\"
|
||||||
|
move "%SolutionDir%Publish\Windows\*.dll" "%SolutionDir%Publish\Windows\Lib\"
|
||||||
|
move "%SolutionDir%Publish\Windows\*.json" "%SolutionDir%Publish\Windows\Lib\"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%CurrentConfiguration%" == "Prerelease" (
|
||||||
|
echo PR-RT
|
||||||
|
move "%SolutionDir%Publish\WindowsPrerelease\runtimes" "%SolutionDir%Publish\WindowsPrerelease\Lib\runtimes"
|
||||||
|
if exist "%SolutionDir%Publish\WindowsPrerelease\refs" move "%SolutionDir%Publish\WindowsPrerelease\refs" "%SolutionDir%Publish\WindowsPrerelease\Lib\refs"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if "%CurrentConfiguration%" == "Release" (
|
||||||
|
echo R-RT
|
||||||
|
move "%SolutionDir%Publish\Windows\runtimes" "%SolutionDir%Publish\Windows\Lib\runtimes"
|
||||||
|
if exist "%SolutionDir%Publish\Windows\refs" move "%SolutionDir%Publish\Windows\refs" "%SolutionDir%Publish\Windows\Lib\refs"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo making start scripts
|
||||||
|
@(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 #!/bin/bash && echo dotnet Lib\IW4MAdmin.dll) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.sh"
|
||||||
|
@(echo #!/bin/bash && echo dotnet Lib\IW4MAdmin.dll) > "%SolutionDir%Publish\Windows\StartIW4MAdmin.sh"
|
||||||
|
|
||||||
|
eCHO "%CurrentConfiguration%"
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Configuration;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
|
||||||
{
|
|
||||||
class ConfigurationGenerator
|
|
||||||
{
|
|
||||||
public static List<ServerConfiguration> GenerateServerConfig(List<ServerConfiguration> configList)
|
|
||||||
{
|
|
||||||
|
|
||||||
var loc = Utilities.CurrentLocalization.LocalizationSet;
|
|
||||||
var newConfig = new ServerConfiguration();
|
|
||||||
|
|
||||||
while (string.IsNullOrEmpty(newConfig.IPAddress))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string input = Utilities.PromptString(loc["SETUP_SERVER_IP"]);
|
|
||||||
IPAddress.Parse(input);
|
|
||||||
newConfig.IPAddress = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (newConfig.Port == 0)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
newConfig.Port = Int16.Parse(Utilities.PromptString(loc["SETUP_SERVER_PORT"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newConfig.Password = Utilities.PromptString(loc["SETUP_SERVER_RCON"]);
|
|
||||||
newConfig.AutoMessages = new List<string>();
|
|
||||||
newConfig.Rules = new List<string>();
|
|
||||||
|
|
||||||
newConfig.UseT6MParser = Utilities.PromptBool(loc["SETUP_SERVER_USET6M"]);
|
|
||||||
|
|
||||||
configList.Add(newConfig);
|
|
||||||
|
|
||||||
if (Utilities.PromptBool(loc["SETUP_SERVER_SAVE"]))
|
|
||||||
GenerateServerConfig(configList);
|
|
||||||
|
|
||||||
return configList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,6 +3,7 @@
|
|||||||
"AutoMessages": [
|
"AutoMessages": [
|
||||||
"This server uses ^5IW4M Admin v{{VERSION}} ^7get it at ^5raidmax.org/IW4MAdmin",
|
"This server uses ^5IW4M Admin v{{VERSION}} ^7get it at ^5raidmax.org/IW4MAdmin",
|
||||||
"^5IW4M Admin ^7sees ^5YOU!",
|
"^5IW4M Admin ^7sees ^5YOU!",
|
||||||
|
"{{TOPSTATS}}",
|
||||||
"This server has seen a total of ^5{{TOTALPLAYERS}} ^7players!",
|
"This server has seen a total of ^5{{TOTALPLAYERS}} ^7players!",
|
||||||
"Cheaters are ^1unwelcome ^7 on this server",
|
"Cheaters are ^1unwelcome ^7 on this server",
|
||||||
"Did you know 8/10 people agree with unverified statistics?"
|
"Did you know 8/10 people agree with unverified statistics?"
|
||||||
@ -24,11 +25,6 @@
|
|||||||
"Name": "mp_rust"
|
"Name": "mp_rust"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
"Alias": "Highrise",
|
|
||||||
"Name": "mp_highrise"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Terminal",
|
"Alias": "Terminal",
|
||||||
"Name": "mp_terminal"
|
"Name": "mp_terminal"
|
||||||
@ -39,16 +35,6 @@
|
|||||||
"Name": "mp_crash"
|
"Name": "mp_crash"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
"Alias": "Skidrow",
|
|
||||||
"Name": "mp_nightshift"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"Alias": "Quarry",
|
|
||||||
"Name": "mp_quarry"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Afghan",
|
"Alias": "Afghan",
|
||||||
"Name": "mp_afghan"
|
"Name": "mp_afghan"
|
||||||
@ -170,7 +156,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "IW4 Credits",
|
"Alias": "Test map",
|
||||||
"Name": "iw4_credits"
|
"Name": "iw4_credits"
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -189,6 +175,11 @@
|
|||||||
"Name": "mp_cargoship_sh"
|
"Name": "mp_cargoship_sh"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"Alias": "Cargoship",
|
||||||
|
"Name": "mp_cargoship"
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Shipment",
|
"Alias": "Shipment",
|
||||||
"Name": "mp_shipment"
|
"Name": "mp_shipment"
|
||||||
@ -215,23 +206,43 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Favela - Tropical",
|
"Alias": "Tropical Favela",
|
||||||
"Name": "mp_fav_tropical"
|
"Name": "mp_fav_tropical"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Estate - Tropical",
|
"Alias": "Tropical Estate",
|
||||||
"Name": "mp_estate_tropical"
|
"Name": "mp_estate_tropical"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Crash - Tropical",
|
"Alias": "Tropical Crash",
|
||||||
"Name": "mp_crash_tropical"
|
"Name": "mp_crash_tropical"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Forgotten City",
|
"Alias": "Forgotten City",
|
||||||
"Name": "mp_bloc_sh"
|
"Name": "mp_bloc_sh"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"Alias": "Crossfire",
|
||||||
|
"Name": "mp_cross_fire"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"Alias": "Bloc",
|
||||||
|
"Name": "mp_bloc"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"Alias": "Oilrig",
|
||||||
|
"Name": "oilrig"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"Name": "Village",
|
||||||
|
"Alias": "co_hunted"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
11
Application/EventParsers/IW3EventParser.cs
Normal file
11
Application/EventParsers/IW3EventParser.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
|
{
|
||||||
|
class IW3EventParser : IW4EventParser
|
||||||
|
{
|
||||||
|
public override string GetGameDir() => "main";
|
||||||
|
}
|
||||||
|
}
|
@ -1,110 +1,231 @@
|
|||||||
using System;
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
|
|
||||||
namespace Application.EventParsers
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
{
|
{
|
||||||
class IW4EventParser : IEventParser
|
class IW4EventParser : IEventParser
|
||||||
{
|
{
|
||||||
public GameEvent GetEvent(Server server, string logLine)
|
private const string SayRegex = @"(say|sayteam);(.{1,32});([0-9]+)(.*);(.*)";
|
||||||
{
|
|
||||||
string[] lineSplit = logLine.Split(';');
|
|
||||||
string cleanedEventLine = Regex.Replace(lineSplit[0], @"[0-9]+:[0-9]+\ ", "").Trim();
|
|
||||||
|
|
||||||
if (cleanedEventLine[0] == 'K')
|
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, SayRegex);
|
||||||
|
|
||||||
|
if (matchResult.Success)
|
||||||
|
{
|
||||||
|
string message = matchResult.Groups[5].ToString()
|
||||||
|
.Replace("\x15", "")
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
var origin = server.GetClientsAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2));
|
||||||
|
|
||||||
|
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)
|
if (!server.CustomCallback)
|
||||||
{
|
{
|
||||||
|
var origin = server.GetClientsAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6));
|
||||||
|
var target = server.GetClientsAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2));
|
||||||
|
|
||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Script,
|
Type = GameEvent.EventType.Kill,
|
||||||
Data = logLine,
|
Data = logLine,
|
||||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
|
Origin = origin,
|
||||||
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
Target = target,
|
||||||
Owner = server
|
Owner = server
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanedEventLine == "say" || cleanedEventLine == "sayteam")
|
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()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Say,
|
Type = GameEvent.EventType.ScriptKill,
|
||||||
Data = lineSplit[4].Replace("\x15", ""),
|
|
||||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
|
||||||
Owner = server,
|
|
||||||
Message = lineSplit[4].Replace("\x15", "")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanedEventLine.Contains("ScriptKill"))
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Script,
|
|
||||||
Data = logLine,
|
Data = logLine,
|
||||||
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
|
Origin = origin,
|
||||||
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong()),
|
Target = target,
|
||||||
Owner = server
|
Owner = server
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanedEventLine.Contains("ExitLevel"))
|
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)
|
||||||
|
{
|
||||||
|
if (Regex.Match(eventType, @"^(D);((?:bot[0-9]+)|(?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$").Success)
|
||||||
|
{
|
||||||
|
var origin = server.GetClientsAsList().First(c => c.NetworkId == lineSplit[5].ConvertLong());
|
||||||
|
var target = server.GetClientsAsList().First(c => c.NetworkId == lineSplit[1].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, @"^(J;)(.{1,32});([0-9]+);(.*)$");
|
||||||
|
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[4].ToString().StripColors(),
|
||||||
|
},
|
||||||
|
NetworkId = regexMatch.Groups[2].ToString().ConvertLong(),
|
||||||
|
ClientNumber = Convert.ToInt32(regexMatch.Groups[3].ToString()),
|
||||||
|
State = EFClient.ClientState.Connecting,
|
||||||
|
CurrentServer = server
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType == "Q")
|
||||||
|
{
|
||||||
|
var regexMatch = Regex.Match(logLine, @"^(Q;)(.{1,32});([0-9]+);(.*)$");
|
||||||
|
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[4].ToString().StripColors()
|
||||||
|
},
|
||||||
|
NetworkId = regexMatch.Groups[2].ToString().ConvertLong(),
|
||||||
|
ClientNumber = Convert.ToInt32(regexMatch.Groups[3].ToString()),
|
||||||
|
State = EFClient.ClientState.Disconnecting
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType.Contains("ExitLevel"))
|
||||||
{
|
{
|
||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.MapEnd,
|
Type = GameEvent.EventType.MapEnd,
|
||||||
Data = lineSplit[0],
|
Data = lineSplit[0],
|
||||||
Origin = new Player()
|
Origin = Utilities.IW4MAdminClient(server),
|
||||||
{
|
Target = Utilities.IW4MAdminClient(server),
|
||||||
ClientId = 1
|
|
||||||
},
|
|
||||||
Target = new Player()
|
|
||||||
{
|
|
||||||
ClientId = 1
|
|
||||||
},
|
|
||||||
Owner = server
|
Owner = server
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanedEventLine.Contains("InitGame"))
|
if (eventType.Contains("InitGame"))
|
||||||
{
|
{
|
||||||
|
string dump = eventType.Replace("InitGame: ", "");
|
||||||
|
|
||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.MapChange,
|
Type = GameEvent.EventType.MapChange,
|
||||||
Data = lineSplit[0],
|
Data = lineSplit[0],
|
||||||
Origin = new Player()
|
Origin = Utilities.IW4MAdminClient(server),
|
||||||
{
|
Target = Utilities.IW4MAdminClient(server),
|
||||||
ClientId = 1
|
Owner = server,
|
||||||
},
|
Extra = dump.DictionaryFromKeyValue()
|
||||||
Target = new Player()
|
|
||||||
{
|
|
||||||
ClientId = 1
|
|
||||||
},
|
|
||||||
Owner = server
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GameEvent()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Unknown,
|
Type = GameEvent.EventType.Unknown,
|
||||||
Origin = new Player()
|
Origin = Utilities.IW4MAdminClient(server),
|
||||||
{
|
Target = Utilities.IW4MAdminClient(server),
|
||||||
ClientId = 1
|
|
||||||
},
|
|
||||||
Target = new Player()
|
|
||||||
{
|
|
||||||
ClientId = 1
|
|
||||||
},
|
|
||||||
Owner = server
|
Owner = server
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// other parsers can derive from this parser so we make it virtual
|
// other parsers can derive from this parser so we make it virtual
|
||||||
public virtual string GetGameDir() => "userraw";
|
public virtual string GetGameDir()
|
||||||
|
{
|
||||||
|
return "userraw";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,58 @@
|
|||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Text.RegularExpressions;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Application.EventParsers
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
{
|
{
|
||||||
class IW5EventParser : IW4EventParser
|
class IW5EventParser : IW4EventParser
|
||||||
{
|
{
|
||||||
public override string GetGameDir() => "rzodemo";
|
public override string GetGameDir()
|
||||||
|
{
|
||||||
|
return "logs";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override GameEvent GetEvent(Server server, string logLine)
|
||||||
|
{
|
||||||
|
string cleanedEventLine = Regex.Replace(logLine, @"[0-9]+:[0-9]+\ ", "").Trim();
|
||||||
|
|
||||||
|
if (cleanedEventLine.Contains("J;"))
|
||||||
|
{
|
||||||
|
string[] lineSplit = cleanedEventLine.Split(';');
|
||||||
|
|
||||||
|
int clientNum = Int32.Parse(lineSplit[2]);
|
||||||
|
|
||||||
|
var player = new EFClient()
|
||||||
|
{
|
||||||
|
NetworkId = lineSplit[1].ConvertLong(),
|
||||||
|
ClientNumber = clientNum,
|
||||||
|
CurrentAlias = new EFAlias()
|
||||||
|
{
|
||||||
|
Active = false,
|
||||||
|
Name = lineSplit[3]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new GameEvent()
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.PreConnect,
|
||||||
|
Origin = new EFClient()
|
||||||
|
{
|
||||||
|
ClientId = 1
|
||||||
|
},
|
||||||
|
Target = new EFClient()
|
||||||
|
{
|
||||||
|
ClientId = 1
|
||||||
|
},
|
||||||
|
Owner = server,
|
||||||
|
Extra = player
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return base.GetEvent(server, logLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
Application/EventParsers/T5MEventParser.cs
Normal file
11
Application/EventParsers/T5MEventParser.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
|
{
|
||||||
|
class T5MEventParser : IW4EventParser
|
||||||
|
{
|
||||||
|
public override string GetGameDir() => "v2";
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@ -6,107 +7,10 @@ using SharedLibraryCore;
|
|||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Objects;
|
||||||
|
|
||||||
namespace Application.EventParsers
|
namespace IW4MAdmin.Application.EventParsers
|
||||||
{
|
{
|
||||||
class T6MEventParser : IEventParser
|
class T6MEventParser : IW4EventParser
|
||||||
{
|
{
|
||||||
public GameEvent GetEvent(Server server, string logLine)
|
public override string GetGameDir() => $"t6r{Path.DirectorySeparatorChar}data";
|
||||||
{
|
|
||||||
string cleanedLogLine = Regex.Replace(logLine, @"^ *[0-9]+:[0-9]+ *", "");
|
|
||||||
string[] lineSplit = cleanedLogLine.Split(';');
|
|
||||||
|
|
||||||
if (lineSplit[0][0] == 'K')
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Script,
|
|
||||||
Data = cleanedLogLine,
|
|
||||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
|
|
||||||
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lineSplit[0][0] == 'D')
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Damage,
|
|
||||||
Data = cleanedLogLine,
|
|
||||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
|
|
||||||
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lineSplit[0] == "say" || lineSplit[0] == "sayteam")
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Say,
|
|
||||||
Data = lineSplit[4],
|
|
||||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
|
||||||
Owner = server,
|
|
||||||
Message = lineSplit[4]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lineSplit[0].Contains("ExitLevel"))
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.MapEnd,
|
|
||||||
Data = lineSplit[0],
|
|
||||||
Origin = new Player()
|
|
||||||
{
|
|
||||||
ClientId = 1
|
|
||||||
},
|
|
||||||
Target = new Player()
|
|
||||||
{
|
|
||||||
ClientId = 1
|
|
||||||
},
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/*if (lineSplit[0].Contains("ShutdownGame"))
|
|
||||||
{
|
|
||||||
|
|
||||||
}*/
|
|
||||||
|
|
||||||
if (lineSplit[0].Contains("InitGame"))
|
|
||||||
{
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.MapChange,
|
|
||||||
Data = lineSplit[0],
|
|
||||||
Origin = new Player()
|
|
||||||
{
|
|
||||||
ClientId = 1
|
|
||||||
},
|
|
||||||
Target = new Player()
|
|
||||||
{
|
|
||||||
ClientId = 1
|
|
||||||
},
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GameEvent()
|
|
||||||
{
|
|
||||||
Type = GameEvent.EventType.Unknown,
|
|
||||||
Origin = new Player()
|
|
||||||
{
|
|
||||||
ClientId = 1
|
|
||||||
},
|
|
||||||
Target = new Player()
|
|
||||||
{
|
|
||||||
ClientId = 1
|
|
||||||
},
|
|
||||||
Owner = server
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetGameDir() => $"t6r{Path.DirectorySeparatorChar}data";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
Application/GameEventHandler.cs
Normal file
23
Application/GameEventHandler.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Events;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application
|
||||||
|
{
|
||||||
|
class GameEventHandler : IEventHandler
|
||||||
|
{
|
||||||
|
readonly IManager Manager;
|
||||||
|
public GameEventHandler(IManager mgr)
|
||||||
|
{
|
||||||
|
Manager = mgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEvent(GameEvent gameEvent)
|
||||||
|
{
|
||||||
|
((Manager as ApplicationManager).OnServerEvent)(this, new GameEventArgs(null, false, gameEvent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
Application/IO/GameLogEventDetection.cs
Normal file
88
Application/IO/GameLogEventDetection.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.IO
|
||||||
|
{
|
||||||
|
class GameLogEventDetection
|
||||||
|
{
|
||||||
|
Server Server;
|
||||||
|
long PreviousFileSize;
|
||||||
|
IGameLogReader Reader;
|
||||||
|
readonly string GameLogFile;
|
||||||
|
|
||||||
|
class EventState
|
||||||
|
{
|
||||||
|
public ILogger Log { get; set; }
|
||||||
|
public string ServerId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameLogEventDetection(Server server, string gameLogPath, string gameLogName)
|
||||||
|
{
|
||||||
|
GameLogFile = gameLogPath;
|
||||||
|
// todo: abtract this more
|
||||||
|
if (gameLogPath.StartsWith("http"))
|
||||||
|
{
|
||||||
|
Reader = new GameLogReaderHttp(gameLogPath, server.EventParser);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Reader = new GameLogReader(gameLogPath, server.EventParser);
|
||||||
|
}
|
||||||
|
|
||||||
|
Server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PollForChanges()
|
||||||
|
{
|
||||||
|
while (!Server.Manager.ShutdownRequested())
|
||||||
|
{
|
||||||
|
if ((Server.Manager as ApplicationManager).IsInitialized)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await UpdateLogEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Server.Logger.WriteWarning($"Failed to update log event for {Server.EndPoint}");
|
||||||
|
Server.Logger.WriteDebug($"Exception: {e.Message}");
|
||||||
|
Server.Logger.WriteDebug($"StackTrace: {e.StackTrace}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Thread.Sleep(Reader.UpdateInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateLogEvents()
|
||||||
|
{
|
||||||
|
long fileSize = Reader.Length;
|
||||||
|
|
||||||
|
if (PreviousFileSize == 0)
|
||||||
|
PreviousFileSize = fileSize;
|
||||||
|
|
||||||
|
long fileDiff = fileSize - PreviousFileSize;
|
||||||
|
|
||||||
|
// this makes the http log get pulled
|
||||||
|
if (fileDiff < 1 && fileSize != -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PreviousFileSize = fileSize;
|
||||||
|
|
||||||
|
var events = await Reader.ReadEventsFromLog(Server, fileDiff, 0);
|
||||||
|
|
||||||
|
foreach (var ev in events)
|
||||||
|
{
|
||||||
|
Server.Manager.GetEventHandler().AddEvent(ev);
|
||||||
|
await ev.WaitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
PreviousFileSize = fileSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
Application/IO/GameLogReader.cs
Normal file
70
Application/IO/GameLogReader.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.IO
|
||||||
|
{
|
||||||
|
class GameLogReader : IGameLogReader
|
||||||
|
{
|
||||||
|
IEventParser Parser;
|
||||||
|
readonly string LogFile;
|
||||||
|
|
||||||
|
public long Length => new FileInfo(LogFile).Length;
|
||||||
|
|
||||||
|
public int UpdateInterval => 300;
|
||||||
|
|
||||||
|
public GameLogReader(string logFile, IEventParser parser)
|
||||||
|
{
|
||||||
|
LogFile = logFile;
|
||||||
|
Parser = parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
|
||||||
|
{
|
||||||
|
// allocate the bytes for the new log lines
|
||||||
|
List<string> logLines = new List<string>();
|
||||||
|
|
||||||
|
// open the file as a stream
|
||||||
|
using (var rd = new StreamReader(new FileStream(LogFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), Utilities.EncodingType))
|
||||||
|
{
|
||||||
|
// todo: max async
|
||||||
|
// take the old start position and go back the number of new characters
|
||||||
|
rd.BaseStream.Seek(-fileSizeDiff, SeekOrigin.End);
|
||||||
|
// the difference should be in the range of a int :P
|
||||||
|
string newLine;
|
||||||
|
while (!String.IsNullOrEmpty(newLine = rd.ReadLine()))
|
||||||
|
{
|
||||||
|
logLines.Add(newLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GameEvent> events = new List<GameEvent>();
|
||||||
|
|
||||||
|
// parse each line
|
||||||
|
foreach (string eventLine in logLines)
|
||||||
|
{
|
||||||
|
if (eventLine.Length > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// todo: catch elsewhere
|
||||||
|
events.Add(Parser.GetEvent(server, eventLine));
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
server.Logger.WriteWarning("Could not properly parse event line");
|
||||||
|
server.Logger.WriteDebug(e.Message);
|
||||||
|
server.Logger.WriteDebug(eventLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
74
Application/IO/GameLogReaderHttp.cs
Normal file
74
Application/IO/GameLogReaderHttp.cs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
using IW4MAdmin.Application.API.GameLogServer;
|
||||||
|
using RestEase;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static SharedLibraryCore.Utilities;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.IO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// provides capibility of reading log files over HTTP
|
||||||
|
/// </summary>
|
||||||
|
class GameLogReaderHttp : IGameLogReader
|
||||||
|
{
|
||||||
|
readonly IEventParser Parser;
|
||||||
|
readonly IGameLogServer Api;
|
||||||
|
readonly string LogFile;
|
||||||
|
|
||||||
|
public GameLogReaderHttp(string logFile, IEventParser parser)
|
||||||
|
{
|
||||||
|
LogFile = logFile;
|
||||||
|
Parser = parser;
|
||||||
|
Api = RestClient.For<IGameLogServer>(logFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long Length => -1;
|
||||||
|
|
||||||
|
public int UpdateInterval => 350;
|
||||||
|
|
||||||
|
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
|
||||||
|
{
|
||||||
|
#if DEBUG == true
|
||||||
|
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})");
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse each line
|
||||||
|
foreach (string eventLine in response.Data.Split(Environment.NewLine))
|
||||||
|
{
|
||||||
|
if (eventLine.Length > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var e = Parser.GetEvent(server, eventLine);
|
||||||
|
#if DEBUG == true
|
||||||
|
server.Logger.WriteDebug($"Parsed event with id {e.Id} from http");
|
||||||
|
#endif
|
||||||
|
events.Add(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
server.Logger.WriteWarning("Could not properly parse event line");
|
||||||
|
server.Logger.WriteDebug(e.Message);
|
||||||
|
server.Logger.WriteDebug(eventLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1003
Application/IW4MServer.cs
Normal file
1003
Application/IW4MServer.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,33 +1,75 @@
|
|||||||
using SharedLibraryCore;
|
using IW4MAdmin.Application.API.Master;
|
||||||
|
using SharedLibraryCore;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Localization
|
namespace IW4MAdmin.Application.Localization
|
||||||
{
|
{
|
||||||
public class Configure
|
public class Configure
|
||||||
{
|
{
|
||||||
public static void Initialize()
|
public static void Initialize(string customLocale = null)
|
||||||
{
|
{
|
||||||
string currentLocal = CultureInfo.CurrentCulture.Name;
|
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
|
||||||
string localizationFile = $"Localization{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocal}.json";
|
string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
|
||||||
string localizationContents;
|
|
||||||
|
|
||||||
if (File.Exists(localizationFile))
|
try
|
||||||
{
|
{
|
||||||
localizationContents = File.ReadAllText(localizationFile);
|
var api = Endpoint.Get();
|
||||||
|
var localization = api.GetLocalization(currentLocale).Result;
|
||||||
|
Utilities.CurrentLocalization = localization;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
catch (Exception)
|
||||||
{
|
{
|
||||||
localizationFile = $"Localization{Path.DirectorySeparatorChar}IW4MAdmin.en-US.json";
|
// the online localization failed so will default to local files
|
||||||
localizationContents = File.ReadAllText(localizationFile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Utilities.CurrentLocalization = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents);
|
// culture doesn't exist so we just want language
|
||||||
|
if (localizationFiles.Length == 0)
|
||||||
|
{
|
||||||
|
localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale.Substring(0, 2)}*.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
// language doesn't exist either so defaulting to english
|
||||||
|
if (localizationFiles.Length == 0)
|
||||||
|
{
|
||||||
|
localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), "*.en-US.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
// this should never happen unless the localization folder is empty
|
||||||
|
if (localizationFiles.Length == 0)
|
||||||
|
{
|
||||||
|
throw new Exception("No localization files were found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var localizationDict = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
foreach (string filePath in localizationFiles)
|
||||||
|
{
|
||||||
|
var localizationContents = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
var eachLocalizationFile = Newtonsoft.Json.JsonConvert.DeserializeObject<SharedLibraryCore.Localization.Layout>(localizationContents);
|
||||||
|
|
||||||
|
foreach (var item in eachLocalizationFile.LocalizationIndex.Set)
|
||||||
|
{
|
||||||
|
if (!localizationDict.TryAdd(item.Key, item.Value))
|
||||||
|
{
|
||||||
|
Program.ServerManager.GetLogger(0).WriteError($"Could not add locale string {item.Key} to localization");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string localizationFile = $"{Path.Join(Utilities.OperatingDirectory, "Localization")}{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocale}-{currentLocale.ToUpper()}.json";
|
||||||
|
|
||||||
|
Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict)
|
||||||
|
{
|
||||||
|
LocalizationName = currentLocale,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,109 +1,269 @@
|
|||||||
{
|
{
|
||||||
"LocalizationName": "en-US",
|
"LocalizationName": "en-US",
|
||||||
"LocalizationSet": {
|
"LocalizationIndex": {
|
||||||
"MANAGER_VERSION_FAIL": "Could not get latest IW4MAdmin version",
|
"Set": {
|
||||||
"MANAGER_VERSION_UPDATE": "has an update. Latest version is",
|
"BROADCAST_OFFLINE": "^5IW4MAdmin ^7is going ^1OFFLINE",
|
||||||
"MANAGER_VERSION_CURRENT": "Your version is",
|
"BROADCAST_ONLINE": "^5IW4MADMIN ^7is now ^2ONLINE",
|
||||||
"MANAGER_VERSION_SUCCESS": "IW4MAdmin is up to date",
|
"COMMAND_HELP_OPTIONAL": "optional",
|
||||||
"MANAGER_INIT_FAIL": "Fatal error during initialization",
|
"COMMAND_HELP_SYNTAX": "syntax:",
|
||||||
"MANAGER_EXIT": "Press any key to exit...",
|
"COMMAND_MISSINGARGS": "Not enough arguments supplied",
|
||||||
"SETUP_ENABLE_WEBFRONT": "Enable webfront",
|
"COMMAND_NOACCESS": "You do not have access to that command",
|
||||||
"SETUP_ENABLE_MULTIOWN": "Enable multiple owners",
|
"COMMAND_NOTAUTHORIZED": "You are not authorized to execute that command",
|
||||||
"SETUP_ENABLE_STEPPEDPRIV": "Enable stepped privilege hierarchy",
|
"COMMAND_TARGET_MULTI": "Multiple players match that name",
|
||||||
"SETUP_ENABLE_CUSTOMSAY": "Enable custom say name",
|
"COMMAND_TARGET_NOTFOUND": "Unable to find specified player",
|
||||||
"SETUP_SAY_NAME": "Enter custom say name",
|
"COMMAND_UNKNOWN": "You entered an unknown command",
|
||||||
"SETUP_USE_CUSTOMENCODING": "Use custom encoding parser",
|
"COMMANDS_ADMINS_DESC": "list currently connected privileged clients",
|
||||||
"SETUP_ENCODING_STRING": "Enter encoding string",
|
"COMMANDS_ADMINS_NONE": "No visible administrators online",
|
||||||
"SETUP_ENABLE_VPNS": "Enable client VPNs",
|
"COMMANDS_ALIAS_ALIASES": "Aliases",
|
||||||
"SETUP_IPHUB_KEY": "Enter iphub.info api key",
|
"COMMANDS_ALIAS_DESC": "get past aliases and ips of a client",
|
||||||
"SETUP_DISPLAY_DISCORD": "Display discord link on webfront",
|
"COMMANDS_ALIAS_IPS": "IPs",
|
||||||
"SETUP_DISCORD_INVITE": "Enter discord invite link",
|
"COMMANDS_ARGS_CLEAR": "clear",
|
||||||
"SETUP_SERVER_USET6M": "Use T6M parser",
|
"COMMANDS_ARGS_CLIENTID": "client id",
|
||||||
"SETUP_SERVER_IP": "Enter server IP Address",
|
"COMMANDS_ARGS_COMMANDS": "commands",
|
||||||
"SETUP_SERVER_PORT": "Enter server port",
|
"COMMANDS_ARGS_DURATION": "duration (m|h|d|w|y)",
|
||||||
"SETUP_SERVER_RCON": "Enter server RCon password",
|
"COMMANDS_ARGS_INACTIVE": "inactive days",
|
||||||
"SETUP_SERVER_SAVE": "Configuration saved, add another",
|
"COMMANDS_ARGS_LEVEL": "level",
|
||||||
"SERVER_KICK_VPNS_NOTALLOWED": "VPNs are not allowed on this server",
|
"COMMANDS_ARGS_MAP": "map",
|
||||||
"SERVER_KICK_TEXT": "You were kicked",
|
"COMMANDS_ARGS_MESSAGE": "message",
|
||||||
"SERVER_KICK_MINNAME": "Your name must contain at least 3 characters",
|
"COMMANDS_ARGS_PASSWORD": "password",
|
||||||
"SERVER_KICK_NAME_INUSE": "Your name is being used by someone else",
|
"COMMANDS_ARGS_PLAYER": "player",
|
||||||
"SERVER_KICK_GENERICNAME": "Please change your name using /name",
|
"COMMANDS_ARGS_REASON": "reason",
|
||||||
"SERVER_KICK_CONTROLCHARS": "Your name cannot contain control characters",
|
"COMMANDS_BAN_DESC": "permanently ban a client from the server",
|
||||||
"SERVER_TB_TEXT": "You're temporarily banned",
|
"COMMANDS_BAN_FAIL": "You cannot ban",
|
||||||
"SERVER_TB_REMAIN": "You are temporarily banned",
|
"COMMANDS_BAN_SUCCESS": "has been permanently banned",
|
||||||
"SERVER_BAN_TEXT": "You're banned",
|
"COMMANDS_BANINFO_DESC": "get information about a ban for a client",
|
||||||
"SERVER_BAN_PREV": "Previously banned for",
|
"COMMANDS_BANINFO_NONE": "No active ban was found for that player",
|
||||||
"SERVER_BAN_APPEAL": "appeal at",
|
"COMMANDS_BANINO_SUCCESS": "was banned by ^5{0} ^7for:",
|
||||||
"SERVER_REPORT_COUNT": "There are ^5{0} ^7recent reports",
|
"COMMANDS_FASTRESTART_DESC": "fast restart current map",
|
||||||
"SERVER_WARNLIMT_REACHED": "Too many warnings",
|
"COMMANDS_FASTRESTART_MASKED": "The map has been fast restarted",
|
||||||
"SERVER_WARNING": "Warning",
|
"COMMANDS_FASTRESTART_UNMASKED": "fast restarted the map",
|
||||||
"SERVER_WEBSITE_GENERIC": "this server's website",
|
"COMMANDS_FIND_DESC": "find client in database",
|
||||||
"BROADCAST_ONLINE": "^5IW4MADMIN ^7is now ^2ONLINE",
|
"COMMANDS_FIND_EMPTY": "No players found",
|
||||||
"BROADCAST_OFFLINE": "IW4MAdmin is going offline",
|
"COMMANDS_FIND_MIN": "Please enter at least 3 characters",
|
||||||
"COMMAND_HELP_SYNTAX": "syntax:",
|
"COMMANDS_FLAG_DESC": "flag a suspicious client and announce to admins on join",
|
||||||
"COMMAND_HELP_OPTIONAL": "optional",
|
"COMMANDS_FLAG_FAIL": "You cannot flag",
|
||||||
"COMMAND_UNKNOWN": "You entered an unknown command",
|
"COMMANDS_FLAG_SUCCESS": "You have flagged",
|
||||||
"COMMAND_NOACCESS": "You do not have access to that command",
|
"COMMANDS_FLAG_UNFLAG": "You have unflagged",
|
||||||
"COMMAND_NOTAUTHORIZED": "You are not authorized to execute that command",
|
"COMMANDS_HELP_DESC": "list all available commands",
|
||||||
"COMMAND_MISSINGARGS": "Not enough arguments supplied",
|
"COMMANDS_HELP_MOREINFO": "Type !help <command name> to get command usage syntax",
|
||||||
"COMMAND_TARGET_MULTI": "Multiple players match that name",
|
"COMMANDS_HELP_NOTFOUND": "Could not find that command",
|
||||||
"COMMAND_TARGET_NOTFOUND": "Unable to find specified player",
|
"COMMANDS_IP_DESC": "view your external IP address",
|
||||||
"PLUGIN_IMPORTER_NOTFOUND": "No plugins found to load",
|
"COMMANDS_IP_SUCCESS": "Your external IP is",
|
||||||
"PLUGIN_IMPORTER_REGISTERCMD": "Registered command",
|
"COMMANDS_KICK_DESC": "kick a client by name",
|
||||||
"COMMANDS_OWNER_SUCCESS": "Congratulations, you have claimed ownership of this server!",
|
"COMMANDS_KICK_FAIL": "You do not have the required privileges to kick",
|
||||||
"COMMANDS_OWNER_FAIL": "This server already has an owner",
|
"COMMANDS_KICK_SUCCESS": "has been kicked",
|
||||||
"COMMANDS_WARN_FAIL": "You do not have the required privileges to warn",
|
"COMMANDS_LIST_DESC": "list active clients",
|
||||||
"COMMANDS_WARNCLEAR_SUCCESS": "All warning cleared for",
|
"COMMANDS_MAP_DESC": "change to specified map",
|
||||||
"COMMANDS_KICK_SUCCESS": "has been kicked",
|
"COMMANDS_MAP_SUCCESS": "Changing to map",
|
||||||
"COMMANDS_KICK_FAIL": "You do not have the required privileges to kick",
|
"COMMANDS_MAP_UKN": "Attempting to change to unknown map",
|
||||||
"COMMANDS_TEMPBAN_SUCCESS": "has been temporarily banned for",
|
"COMMANDS_MAPROTATE": "Map rotating in ^55 ^7seconds",
|
||||||
"COMMANDS_TEMPBAN_FAIL": "You cannot temporarily ban",
|
"COMMANDS_MAPROTATE_DESC": "cycle to the next map in rotation",
|
||||||
"COMMANDS_BAN_SUCCESS": "has been permanently banned",
|
"COMMANDS_MASK_DESC": "hide your presence as a privileged client",
|
||||||
"COMMANDS_BAN_FAIL": "You cannot ban",
|
"COMMANDS_MASK_OFF": "You are now unmasked",
|
||||||
"COMMANDS_UNBAN_SUCCESS": "Successfully unbanned",
|
"COMMANDS_MASK_ON": "You are now masked",
|
||||||
"COMMANDS_UNBAN_FAIL": "is not banned",
|
"COMMANDS_OWNER_DESC": "claim ownership of the server",
|
||||||
"COMMANDS_HELP_NOTFOUND": "Could not find that command",
|
"COMMANDS_OWNER_FAIL": "This server already has an owner",
|
||||||
"COMMANDS_HELP_MOREINFO": "Type !help <command name> to get command usage syntax",
|
"COMMANDS_OWNER_SUCCESS": "Congratulations, you have claimed ownership of this server!",
|
||||||
"COMMANDS_FASTRESTART_UNMASKED": "fast restarted the map",
|
"COMMANDS_PASSWORD_FAIL": "Your password must be at least 5 characters long",
|
||||||
"COMMANDS_FASTRESTART_MASKED": "The map has been fast restarted",
|
"COMMANDS_PASSWORD_SUCCESS": "Your password has been set successfully",
|
||||||
"COMMANDS_MAPROTATE": "Map rotating in ^55 ^7seconds",
|
"COMMANDS_PING_DESC": "get client's latency",
|
||||||
"COMMANDS_SETLEVEL_SELF": "You cannot change your own level",
|
"COMMANDS_PING_SELF": "Your latency is",
|
||||||
"COMMANDS_SETLEVEL_OWNER": "There can only be 1 owner. Modify your settings if multiple owners are required",
|
"COMMANDS_PING_TARGET": "latency is",
|
||||||
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "This server does not allow you to promote",
|
"COMMANDS_PLUGINS_DESC": "view all loaded plugins",
|
||||||
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "You can only promote ^5{0} ^7to ^5{1} ^7or lower privilege",
|
"COMMANDS_PLUGINS_LOADED": "Loaded Plugins",
|
||||||
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "Congratulations! You have been promoted to",
|
"COMMANDS_PM_DESC": "send message to other client",
|
||||||
"COMMANDS_SETLEVEL_SUCCESS": "was successfully promoted",
|
"COMMANDS_PRUNE_DESC": "demote any privileged clients that have not connected recently (defaults to 30 days)",
|
||||||
"COMMANDS_SETLEVEL_FAIL": "Invalid group specified",
|
"COMMANDS_PRUNE_FAIL": "Invalid number of inactive days",
|
||||||
"COMMANDS_ADMINS_NONE": "No visible administrators online",
|
"COMMANDS_PRUNE_SUCCESS": "inactive privileged users were pruned",
|
||||||
"COMMANDS_MAP_SUCCESS": "Changing to map",
|
"COMMANDS_QUIT_DESC": "quit IW4MAdmin",
|
||||||
"COMMANDS_MAP_UKN": "Attempting to change to unknown map",
|
"COMMANDS_RCON_DESC": "send rcon command to server",
|
||||||
"COMMANDS_FIND_MIN": "Please enter at least 3 characters",
|
"COMMANDS_RCON_SUCCESS": "Successfully sent RCon command",
|
||||||
"COMMANDS_FIND_EMPTY": "No players found",
|
"COMMANDS_REPORT_DESC": "report a client for suspicious behavior",
|
||||||
"COMMANDS_RULES_NONE": "The server owner has not set any rules",
|
"COMMANDS_REPORT_FAIL": "You cannot report",
|
||||||
"COMMANDS_FLAG_SUCCESS": "You have flagged",
|
"COMMANDS_REPORT_FAIL_CAMP": "You cannot report an player for camping",
|
||||||
"COMMANDS_FLAG_UNFLAG": "You have unflagged",
|
"COMMANDS_REPORT_FAIL_DUPLICATE": "You have already reported this player",
|
||||||
"COMMANDS_FLAG_FAIL": "You cannot flag",
|
"COMMANDS_REPORT_FAIL_SELF": "You cannot report yourself",
|
||||||
"COMMANDS_REPORT_FAIL_CAMP": "You cannot report an player for camping",
|
"COMMANDS_REPORT_SUCCESS": "Thank you for your report, an administrator has been notified",
|
||||||
"COMMANDS_REPORT_FAIL_DUPLICATE": "You have already reported this player",
|
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Reports successfully cleared",
|
||||||
"COMMANDS_REPORT_FAIL_SELF": "You cannot report yourself",
|
"COMMANDS_REPORTS_DESC": "get or clear recent reports",
|
||||||
"COMMANDS_REPORT_FAIL": "You cannot report",
|
"COMMANDS_REPORTS_NONE": "No players reported yet",
|
||||||
"COMMANDS_REPORT_SUCCESS": "Thank you for your report, an administrator has been notified",
|
"COMMANDS_RULES_DESC": "list server rules",
|
||||||
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Reports successfully cleared",
|
"COMMANDS_RULES_NONE": "The server owner has not set any rules",
|
||||||
"COMMANDS_REPORTS_NONE": "No players reported yet",
|
"COMMANDS_SAY_DESC": "broadcast message to all clients",
|
||||||
"COMMANDS_MASK_ON": "You are now masked",
|
"COMMANDS_SETLEVEL_DESC": "set client to specified privilege level",
|
||||||
"COMMANDS_MASK_OFF": "You are now unmasked",
|
"COMMANDS_SETLEVEL_FAIL": "Invalid group specified",
|
||||||
"COMMANDS_BANINFO_NONE": "No active ban was found for that player",
|
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "You can only promote ^5{0} ^7to ^5{1} ^7or lower privilege",
|
||||||
"COMMANDS_BANINO_SUCCESS": "was banned by ^5{0} ^7for:",
|
"COMMANDS_SETLEVEL_OWNER": "There can only be 1 owner. Modify your settings if multiple owners are required",
|
||||||
"COMMANDS_ALIAS_ALIASES": "Aliases",
|
"COMMANDS_SETLEVEL_SELF": "You cannot change your own level",
|
||||||
"COMMANDS_ALIAS_IPS": "IPs",
|
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "This server does not allow you to promote",
|
||||||
"COMMANDS_RCON_SUCCESS": "Successfully sent RCon command",
|
"COMMANDS_SETLEVEL_SUCCESS": "was successfully promoted",
|
||||||
"COMMANDS_PLUGINS_LOADED": "Loaded Plugins",
|
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "Congratulations! You have been promoted to",
|
||||||
"COMMANDS_IP_SUCCESS": "Your external IP is",
|
"COMMANDS_SETPASSWORD_DESC": "set your authentication password",
|
||||||
"COMMANDS_PRUNE_FAIL": "Invalid number of inactive days",
|
"COMMANDS_TEMPBAN_DESC": "temporarily ban a client for specified time (defaults to 1 hour)",
|
||||||
"COMMANDS_PRUNE_SUCCESS": "inactive privileged users were pruned",
|
"COMMANDS_TEMPBAN_FAIL": "You cannot temporarily ban",
|
||||||
"COMMANDS_PASSWORD_FAIL": "Your password must be at least 5 characters long",
|
"COMMANDS_TEMPBAN_SUCCESS": "has been temporarily banned for",
|
||||||
"COMMANDS_PASSWORD_SUCCESS": "Your password has been set successfully",
|
"COMMANDS_UNBAN_DESC": "unban client by client id",
|
||||||
"COMMANDS_PING_TARGET": "ping is",
|
"COMMANDS_UNBAN_FAIL": "is not banned",
|
||||||
"COMMANDS_PING_SELF": "Your ping is"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
269
Application/Localization/IW4MAdmin.es-EC.json
Normal file
269
Application/Localization/IW4MAdmin.es-EC.json
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
269
Application/Localization/IW4MAdmin.pt-BR.json
Normal file
269
Application/Localization/IW4MAdmin.pt-BR.json
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
269
Application/Localization/IW4MAdmin.ru-RU.json
Normal file
269
Application/Localization/IW4MAdmin.ru-RU.json
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
{
|
||||||
|
"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,12 +1,15 @@
|
|||||||
using System;
|
using SharedLibraryCore;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
class Logger : SharedLibraryCore.Interfaces.ILogger
|
class Logger : SharedLibraryCore.Interfaces.ILogger
|
||||||
{
|
{
|
||||||
enum LogType
|
enum LogType
|
||||||
{
|
{
|
||||||
Verbose,
|
Verbose,
|
||||||
Info,
|
Info,
|
||||||
Debug,
|
Debug,
|
||||||
@ -15,27 +18,61 @@ namespace IW4MAdmin.Application
|
|||||||
Assert
|
Assert
|
||||||
}
|
}
|
||||||
|
|
||||||
string FileName;
|
readonly string FileName;
|
||||||
object ThreadLock;
|
readonly SemaphoreSlim OnLogWriting;
|
||||||
|
static readonly short MAX_LOG_FILES = 10;
|
||||||
|
|
||||||
public Logger(string fn)
|
public Logger(string fn)
|
||||||
{
|
{
|
||||||
FileName = fn;
|
FileName = Path.Join(Utilities.OperatingDirectory, "Log", $"{fn}.log");
|
||||||
ThreadLock = new object();
|
OnLogWriting = new SemaphoreSlim(1, 1);
|
||||||
if (File.Exists(fn))
|
RotateLogs();
|
||||||
File.Delete(fn);
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// rotates logs when log is initialized
|
||||||
|
/// </summary>
|
||||||
|
private void RotateLogs()
|
||||||
|
{
|
||||||
|
string maxLog = FileName + MAX_LOG_FILES;
|
||||||
|
|
||||||
|
if (File.Exists(maxLog))
|
||||||
|
{
|
||||||
|
File.Delete(maxLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = MAX_LOG_FILES - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
string logToMove = i == 0 ? FileName : FileName + i;
|
||||||
|
string movedLogName = FileName + (i + 1);
|
||||||
|
|
||||||
|
if (File.Exists(logToMove))
|
||||||
|
{
|
||||||
|
File.Move(logToMove, movedLogName);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Write(string msg, LogType type)
|
void Write(string msg, LogType type)
|
||||||
{
|
{
|
||||||
string LogLine = $"[{DateTime.Now.ToString("HH:mm:ss")}] - {type}: {msg}";
|
OnLogWriting.Wait();
|
||||||
lock (ThreadLock)
|
|
||||||
|
string stringType = type.ToString();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stringType = Utilities.CurrentLocalization.LocalizationIndex[$"GLOBAL_{type.ToString().ToUpper()}"];
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception) { }
|
||||||
|
|
||||||
|
string LogLine = $"[{DateTime.Now.ToString("MM.dd.yyy HH:mm:ss.fff")}] - {stringType}: {msg}";
|
||||||
|
try
|
||||||
{
|
{
|
||||||
#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);
|
|
||||||
#else
|
#else
|
||||||
if (type == LogType.Error || type == LogType.Verbose)
|
if (type == LogType.Error || type == LogType.Verbose)
|
||||||
Console.WriteLine(LogLine);
|
Console.WriteLine(LogLine);
|
||||||
@ -43,6 +80,14 @@ namespace IW4MAdmin.Application
|
|||||||
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
|
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Well.. It looks like your machine can't event write to the log file. That's something else...");
|
||||||
|
Console.WriteLine(ex.GetExceptionInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
OnLogWriting.Release(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteVerbose(string msg)
|
public void WriteVerbose(string msg)
|
||||||
|
@ -6,40 +6,60 @@ using System.Reflection;
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Objects;
|
||||||
using SharedLibraryCore.Database;
|
using SharedLibraryCore.Database;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using SharedLibraryCore.Localization;
|
||||||
|
using IW4MAdmin.Application.Migration;
|
||||||
|
using SharedLibraryCore.Exceptions;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
static public double Version { get; private set; }
|
static public double Version { get; private set; }
|
||||||
static public ApplicationManager ServerManager = ApplicationManager.GetInstance();
|
static public ApplicationManager ServerManager;
|
||||||
public static string OperatingDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
|
private static ManualResetEventSlim OnShutdownComplete = new ManualResetEventSlim();
|
||||||
|
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
AppDomain.CurrentDomain.SetData("DataDirectory", OperatingDirectory);
|
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
|
||||||
System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.BelowNormal;
|
Console.OutputEncoding = Encoding.UTF8;
|
||||||
Localization.Configure.Initialize();
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
var loc = Utilities.CurrentLocalization.LocalizationSet;
|
|
||||||
|
|
||||||
Version = Assembly.GetExecutingAssembly().GetName().Version.Major + Assembly.GetExecutingAssembly().GetName().Version.Minor / 10.0f;
|
Version = Utilities.GetVersionAsDouble();
|
||||||
|
|
||||||
Console.WriteLine("=====================================================");
|
Console.WriteLine("=====================================================");
|
||||||
Console.WriteLine(" IW4M ADMIN");
|
Console.WriteLine(" IW4M ADMIN");
|
||||||
Console.WriteLine(" by RaidMax ");
|
Console.WriteLine(" by RaidMax ");
|
||||||
Console.WriteLine($" Version {Version.ToString("0.0")}");
|
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
|
||||||
Console.WriteLine("=====================================================");
|
Console.WriteLine("=====================================================");
|
||||||
|
|
||||||
|
Index loc = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var db = new DatabaseContext())
|
ServerManager = ApplicationManager.GetInstance();
|
||||||
new ContextSeed(db).Seed().Wait();
|
try
|
||||||
|
{
|
||||||
|
Localization.Configure.Initialize(ServerManager.GetApplicationSettings().Configuration().CustomLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (ServerException)
|
||||||
|
{
|
||||||
|
Localization.Configure.Initialize();
|
||||||
|
}
|
||||||
|
loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
|
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
|
||||||
|
|
||||||
CheckDirectories();
|
CheckDirectories();
|
||||||
|
// do any needed migrations
|
||||||
|
// todo: move out
|
||||||
|
ConfigurationMigration.MoveConfigFolder10518(null);
|
||||||
|
|
||||||
ServerManager = ApplicationManager.GetInstance();
|
ServerManager.Logger.WriteInfo($"Version is {Version}");
|
||||||
|
|
||||||
var api = API.Master.Endpoint.Get();
|
var api = API.Master.Endpoint.Get();
|
||||||
|
|
||||||
var version = new API.Master.VersionInfo()
|
var version = new API.Master.VersionInfo()
|
||||||
{
|
{
|
||||||
CurrentVersionStable = 99.99f
|
CurrentVersionStable = 99.99f
|
||||||
@ -65,7 +85,7 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
Console.ForegroundColor = ConsoleColor.Red;
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
|
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
|
||||||
Console.ForegroundColor = ConsoleColor.White;
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !PRERELEASE
|
#if !PRERELEASE
|
||||||
@ -74,7 +94,7 @@ namespace IW4MAdmin.Application
|
|||||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||||
Console.WriteLine($"IW4MAdmin {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString("0.0")}]");
|
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.WriteLine($"{loc["MANAGER_VERSION_CURRENT"]} [v{Version.ToString("0.0")}]");
|
||||||
Console.ForegroundColor = ConsoleColor.White;
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
else if (version.CurrentVersionPrerelease > Version)
|
else if (version.CurrentVersionPrerelease > Version)
|
||||||
@ -82,22 +102,22 @@ namespace IW4MAdmin.Application
|
|||||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||||
Console.WriteLine($"IW4MAdmin-Prerelease {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString("0.0")}-pr]");
|
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.WriteLine($"{loc["MANAGER_VERSION_CURRENT"]} [v{Version.ToString("0.0")}-pr]");
|
||||||
Console.ForegroundColor = ConsoleColor.White;
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.ForegroundColor = ConsoleColor.Green;
|
Console.ForegroundColor = ConsoleColor.Green;
|
||||||
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
|
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
|
||||||
Console.ForegroundColor = ConsoleColor.White;
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerManager.Init().Wait();
|
ServerManager.Init().Wait();
|
||||||
|
|
||||||
var consoleTask = Task.Run(() =>
|
var consoleTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
String userInput;
|
string userInput;
|
||||||
Player Origin = ServerManager.GetClientService().Get(1).Result.AsPlayer();
|
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@ -108,47 +128,78 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
if (ServerManager.Servers.Count == 0)
|
if (ServerManager.Servers.Count == 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine("No servers are currently being monitored");
|
Console.WriteLine(loc["MANAGER_CONSOLE_NOSERV"]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Origin.CurrentServer = ServerManager.Servers[0];
|
if (userInput?.Length > 0)
|
||||||
GameEvent E = new GameEvent(GameEvent.EventType.Say, userInput, Origin, null, ServerManager.Servers[0]);
|
{
|
||||||
ServerManager.Servers[0].ExecuteEvent(E);
|
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('>');
|
Console.Write('>');
|
||||||
|
|
||||||
} while (ServerManager.Running);
|
} while (ServerManager.Running);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront)
|
|
||||||
{
|
|
||||||
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerManager.Start();
|
|
||||||
ServerManager.Logger.WriteVerbose("Shutdown complete");
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Console.WriteLine(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"];
|
||||||
|
|
||||||
|
Console.WriteLine(failMessage);
|
||||||
while (e.InnerException != null)
|
while (e.InnerException != null)
|
||||||
{
|
{
|
||||||
e = e.InnerException;
|
e = e.InnerException;
|
||||||
}
|
}
|
||||||
Console.WriteLine($"Exception: {e.Message}");
|
Console.WriteLine(e.Message);
|
||||||
Console.WriteLine(loc["MANAGER_EXIT"]);
|
Console.WriteLine(exitMessage);
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront)
|
||||||
|
{
|
||||||
|
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
|
||||||
|
}
|
||||||
|
|
||||||
|
OnShutdownComplete.Reset();
|
||||||
|
ServerManager.Start();
|
||||||
|
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
|
||||||
|
OnShutdownComplete.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnCancelKey(object sender, ConsoleCancelEventArgs e)
|
||||||
|
{
|
||||||
|
ServerManager.Stop();
|
||||||
|
OnShutdownComplete.Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void CheckDirectories()
|
static void CheckDirectories()
|
||||||
{
|
{
|
||||||
string curDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
|
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Plugins")))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Plugins"));
|
||||||
|
}
|
||||||
|
|
||||||
if (!Directory.Exists($"{curDirectory}Plugins"))
|
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Database")))
|
||||||
Directory.CreateDirectory($"{curDirectory}Plugins");
|
{
|
||||||
|
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Database"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Log")))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,21 @@
|
|||||||
using System;
|
using IW4MAdmin.Application.API.Master;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Commands;
|
||||||
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Database;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Events;
|
||||||
|
using SharedLibraryCore.Exceptions;
|
||||||
|
using SharedLibraryCore.Helpers;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using SharedLibraryCore.Services;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Reflection;
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using SharedLibraryCore.Commands;
|
|
||||||
using SharedLibraryCore.Helpers;
|
|
||||||
using SharedLibraryCore.Exceptions;
|
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using SharedLibraryCore.Services;
|
|
||||||
using IW4MAdmin.Application.API;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using WebfrontCore;
|
|
||||||
using SharedLibraryCore.Configuration;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
@ -26,30 +23,33 @@ namespace IW4MAdmin.Application
|
|||||||
{
|
{
|
||||||
private List<Server> _servers;
|
private List<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, Player> PrivilegedClients { get; set; }
|
public Dictionary<int, EFClient> PrivilegedClients { get; set; }
|
||||||
public ILogger Logger { get; private set; }
|
public ILogger Logger => GetLogger(0);
|
||||||
public bool Running { get; private set; }
|
public bool Running { get; private set; }
|
||||||
public EventHandler<GameEvent> ServerEventOccurred { get; private set; }
|
public bool IsInitialized { get; private set; }
|
||||||
|
// define what the delagate function looks like
|
||||||
|
public delegate void OnServerEventEventHandler(object sender, GameEventArgs e);
|
||||||
|
// expose the event handler so we can execute the events
|
||||||
|
public OnServerEventEventHandler OnServerEvent { get; set; }
|
||||||
public DateTime StartTime { get; private set; }
|
public DateTime StartTime { get; private set; }
|
||||||
|
public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString();
|
||||||
|
|
||||||
static ApplicationManager Instance;
|
static ApplicationManager Instance;
|
||||||
List<AsyncStatus> TaskStatuses;
|
readonly List<AsyncStatus> TaskStatuses;
|
||||||
List<Command> Commands;
|
List<Command> Commands;
|
||||||
List<MessageToken> MessageTokens;
|
readonly List<MessageToken> MessageTokens;
|
||||||
ClientService ClientSvc;
|
ClientService ClientSvc;
|
||||||
AliasService AliasSvc;
|
readonly AliasService AliasSvc;
|
||||||
PenaltyService PenaltySvc;
|
readonly PenaltyService PenaltySvc;
|
||||||
BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
public BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
||||||
EventApi Api;
|
GameEventHandler Handler;
|
||||||
#if FTP_LOG
|
ManualResetEventSlim OnQuit;
|
||||||
const int UPDATE_FREQUENCY = 700;
|
readonly IPageList PageList;
|
||||||
#else
|
readonly SemaphoreSlim ProcessingEvent = new SemaphoreSlim(1, 1);
|
||||||
const int UPDATE_FREQUENCY = 450;
|
readonly Dictionary<long, ILogger> Loggers = new Dictionary<long, ILogger>();
|
||||||
#endif
|
|
||||||
|
|
||||||
private ApplicationManager()
|
private ApplicationManager()
|
||||||
{
|
{
|
||||||
Logger = new Logger($@"{Utilities.OperatingDirectory}IW4MAdmin.log");
|
|
||||||
_servers = new List<Server>();
|
_servers = new List<Server>();
|
||||||
Commands = new List<Command>();
|
Commands = new List<Command>();
|
||||||
TaskStatuses = new List<AsyncStatus>();
|
TaskStatuses = new List<AsyncStatus>();
|
||||||
@ -57,19 +57,73 @@ namespace IW4MAdmin.Application
|
|||||||
ClientSvc = new ClientService();
|
ClientSvc = new ClientService();
|
||||||
AliasSvc = new AliasService();
|
AliasSvc = new AliasService();
|
||||||
PenaltySvc = new PenaltyService();
|
PenaltySvc = new PenaltyService();
|
||||||
PrivilegedClients = new Dictionary<int, Player>();
|
|
||||||
Api = new EventApi();
|
|
||||||
ServerEventOccurred += Api.OnServerEvent;
|
|
||||||
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
||||||
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
|
|
||||||
StartTime = DateTime.UtcNow;
|
StartTime = DateTime.UtcNow;
|
||||||
|
OnQuit = new ManualResetEventSlim();
|
||||||
|
PageList = new PageList();
|
||||||
|
OnServerEvent += OnGameEvent;
|
||||||
|
OnServerEvent += EventApi.OnGameEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCancelKey(object sender, ConsoleCancelEventArgs args)
|
private async void OnGameEvent(object sender, GameEventArgs args)
|
||||||
{
|
{
|
||||||
Stop();
|
#if DEBUG == true
|
||||||
}
|
Logger.WriteDebug($"Entering event process for {args.Event.Id}");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var newEvent = args.Event;
|
||||||
|
|
||||||
|
// the event has failed already
|
||||||
|
if (newEvent.Failed)
|
||||||
|
{
|
||||||
|
goto skip;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await newEvent.Owner.ExecuteEvent(newEvent);
|
||||||
|
|
||||||
|
// save the event info to the database
|
||||||
|
var changeHistorySvc = new ChangeHistoryService();
|
||||||
|
await changeHistorySvc.Add(args.Event);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Logger.WriteDebug($"Processed event with id {newEvent.Id}");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// this happens if a plugin requires login
|
||||||
|
catch (AuthorizationException ex)
|
||||||
|
{
|
||||||
|
newEvent.FailReason = GameEvent.EventFailReason.Permission;
|
||||||
|
newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (NetworkException ex)
|
||||||
|
{
|
||||||
|
newEvent.FailReason = GameEvent.EventFailReason.Exception;
|
||||||
|
Logger.WriteError(ex.Message);
|
||||||
|
Logger.WriteDebug(ex.GetExceptionInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (ServerException ex)
|
||||||
|
{
|
||||||
|
newEvent.FailReason = GameEvent.EventFailReason.Exception;
|
||||||
|
Logger.WriteWarning(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
newEvent.FailReason = GameEvent.EventFailReason.Exception;
|
||||||
|
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}");
|
||||||
|
Logger.WriteDebug(ex.GetExceptionInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
skip:
|
||||||
|
|
||||||
|
// tell anyone waiting for the output that we're done
|
||||||
|
newEvent.OnProcessed.Set();
|
||||||
|
}
|
||||||
|
|
||||||
public IList<Server> GetServers()
|
public IList<Server> GetServers()
|
||||||
{
|
{
|
||||||
@ -86,38 +140,77 @@ namespace IW4MAdmin.Application
|
|||||||
return Instance ?? (Instance = new ApplicationManager());
|
return Instance ?? (Instance = new ApplicationManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateServerStates()
|
||||||
|
{
|
||||||
|
// store the server hash code and task for it
|
||||||
|
var runningUpdateTasks = new Dictionary<long, Task>();
|
||||||
|
|
||||||
|
while (Running)
|
||||||
|
{
|
||||||
|
// select the server ids that have completed the update task
|
||||||
|
var serverTasksToRemove = runningUpdateTasks
|
||||||
|
.Where(ut => ut.Value.Status == TaskStatus.RanToCompletion ||
|
||||||
|
ut.Value.Status == TaskStatus.Canceled ||
|
||||||
|
ut.Value.Status == TaskStatus.Faulted)
|
||||||
|
.Select(ut => ut.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// this is to prevent the log reader from starting before the initial
|
||||||
|
// query of players on the server
|
||||||
|
if (serverTasksToRemove.Count > 0)
|
||||||
|
{
|
||||||
|
IsInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the update tasks as they have completd
|
||||||
|
foreach (long serverId in serverTasksToRemove)
|
||||||
|
{
|
||||||
|
runningUpdateTasks.Remove(serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// select the servers where the tasks have completed
|
||||||
|
var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
|
||||||
|
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
|
||||||
|
{
|
||||||
|
runningUpdateTasks.Add(server.EndPoint, Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await server.ProcessUpdatesAsync(new CancellationToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.WriteWarning($"Failed to update status for {server}");
|
||||||
|
Logger.WriteDebug(e.GetExceptionInfo());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
Logger.WriteDebug($"{runningUpdateTasks.Count} servers queued for stats updates");
|
||||||
|
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
|
||||||
|
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
|
||||||
|
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
|
||||||
|
#endif
|
||||||
|
await Task.Delay(ConfigHandler.Configuration().RConPollRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger the event processing loop to end
|
||||||
|
SetHasEvent();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Init()
|
public async Task Init()
|
||||||
{
|
{
|
||||||
|
Running = true;
|
||||||
|
|
||||||
#region DATABASE
|
#region DATABASE
|
||||||
var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted))
|
using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString,
|
||||||
.Select(c => new
|
GetApplicationSettings().Configuration()?.DatabaseProvider))
|
||||||
{
|
|
||||||
c.Password,
|
|
||||||
c.PasswordSalt,
|
|
||||||
c.ClientId,
|
|
||||||
c.Level,
|
|
||||||
c.Name
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var a in ipList)
|
|
||||||
{
|
{
|
||||||
try
|
await new ContextSeed(db).Seed();
|
||||||
{
|
|
||||||
PrivilegedClients.Add(a.ClientId, new Player()
|
|
||||||
{
|
|
||||||
Name = a.Name,
|
|
||||||
ClientId = a.ClientId,
|
|
||||||
Level = a.Level,
|
|
||||||
PasswordSalt = a.PasswordSalt,
|
|
||||||
Password = a.Password
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (ArgumentException)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PrivilegedClients = (await ClientSvc.GetPrivilegedClients()).ToDictionary(_client => _client.ClientId);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region CONFIG
|
#region CONFIG
|
||||||
@ -138,7 +231,13 @@ namespace IW4MAdmin.Application
|
|||||||
if (newConfig.Servers == null)
|
if (newConfig.Servers == null)
|
||||||
{
|
{
|
||||||
ConfigHandler.Set(newConfig);
|
ConfigHandler.Set(newConfig);
|
||||||
newConfig.Servers = ConfigurationGenerator.GenerateServerConfig(new List<ServerConfiguration>());
|
newConfig.Servers = new List<ServerConfiguration>();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
newConfig.Servers.Add((ServerConfiguration)new ServerConfiguration().Generate());
|
||||||
|
} while (Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["SETUP_SERVER_SAVE"]));
|
||||||
|
|
||||||
config = newConfig;
|
config = newConfig;
|
||||||
await ConfigHandler.Save();
|
await ConfigHandler.Save();
|
||||||
}
|
}
|
||||||
@ -154,13 +253,15 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
|
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
|
||||||
{
|
{
|
||||||
config.WebfrontBindUrl = "http://127.0.0.1:1624";
|
config.WebfrontBindUrl = "http://0.0.0.0:1624";
|
||||||
await ConfigHandler.Save();
|
await ConfigHandler.Save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (config.Servers.Count == 0)
|
else if (config.Servers.Count == 0)
|
||||||
|
{
|
||||||
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
|
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252");
|
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252");
|
||||||
@ -176,18 +277,19 @@ namespace IW4MAdmin.Application
|
|||||||
await Plugin.OnLoadAsync(this);
|
await Plugin.OnLoadAsync(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception e)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.WriteError($"An error occured loading plugin {Plugin.Name}");
|
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_PLUGIN"]} {Plugin.Name}");
|
||||||
Logger.WriteDebug($"Exception: {e.Message}");
|
Logger.WriteDebug(ex.GetExceptionInfo());
|
||||||
Logger.WriteDebug($"Stack Trace: {e.StackTrace}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region COMMANDS
|
#region COMMANDS
|
||||||
if (ClientSvc.GetOwners().Result.Count == 0)
|
if (ClientSvc.GetOwners().Result.Count == 0)
|
||||||
|
{
|
||||||
Commands.Add(new COwner());
|
Commands.Add(new COwner());
|
||||||
|
}
|
||||||
|
|
||||||
Commands.Add(new CQuit());
|
Commands.Add(new CQuit());
|
||||||
Commands.Add(new CKick());
|
Commands.Add(new CKick());
|
||||||
@ -211,6 +313,7 @@ namespace IW4MAdmin.Application
|
|||||||
Commands.Add(new CListRules());
|
Commands.Add(new CListRules());
|
||||||
Commands.Add(new CPrivateMessage());
|
Commands.Add(new CPrivateMessage());
|
||||||
Commands.Add(new CFlag());
|
Commands.Add(new CFlag());
|
||||||
|
Commands.Add(new CUnflag());
|
||||||
Commands.Add(new CReport());
|
Commands.Add(new CReport());
|
||||||
Commands.Add(new CListReports());
|
Commands.Add(new CListReports());
|
||||||
Commands.Add(new CListBanInfo());
|
Commands.Add(new CListBanInfo());
|
||||||
@ -223,14 +326,20 @@ namespace IW4MAdmin.Application
|
|||||||
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 CNextMap());
|
||||||
|
|
||||||
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
|
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
|
||||||
|
{
|
||||||
Commands.Add(C);
|
Commands.Add(C);
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region INIT
|
#region INIT
|
||||||
async Task Init(ServerConfiguration Conf)
|
async Task Init(ServerConfiguration Conf)
|
||||||
{
|
{
|
||||||
|
// setup the event handler after the class is initialized
|
||||||
|
Handler = new GameEventHandler(this);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ServerInstance = new IW4MServer(this, Conf);
|
var ServerInstance = new IW4MServer(this, Conf);
|
||||||
@ -241,21 +350,26 @@ namespace IW4MAdmin.Application
|
|||||||
_servers.Add(ServerInstance);
|
_servers.Add(ServerInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.WriteVerbose($"Now monitoring {ServerInstance.Hostname}");
|
Logger.WriteVerbose($"{Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"]} {ServerInstance.Hostname}");
|
||||||
|
// add the start event for this server
|
||||||
|
|
||||||
// this way we can keep track of execution time and see if problems arise.
|
var e = new GameEvent()
|
||||||
var Status = new AsyncStatus(ServerInstance, UPDATE_FREQUENCY);
|
|
||||||
lock (TaskStatuses)
|
|
||||||
{
|
{
|
||||||
TaskStatuses.Add(Status);
|
Type = GameEvent.EventType.Start,
|
||||||
}
|
Data = $"{ServerInstance.GameName} started",
|
||||||
|
Owner = ServerInstance
|
||||||
|
};
|
||||||
|
|
||||||
|
Handler.AddEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (ServerException e)
|
catch (ServerException e)
|
||||||
{
|
{
|
||||||
Logger.WriteError($"Not monitoring server {Conf.IPAddress}:{Conf.Port} due to uncorrectable errors");
|
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"]} [{Conf.IPAddress}:{Conf.Port}]");
|
||||||
if (e.GetType() == typeof(DvarException))
|
if (e.GetType() == typeof(DvarException))
|
||||||
Logger.WriteDebug($"Could not get the dvar value for {(e as DvarException).Data["dvar_name"]} (ensure the server has a map loaded)");
|
{
|
||||||
|
Logger.WriteDebug($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"]} {(e as DvarException).Data["dvar_name"]} ({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})");
|
||||||
|
}
|
||||||
else if (e.GetType() == typeof(NetworkException))
|
else if (e.GetType() == typeof(NetworkException))
|
||||||
{
|
{
|
||||||
Logger.WriteDebug(e.Message);
|
Logger.WriteDebug(e.Message);
|
||||||
@ -267,126 +381,116 @@ namespace IW4MAdmin.Application
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
|
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
Running = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HeartBeatThread()
|
private async Task SendHeartbeat(object state)
|
||||||
{
|
{
|
||||||
bool successfulConnection = false;
|
var heartbeatState = (HeartbeatState)state;
|
||||||
restartConnection:
|
|
||||||
while (!successfulConnection)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
API.Master.Heartbeat.Send(this, true).Wait();
|
|
||||||
successfulConnection = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
successfulConnection = false;
|
|
||||||
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.Sleep(30000);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (Running)
|
while (Running)
|
||||||
{
|
{
|
||||||
Logger.WriteDebug("Sending heartbeat...");
|
if (!heartbeatState.Connected)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
API.Master.Heartbeat.Send(this).Wait();
|
try
|
||||||
}
|
|
||||||
catch (System.Net.Http.HttpRequestException e)
|
|
||||||
{
|
|
||||||
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (AggregateException e)
|
|
||||||
{
|
|
||||||
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
|
||||||
var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(RestEase.ApiException));
|
|
||||||
|
|
||||||
foreach (var ex in exceptions)
|
|
||||||
{
|
{
|
||||||
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
await Heartbeat.Send(this, true);
|
||||||
|
heartbeatState.Connected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
heartbeatState.Connected = false;
|
||||||
|
Logger.WriteWarning($"Could not connect to heartbeat server - {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Heartbeat.Send(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (System.Net.Http.HttpRequestException e)
|
||||||
|
{
|
||||||
|
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (AggregateException e)
|
||||||
|
{
|
||||||
|
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
||||||
|
var exceptions = e.InnerExceptions.Where(ex => ex.GetType() == typeof(RestEase.ApiException));
|
||||||
|
|
||||||
|
foreach (var ex in exceptions)
|
||||||
{
|
{
|
||||||
successfulConnection = false;
|
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
goto restartConnection;
|
{
|
||||||
|
heartbeatState.Connected = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
catch (RestEase.ApiException e)
|
catch (RestEase.ApiException e)
|
||||||
{
|
|
||||||
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
|
||||||
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
|
||||||
{
|
{
|
||||||
successfulConnection = false;
|
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
||||||
goto restartConnection;
|
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
heartbeatState.Connected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Thread.Sleep(30000);
|
}
|
||||||
|
await Task.Delay(30000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
Task.Run(() => HeartBeatThread());
|
// this needs to be run seperately from the main thread
|
||||||
while (Running || TaskStatuses.Count > 0)
|
var _ = Task.Run(() => SendHeartbeat(new HeartbeatState()));
|
||||||
|
_ = Task.Run(() => UpdateServerStates());
|
||||||
|
|
||||||
|
while (Running)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < TaskStatuses.Count; i++)
|
OnQuit.Wait();
|
||||||
{
|
OnQuit.Reset();
|
||||||
var Status = TaskStatuses[i];
|
|
||||||
|
|
||||||
// task is read to be rerun
|
|
||||||
if (Status.RequestedTask == null || Status.RequestedTask.Status == TaskStatus.RanToCompletion)
|
|
||||||
{
|
|
||||||
// remove the task when we want to quit and last run has finished
|
|
||||||
if (!Running)
|
|
||||||
{
|
|
||||||
TaskStatuses.RemoveAt(i);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// normal operation
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Status.Update(new Task<bool>(() => { return (Status.Dependant as Server).ProcessUpdatesAsync(Status.GetToken()).Result; }));
|
|
||||||
if (Status.RunAverage > 1000 + UPDATE_FREQUENCY && !(Status.Dependant as Server).Throttled)
|
|
||||||
Logger.WriteWarning($"Update task average execution is longer than desired for {(Status.Dependant as Server)} [{Status.RunAverage}ms]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Status.RequestedTask.Status == TaskStatus.Faulted)
|
|
||||||
{
|
|
||||||
Logger.WriteWarning($"Update task for {(Status.Dependant as Server)} faulted, restarting");
|
|
||||||
Status.Abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.Sleep(UPDATE_FREQUENCY);
|
|
||||||
}
|
}
|
||||||
#if !DEBUG
|
|
||||||
foreach (var S in Servers)
|
|
||||||
S.Broadcast(Utilities.CurrentLocalization.LocalizationSet["BROADCAST_OFFLINE"]).Wait();
|
|
||||||
#endif
|
|
||||||
_servers.Clear();
|
_servers.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
Running = false;
|
Running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ILogger GetLogger()
|
public ILogger GetLogger(long serverId)
|
||||||
{
|
{
|
||||||
return Logger;
|
if (Loggers.ContainsKey(serverId))
|
||||||
|
{
|
||||||
|
return Loggers[serverId];
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger newLogger;
|
||||||
|
|
||||||
|
if (serverId == 0)
|
||||||
|
{
|
||||||
|
newLogger = new Logger("IW4MAdmin-Manager");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Loggers.Add(serverId, newLogger);
|
||||||
|
return newLogger;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IList<MessageToken> GetMessageTokens()
|
public IList<MessageToken> GetMessageTokens()
|
||||||
@ -394,22 +498,59 @@ namespace IW4MAdmin.Application
|
|||||||
return MessageTokens;
|
return MessageTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IList<Player> GetActiveClients()
|
public IList<EFClient> GetActiveClients()
|
||||||
{
|
{
|
||||||
var ActiveClients = new List<Player>();
|
return _servers.SelectMany(s => s.Clients).Where(p => p != null).ToList();
|
||||||
|
|
||||||
foreach (var server in _servers)
|
|
||||||
ActiveClients.AddRange(server.Players.Where(p => p != null));
|
|
||||||
|
|
||||||
return ActiveClients;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientService GetClientService() => ClientSvc;
|
public ClientService GetClientService()
|
||||||
public AliasService GetAliasService() => AliasSvc;
|
{
|
||||||
public PenaltyService GetPenaltyService() => PenaltySvc;
|
return ClientSvc;
|
||||||
public IConfigurationHandler<ApplicationConfiguration> GetApplicationSettings() => ConfigHandler;
|
}
|
||||||
public IDictionary<int, Player> GetPrivilegedClients() => PrivilegedClients;
|
|
||||||
public IEventApi GetEventApi() => Api;
|
public AliasService GetAliasService()
|
||||||
public bool ShutdownRequested() => !Running;
|
{
|
||||||
|
return AliasSvc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PenaltyService GetPenaltyService()
|
||||||
|
{
|
||||||
|
return PenaltySvc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfigurationHandler<ApplicationConfiguration> GetApplicationSettings()
|
||||||
|
{
|
||||||
|
return ConfigHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDictionary<int, EFClient> GetPrivilegedClients()
|
||||||
|
{
|
||||||
|
return PrivilegedClients;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShutdownRequested()
|
||||||
|
{
|
||||||
|
return !Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEventHandler GetEventHandler()
|
||||||
|
{
|
||||||
|
return Handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHasEvent()
|
||||||
|
{
|
||||||
|
OnQuit.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<Assembly> GetPluginAssemblies()
|
||||||
|
{
|
||||||
|
return SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPageList GetPageList()
|
||||||
|
{
|
||||||
|
return PageList;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
60
Application/Migration/ConfigurationMigration.cs
Normal file
60
Application/Migration/ConfigurationMigration.cs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.Migration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// helps facilitate the migration of configs from one version and or location
|
||||||
|
/// to another
|
||||||
|
/// </summary>
|
||||||
|
class ConfigurationMigration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// moves existing configs from the root folder into a configs folder
|
||||||
|
/// </summary>
|
||||||
|
public static void MoveConfigFolder10518(ILogger log)
|
||||||
|
{
|
||||||
|
string currentDirectory = Utilities.OperatingDirectory;
|
||||||
|
|
||||||
|
// we don't want to do this for migrations or tests where the
|
||||||
|
// property isn't initialized or it's wrong
|
||||||
|
if (currentDirectory != null)
|
||||||
|
{
|
||||||
|
string configDirectory = Path.Join(currentDirectory, "Configuration");
|
||||||
|
|
||||||
|
if (!Directory.Exists(configDirectory))
|
||||||
|
{
|
||||||
|
log?.WriteDebug($"Creating directory for configs {configDirectory}");
|
||||||
|
Directory.CreateDirectory(configDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var configurationFiles = Directory.EnumerateFiles(currentDirectory, "*.json")
|
||||||
|
.Select(f => f.Split(Path.DirectorySeparatorChar).Last())
|
||||||
|
.Where(f => f.Count(c => c == '.') == 1);
|
||||||
|
|
||||||
|
foreach (var configFile in configurationFiles)
|
||||||
|
{
|
||||||
|
log?.WriteDebug($"Moving config file {configFile}");
|
||||||
|
string destinationPath = Path.Join("Configuration", configFile);
|
||||||
|
if (!File.Exists(destinationPath))
|
||||||
|
{
|
||||||
|
File.Move(configFile, destinationPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(Path.Join("Database", "Database.db")) &&
|
||||||
|
File.Exists("Database.db"))
|
||||||
|
{
|
||||||
|
log?.WriteDebug("Moving database file");
|
||||||
|
File.Move("Database.db", Path.Join("Database", "Database.db"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Application.Misc
|
|
||||||
{
|
|
||||||
public class VPNCheck
|
|
||||||
{
|
|
||||||
public static async Task<bool> UsingVPN(string ip, string apiKey)
|
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
return await Task.FromResult(false);
|
|
||||||
|
|
||||||
#else
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var RequestClient = new System.Net.Http.HttpClient())
|
|
||||||
{
|
|
||||||
RequestClient.DefaultRequestHeaders.Add("X-Key", apiKey);
|
|
||||||
string response = await RequestClient.GetStringAsync($"http://v2.api.iphub.info/ip/{ip}");
|
|
||||||
var responseJson = JsonConvert.DeserializeObject<JObject>(response);
|
|
||||||
int blockType = Convert.ToInt32(responseJson["block"]);
|
|
||||||
return blockType == 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
26
Application/PageList.cs
Normal file
26
Application/PageList.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// implementatin of IPageList that supports basic
|
||||||
|
/// pages title and page location for webfront
|
||||||
|
/// </summary>
|
||||||
|
class PageList : IPageList
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pages dictionary
|
||||||
|
/// Key = page name
|
||||||
|
/// Value = page location (url)
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, string> Pages { get; set; }
|
||||||
|
|
||||||
|
public PageList()
|
||||||
|
{
|
||||||
|
Pages = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
|
||||||
-->
|
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup>
|
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
|
||||||
<Configuration>Release</Configuration>
|
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
|
||||||
<PublishDir>C:\Projects\IW4M-Admin\Publish\Windows</PublishDir>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
21
Application/RconParsers/IW3RConParser.cs
Normal file
21
Application/RconParsers/IW3RConParser.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using SharedLibraryCore.RCon;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
|
{
|
||||||
|
class IW3RConParser : IW4RConParser
|
||||||
|
{
|
||||||
|
private static readonly CommandPrefix Prefixes = new CommandPrefix()
|
||||||
|
{
|
||||||
|
Tell = "tell {0} {1}",
|
||||||
|
Say = "say {0}",
|
||||||
|
Kick = "clientkick {0} \"{1}\"",
|
||||||
|
Ban = "clientkick {0} \"{1}\"",
|
||||||
|
TempBan = "tempbanclient {0} \"{1}\""
|
||||||
|
};
|
||||||
|
|
||||||
|
public override CommandPrefix GetCommandPrefixes() => Prefixes;
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +1,19 @@
|
|||||||
using System;
|
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.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using SharedLibraryCore.Interfaces;
|
|
||||||
using SharedLibraryCore.Objects;
|
|
||||||
using SharedLibraryCore;
|
|
||||||
using SharedLibraryCore.RCon;
|
|
||||||
using SharedLibraryCore.Exceptions;
|
|
||||||
|
|
||||||
namespace Application.RconParsers
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
{
|
{
|
||||||
class IW4RConParser : IRConParser
|
class IW4RConParser : IRConParser
|
||||||
{
|
{
|
||||||
private static CommandPrefix Prefixes = new CommandPrefix()
|
private static readonly CommandPrefix Prefixes = new CommandPrefix()
|
||||||
{
|
{
|
||||||
Tell = "tellraw {0} {1}",
|
Tell = "tellraw {0} {1}",
|
||||||
Say = "sayraw {0}",
|
Say = "sayraw {0}",
|
||||||
@ -22,10 +21,13 @@ namespace Application.RconParsers
|
|||||||
Ban = "clientkick {0} \"{1}\"",
|
Ban = "clientkick {0} \"{1}\"",
|
||||||
TempBan = "tempbanclient {0} \"{1}\""
|
TempBan = "tempbanclient {0} \"{1}\""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly string StatusRegex = @"^( *[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]+) *$";
|
||||||
|
|
||||||
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
|
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
|
||||||
{
|
{
|
||||||
return (await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command)).Skip(1).ToArray();
|
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)
|
public async Task<Dvar<T>> GetDvarAsync<T>(Connection connection, string dvarName)
|
||||||
@ -58,7 +60,7 @@ namespace Application.RconParsers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Player>> GetStatusAsync(Connection connection)
|
public async Task<List<EFClient>> GetStatusAsync(Connection connection)
|
||||||
{
|
{
|
||||||
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status");
|
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status");
|
||||||
return ClientsFromStatus(response);
|
return ClientsFromStatus(response);
|
||||||
@ -69,46 +71,76 @@ namespace Application.RconParsers
|
|||||||
return (await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"set {dvarName} {dvarValue}")).Length > 0;
|
return (await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"set {dvarName} {dvarValue}")).Length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandPrefix GetCommandPrefixes() => Prefixes;
|
public virtual CommandPrefix GetCommandPrefixes()
|
||||||
|
|
||||||
private List<Player> ClientsFromStatus(string[] Status)
|
|
||||||
{
|
{
|
||||||
List<Player> StatusPlayers = new List<Player>();
|
return Prefixes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EFClient> ClientsFromStatus(string[] Status)
|
||||||
|
{
|
||||||
|
List<EFClient> StatusPlayers = new List<EFClient>();
|
||||||
|
|
||||||
if (Status.Length < 4)
|
if (Status.Length < 4)
|
||||||
|
{
|
||||||
throw new ServerException("Unexpected status response received");
|
throw new ServerException("Unexpected status response received");
|
||||||
|
}
|
||||||
|
|
||||||
|
int validMatches = 0;
|
||||||
foreach (String S in Status)
|
foreach (String S in Status)
|
||||||
{
|
{
|
||||||
String responseLine = S.Trim();
|
String responseLine = S.Trim();
|
||||||
|
|
||||||
if (Regex.Matches(responseLine, @" *^\d+", RegexOptions.IgnoreCase).Count > 0)
|
var regex = Regex.Match(responseLine, StatusRegex, RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
if (regex.Success)
|
||||||
{
|
{
|
||||||
String[] playerInfo = responseLine.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
validMatches++;
|
||||||
int cID = -1;
|
int clientNumber = int.Parse(regex.Groups[1].Value);
|
||||||
int Ping = -1;
|
int score = int.Parse(regex.Groups[2].Value);
|
||||||
Int32.TryParse(playerInfo[2], out Ping);
|
|
||||||
String cName = Encoding.UTF8.GetString(Encoding.Convert(Utilities.EncodingType, Encoding.UTF8, Utilities.EncodingType.GetBytes(responseLine.Substring(46, 18).StripColors().Trim())));
|
int ping = 999;
|
||||||
long npID = Regex.Match(responseLine, @"([a-z]|[0-9]){16}", RegexOptions.IgnoreCase).Value.ConvertLong();
|
|
||||||
int.TryParse(playerInfo[0], out cID);
|
// their state can be CNCT, ZMBI etc
|
||||||
var regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}");
|
if (regex.Groups[3].Value.Length <= 3)
|
||||||
int cIP = regex.Value.Split(':')[0].ConvertToIP();
|
|
||||||
regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+");
|
|
||||||
int score = Int32.Parse(regex.Value.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)[1]);
|
|
||||||
Player P = new Player()
|
|
||||||
{
|
{
|
||||||
Name = cName,
|
ping = int.Parse(regex.Groups[3].Value);
|
||||||
NetworkId = npID,
|
}
|
||||||
ClientNumber = cID,
|
|
||||||
IPAddress = cIP,
|
long networkId = regex.Groups[4].Value.ConvertLong();
|
||||||
Ping = Ping,
|
string name = regex.Groups[5].Value.StripColors().Trim();
|
||||||
|
int? ip = regex.Groups[7].Value.Split(':')[0].ConvertToIP();
|
||||||
|
|
||||||
|
var client = new EFClient()
|
||||||
|
{
|
||||||
|
CurrentAlias = new EFAlias()
|
||||||
|
{
|
||||||
|
Name = name
|
||||||
|
},
|
||||||
|
NetworkId = networkId,
|
||||||
|
ClientNumber = clientNumber,
|
||||||
|
IPAddress = ip,
|
||||||
|
Ping = ping,
|
||||||
Score = score,
|
Score = score,
|
||||||
IsBot = npID == -1
|
IsBot = ip == null,
|
||||||
|
State = EFClient.ClientState.Connecting
|
||||||
};
|
};
|
||||||
StatusPlayers.Add(P);
|
|
||||||
|
// 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;
|
return StatusPlayers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
171
Application/RconParsers/IW5MRConParser.cs
Normal file
171
Application/RconParsers/IW5MRConParser.cs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using SharedLibraryCore.Objects;
|
||||||
|
using SharedLibraryCore.RCon;
|
||||||
|
using SharedLibraryCore.Exceptions;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
|
{
|
||||||
|
public class IW5MRConParser : IRConParser
|
||||||
|
{
|
||||||
|
private static readonly CommandPrefix Prefixes = new CommandPrefix()
|
||||||
|
{
|
||||||
|
Tell = "tell {0} {1}",
|
||||||
|
Say = "say {0}",
|
||||||
|
Kick = "dropClient {0} \"{1}\"",
|
||||||
|
Ban = "dropClient {0} \"{1}\"",
|
||||||
|
TempBan = "dropClient {0} \"{1}\""
|
||||||
|
};
|
||||||
|
|
||||||
|
public CommandPrefix GetCommandPrefixes() => Prefixes;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// why can't this be real :(
|
||||||
|
if (dvarName == "version")
|
||||||
|
return new Dvar<T>(dvarName)
|
||||||
|
{
|
||||||
|
Value = (T)Convert.ChangeType("IW5 MP 1.9 build 461 Fri Sep 14 00:04:28 2012 win-x86", typeof(T))
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dvarName == "shortversion")
|
||||||
|
return new Dvar<T>(dvarName)
|
||||||
|
{
|
||||||
|
Value = (T)Convert.ChangeType("1.9", typeof(T))
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dvarName == "mapname")
|
||||||
|
return new Dvar<T>(dvarName)
|
||||||
|
{
|
||||||
|
Value = (T)Convert.ChangeType("Unknown", typeof(T))
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dvarName == "g_gametype")
|
||||||
|
return new Dvar<T>(dvarName)
|
||||||
|
{
|
||||||
|
Value = (T)Convert.ChangeType("Unknown", typeof(T))
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dvarName == "fs_game")
|
||||||
|
return new Dvar<T>(dvarName)
|
||||||
|
{
|
||||||
|
Value = (T)Convert.ChangeType("", typeof(T))
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dvarName == "g_logsync")
|
||||||
|
return new Dvar<T>(dvarName)
|
||||||
|
{
|
||||||
|
Value = (T)Convert.ChangeType(1, typeof(T))
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dvarName == "fs_basepath")
|
||||||
|
return new Dvar<T>(dvarName)
|
||||||
|
{
|
||||||
|
Value = (T)Convert.ChangeType("", typeof(T))
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.DVAR, dvarName);
|
||||||
|
|
||||||
|
if (LineSplit.Length < 4)
|
||||||
|
{
|
||||||
|
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[3].StripColors(), @"\^[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);
|
||||||
|
|
||||||
|
// this happens when the client is in a zombie state
|
||||||
|
if (playerInfo.Length < 5)
|
||||||
|
continue;
|
||||||
|
int clientId = -1;
|
||||||
|
int Ping = -1;
|
||||||
|
|
||||||
|
Int32.TryParse(playerInfo[2], out Ping);
|
||||||
|
string name = Encoding.UTF8.GetString(Encoding.Convert(Utilities.EncodingType, Encoding.UTF8, Utilities.EncodingType.GetBytes(responseLine.Substring(23, 15).StripColors().Trim())));
|
||||||
|
long networkId = 0;//playerInfo[4].ConvertLong();
|
||||||
|
int.TryParse(playerInfo[0], out clientId);
|
||||||
|
var regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}");
|
||||||
|
int? ipAddress = regex.Value.Split(':')[0].ConvertToIP();
|
||||||
|
regex = Regex.Match(responseLine, @" +(\d+ +){3}");
|
||||||
|
int score = Int32.Parse(regex.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries)[0]);
|
||||||
|
|
||||||
|
var p = new EFClient()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
NetworkId = networkId,
|
||||||
|
ClientNumber = clientId,
|
||||||
|
IPAddress = ipAddress,
|
||||||
|
Ping = Ping,
|
||||||
|
Score = score,
|
||||||
|
IsBot = false,
|
||||||
|
State = EFClient.ClientState.Connecting
|
||||||
|
};
|
||||||
|
|
||||||
|
StatusPlayers.Add(p);
|
||||||
|
|
||||||
|
if (p.IsBot)
|
||||||
|
p.NetworkId = -p.ClientNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusPlayers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,56 +9,19 @@ using SharedLibraryCore.Objects;
|
|||||||
using SharedLibraryCore.RCon;
|
using SharedLibraryCore.RCon;
|
||||||
using SharedLibraryCore.Exceptions;
|
using SharedLibraryCore.Exceptions;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Linq;
|
using SharedLibraryCore.Database.Models;
|
||||||
using System.Net.Http;
|
|
||||||
|
|
||||||
namespace Application.RconParsers
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
{
|
{
|
||||||
public class T6MRConParser : IRConParser
|
public class T6MRConParser : IRConParser
|
||||||
{
|
{
|
||||||
class T6MResponse
|
private static readonly CommandPrefix Prefixes = new CommandPrefix()
|
||||||
{
|
|
||||||
public class SInfo
|
|
||||||
{
|
|
||||||
public short Com_maxclients { get; set; }
|
|
||||||
public string Game { get; set; }
|
|
||||||
public string Gametype { get; set; }
|
|
||||||
public string Mapname { get; set; }
|
|
||||||
public short NumBots { get; set; }
|
|
||||||
public short NumClients { get; set; }
|
|
||||||
public short Round { get; set; }
|
|
||||||
public string Sv_hostname { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PInfo
|
|
||||||
{
|
|
||||||
public short Assists { get; set; }
|
|
||||||
public string Clan { get; set; }
|
|
||||||
public short Deaths { get; set; }
|
|
||||||
public short Downs { get; set; }
|
|
||||||
public short Headshots { get; set; }
|
|
||||||
public short Id { get; set; }
|
|
||||||
public bool IsBot { get; set; }
|
|
||||||
public short Kills { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
public short Ping { get; set; }
|
|
||||||
public short Revives { get; set; }
|
|
||||||
public int Score { get; set; }
|
|
||||||
public long Xuid { get; set; }
|
|
||||||
public string Ip { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public SInfo Info { get; set; }
|
|
||||||
public PInfo[] Players { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CommandPrefix Prefixes = new CommandPrefix()
|
|
||||||
{
|
{
|
||||||
Tell = "tell {0} {1}",
|
Tell = "tell {0} {1}",
|
||||||
Say = "say {0}",
|
Say = "say {0}",
|
||||||
Kick = "clientKick {0}",
|
Kick = "clientkick_for_reason {0} \"{1}\"",
|
||||||
Ban = "clientKick {0}",
|
Ban = "clientkick_for_reason {0} \"{1}\"",
|
||||||
TempBan = "clientKick {0}"
|
TempBan = "clientkick_for_reason {0} \"{1}\""
|
||||||
};
|
};
|
||||||
|
|
||||||
public CommandPrefix GetCommandPrefixes() => Prefixes;
|
public CommandPrefix GetCommandPrefixes() => Prefixes;
|
||||||
@ -73,7 +36,6 @@ namespace Application.RconParsers
|
|||||||
{
|
{
|
||||||
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"get {dvarName}");
|
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"get {dvarName}");
|
||||||
|
|
||||||
|
|
||||||
if (LineSplit.Length < 2)
|
if (LineSplit.Length < 2)
|
||||||
{
|
{
|
||||||
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
|
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
|
||||||
@ -99,12 +61,10 @@ namespace Application.RconParsers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Player>> GetStatusAsync(Connection connection)
|
public async Task<List<EFClient>> GetStatusAsync(Connection connection)
|
||||||
{
|
{
|
||||||
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status");
|
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status");
|
||||||
return ClientsFromStatus(response);
|
return ClientsFromStatus(response);
|
||||||
|
|
||||||
//return ClientsFromResponse(connection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
|
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
|
||||||
@ -114,45 +74,9 @@ namespace Application.RconParsers
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<Player>> ClientsFromResponse(Connection conn)
|
private List<EFClient> ClientsFromStatus(string[] status)
|
||||||
{
|
{
|
||||||
using (var client = new HttpClient())
|
List<EFClient> StatusPlayers = new List<EFClient>();
|
||||||
{
|
|
||||||
client.BaseAddress = new Uri($"http://{conn.Endpoint.Address}:{conn.Endpoint.Port}/");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var parameters = new FormUrlEncodedContent(new[]
|
|
||||||
{
|
|
||||||
new KeyValuePair<string, string>("rcon_password", conn.RConPassword)
|
|
||||||
});
|
|
||||||
|
|
||||||
var serverResponse = await client.PostAsync("/info", parameters);
|
|
||||||
var serverResponseObject = Newtonsoft.Json.JsonConvert.DeserializeObject<T6MResponse>(await serverResponse.Content.ReadAsStringAsync());
|
|
||||||
|
|
||||||
return serverResponseObject.Players.Select(p => new Player()
|
|
||||||
{
|
|
||||||
Name = p.Name,
|
|
||||||
NetworkId = p.Xuid,
|
|
||||||
ClientNumber = p.Id,
|
|
||||||
IPAddress = p.Ip.Split(':')[0].ConvertToIP(),
|
|
||||||
Ping = p.Ping,
|
|
||||||
Score = p.Score,
|
|
||||||
IsBot = p.IsBot,
|
|
||||||
}).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (HttpRequestException e)
|
|
||||||
{
|
|
||||||
throw new NetworkException(e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private List<Player> ClientsFromStatus(string[] status)
|
|
||||||
{
|
|
||||||
List<Player> StatusPlayers = new List<Player>();
|
|
||||||
|
|
||||||
foreach (string statusLine in status)
|
foreach (string statusLine in status)
|
||||||
{
|
{
|
||||||
@ -173,22 +97,24 @@ namespace Application.RconParsers
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
Ping = 1;
|
Ping = 1;
|
||||||
#endif
|
#endif
|
||||||
int ipAddress = regex.Value.Split(':')[0].ConvertToIP();
|
int? ipAddress = regex.Value.Split(':')[0].ConvertToIP();
|
||||||
regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+");
|
regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+");
|
||||||
int score = 0;
|
var p = new EFClient()
|
||||||
// todo: fix this when T6M score is valid ;)
|
|
||||||
//int score = Int32.Parse(playerInfo[1]);
|
|
||||||
|
|
||||||
StatusPlayers.Add(new Player()
|
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
NetworkId = networkId,
|
NetworkId = networkId,
|
||||||
ClientNumber = clientId,
|
ClientNumber = clientId,
|
||||||
IPAddress = ipAddress,
|
IPAddress = ipAddress,
|
||||||
Ping = Ping,
|
Ping = Ping,
|
||||||
Score = score,
|
Score = 0,
|
||||||
IsBot = networkId < 1
|
State = EFClient.ClientState.Connecting,
|
||||||
});
|
IsBot = ipAddress == null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (p.IsBot)
|
||||||
|
p.NetworkId = -p.ClientNumber;
|
||||||
|
|
||||||
|
StatusPlayers.Add(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
31
Application/SharedGUIDKick.js
Normal file
31
Application/SharedGUIDKick.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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) {
|
||||||
|
}
|
||||||
|
};
|
62
Application/VPNDetection.js
Normal file
62
Application/VPNDetection.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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) {
|
||||||
|
}
|
||||||
|
};
|
214
DiscordWebhook/DiscordWebhook.py
Normal file
214
DiscordWebhook/DiscordWebhook.py
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import collections
|
||||||
|
import os
|
||||||
|
|
||||||
|
# the following classes model the discord webhook api parameters
|
||||||
|
class WebhookAuthor():
|
||||||
|
def __init__(self, name=None, url=None, icon_url=None):
|
||||||
|
if name:
|
||||||
|
self.name = name
|
||||||
|
if url:
|
||||||
|
self.url = url
|
||||||
|
if icon_url:
|
||||||
|
self.icon_url = icon_url
|
||||||
|
|
||||||
|
class WebhookField():
|
||||||
|
def __init__(self, name=None, value=None, inline=False):
|
||||||
|
if name:
|
||||||
|
self.name = name
|
||||||
|
if value:
|
||||||
|
self.value = value
|
||||||
|
if inline:
|
||||||
|
self.inline = inline
|
||||||
|
|
||||||
|
class WebhookEmbed():
|
||||||
|
def __init__(self):
|
||||||
|
self.author = ''
|
||||||
|
self.title = ''
|
||||||
|
self.url = ''
|
||||||
|
self.description = ''
|
||||||
|
self.color = 0
|
||||||
|
self.fields = []
|
||||||
|
self.thumbnail = {}
|
||||||
|
|
||||||
|
class WebhookParams():
|
||||||
|
def __init__(self, username=None, avatar_url=None, content=None):
|
||||||
|
self.username = ''
|
||||||
|
self.avatar_url = ''
|
||||||
|
self.content = ''
|
||||||
|
self.embeds = []
|
||||||
|
|
||||||
|
# quick way to convert all the objects to a nice json object
|
||||||
|
def to_json(self):
|
||||||
|
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
|
||||||
|
|
||||||
|
# gets the relative link to a user's profile
|
||||||
|
def get_client_profile(profile_id):
|
||||||
|
return u'{}/Client/ProfileAsync/{}'.format(base_url, profile_id)
|
||||||
|
|
||||||
|
def get_client_profile_markdown(client_name, profile_id):
|
||||||
|
return u'[{}]({})'.format(client_name, get_client_profile(profile_id))
|
||||||
|
|
||||||
|
#todo: exception handling for opening the file
|
||||||
|
if os.getenv("DEBUG"):
|
||||||
|
config_file_name = 'config.dev.json'
|
||||||
|
else:
|
||||||
|
config_file_name = 'config.json'
|
||||||
|
|
||||||
|
with open(config_file_name) as json_config_file:
|
||||||
|
json_config = json.load(json_config_file)
|
||||||
|
|
||||||
|
# this should be an URL to an IP or FQN to an IW4MAdmin instance
|
||||||
|
# ie http://127.0.0.1 or http://IW4MAdmin.com
|
||||||
|
base_url = json_config['IW4MAdminUrl']
|
||||||
|
end_point = '/api/event'
|
||||||
|
request_url = base_url + end_point
|
||||||
|
# this should be the full discord webhook url
|
||||||
|
# ie https://discordapp.com/api/webhooks/<id>/<token>
|
||||||
|
discord_webhook_notification_url = json_config['DiscordWebhookNotificationUrl']
|
||||||
|
discord_webhook_information_url = json_config['DiscordWebhookInformationUrl']
|
||||||
|
# this should be the numerical id of the discord group
|
||||||
|
# 12345678912345678
|
||||||
|
notify_role_ids = json_config['NotifyRoleIds']
|
||||||
|
|
||||||
|
def get_new_events():
|
||||||
|
events = []
|
||||||
|
response = requests.get(request_url)
|
||||||
|
data = response.json()
|
||||||
|
should_notify = False
|
||||||
|
|
||||||
|
for event in data:
|
||||||
|
# commonly used event info items
|
||||||
|
event_type = event['eventType']['name']
|
||||||
|
server_name = event['ownerEntity']['name']
|
||||||
|
|
||||||
|
if event['originEntity']:
|
||||||
|
origin_client_name = event['originEntity']['name']
|
||||||
|
origin_client_id = int(event['originEntity']['id'])
|
||||||
|
|
||||||
|
if event['targetEntity']:
|
||||||
|
target_client_name = event['targetEntity']['name'] or ''
|
||||||
|
target_client_id = int(event['targetEntity']['id']) or 0
|
||||||
|
|
||||||
|
webhook_item = WebhookParams()
|
||||||
|
webhook_item_embed = WebhookEmbed()
|
||||||
|
|
||||||
|
#todo: the following don't need to be generated every time, as it says the same
|
||||||
|
webhook_item.username = 'IW4MAdmin'
|
||||||
|
webhook_item.avatar_url = 'https://raidmax.org/IW4MAdmin/img/iw4adminicon-3.png'
|
||||||
|
webhook_item_embed.color = 31436
|
||||||
|
webhook_item_embed.url = base_url
|
||||||
|
webhook_item_embed.thumbnail = { 'url' : 'https://raidmax.org/IW4MAdmin/img/iw4adminicon-3.png' }
|
||||||
|
webhook_item.embeds.append(webhook_item_embed)
|
||||||
|
|
||||||
|
# the server should be visible on all event types
|
||||||
|
server_field = WebhookField('Server', server_name)
|
||||||
|
webhook_item_embed.fields.append(server_field)
|
||||||
|
|
||||||
|
role_ids_string = ''
|
||||||
|
for id in notify_role_ids:
|
||||||
|
role_ids_string += '\r\n<@&{}>\r\n'.format(id)
|
||||||
|
|
||||||
|
if event_type == 'Report':
|
||||||
|
report_reason = event['extraInfo']
|
||||||
|
|
||||||
|
report_reason_field = WebhookField('Reason', report_reason)
|
||||||
|
reported_by_field = WebhookField('By', get_client_profile_markdown(origin_client_name, origin_client_id))
|
||||||
|
reported_field = WebhookField('Reported Player',get_client_profile_markdown(target_client_name, target_client_id))
|
||||||
|
|
||||||
|
# add each fields to the embed
|
||||||
|
webhook_item_embed.title = 'Player Reported'
|
||||||
|
webhook_item_embed.fields.append(reported_field)
|
||||||
|
webhook_item_embed.fields.append(reported_by_field)
|
||||||
|
webhook_item_embed.fields.append(report_reason_field)
|
||||||
|
|
||||||
|
should_notify = True
|
||||||
|
|
||||||
|
elif event_type == 'Ban':
|
||||||
|
ban_reason = event['extraInfo']
|
||||||
|
ban_reason_field = WebhookField('Reason', ban_reason)
|
||||||
|
banned_by_field = WebhookField('By', get_client_profile_markdown(origin_client_name, origin_client_id))
|
||||||
|
banned_field = WebhookField('Banned Player', get_client_profile_markdown(target_client_name, target_client_id))
|
||||||
|
|
||||||
|
# add each fields to the embed
|
||||||
|
webhook_item_embed.title = 'Player Banned'
|
||||||
|
webhook_item_embed.fields.append(banned_field)
|
||||||
|
webhook_item_embed.fields.append(banned_by_field)
|
||||||
|
webhook_item_embed.fields.append(ban_reason_field)
|
||||||
|
|
||||||
|
should_notify = True
|
||||||
|
|
||||||
|
elif event_type == 'Connect':
|
||||||
|
connected_field = WebhookField('Connected Player', get_client_profile_markdown(origin_client_name, origin_client_id))
|
||||||
|
webhook_item_embed.title = 'Player Connected'
|
||||||
|
webhook_item_embed.fields.append(connected_field)
|
||||||
|
|
||||||
|
elif event_type == 'Disconnect':
|
||||||
|
disconnected_field = WebhookField('Disconnected Player', get_client_profile_markdown(origin_client_name, origin_client_id))
|
||||||
|
webhook_item_embed.title = 'Player Disconnected'
|
||||||
|
webhook_item_embed.fields.append(disconnected_field)
|
||||||
|
|
||||||
|
elif event_type == 'Say':
|
||||||
|
say_client_field = WebhookField('Player', get_client_profile_markdown(origin_client_name, origin_client_id))
|
||||||
|
message_field = WebhookField('Message', event['extraInfo'])
|
||||||
|
|
||||||
|
webhook_item_embed.title = 'Message From Player'
|
||||||
|
webhook_item_embed.fields.append(say_client_field)
|
||||||
|
webhook_item_embed.fields.append(message_field)
|
||||||
|
|
||||||
|
#if event_type == 'ScriptKill' or event_type == 'Kill':
|
||||||
|
# kill_str = '{} killed {}'.format(get_client_profile_markdown(origin_client_name, origin_client_id),
|
||||||
|
# get_client_profile_markdown(target_client_name, target_client_id))
|
||||||
|
# killed_field = WebhookField('Kill Information', kill_str)
|
||||||
|
# webhook_item_embed.title = 'Player Killed'
|
||||||
|
# webhook_item_embed.fields.append(killed_field)
|
||||||
|
|
||||||
|
#todo: handle other events
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
#make sure there's at least one group to notify
|
||||||
|
if len(notify_role_ids) > 0:
|
||||||
|
# unfortunately only the content can be used to to notify members in groups
|
||||||
|
#embed content shows the role but doesn't notify
|
||||||
|
webhook_item.content = role_ids_string
|
||||||
|
|
||||||
|
events.append({'item' : webhook_item, 'notify' : should_notify})
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
# sends the data to the webhook location
|
||||||
|
def execute_webhook(data):
|
||||||
|
for event in data:
|
||||||
|
event_json = event['item'].to_json()
|
||||||
|
url = None
|
||||||
|
|
||||||
|
if event['notify']:
|
||||||
|
url = discord_webhook_notification_url
|
||||||
|
else:
|
||||||
|
if len(discord_webhook_information_url) > 0:
|
||||||
|
url = discord_webhook_information_url
|
||||||
|
|
||||||
|
if url :
|
||||||
|
response = requests.post(url,
|
||||||
|
data=event_json,
|
||||||
|
headers={'Content-type' : 'application/json'})
|
||||||
|
|
||||||
|
# grabs new events and executes the webhook fo each valid event
|
||||||
|
def run():
|
||||||
|
failed_count = 1
|
||||||
|
print('starting polling for events')
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
new_events = get_new_events()
|
||||||
|
execute_webhook(new_events)
|
||||||
|
except Exception as e:
|
||||||
|
print('failed to get new events ({})'.format(failed_count))
|
||||||
|
print(e)
|
||||||
|
failed_count += 1
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
99
DiscordWebhook/DiscordWebhook.pyproj
Normal file
99
DiscordWebhook/DiscordWebhook.pyproj
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
<ProjectGuid>15a81d6e-7502-46ce-8530-0647a380b5f4</ProjectGuid>
|
||||||
|
<ProjectHome>.</ProjectHome>
|
||||||
|
<StartupFile>DiscordWebhook.py</StartupFile>
|
||||||
|
<SearchPath>
|
||||||
|
</SearchPath>
|
||||||
|
<WorkingDirectory>.</WorkingDirectory>
|
||||||
|
<OutputPath>.</OutputPath>
|
||||||
|
<Name>DiscordWebhook</Name>
|
||||||
|
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
|
||||||
|
<RootNamespace>DiscordWebhook</RootNamespace>
|
||||||
|
<InterpreterId>MSBuild|env|$(MSBuildProjectFullPath)</InterpreterId>
|
||||||
|
<IsWindowsApplication>False</IsWindowsApplication>
|
||||||
|
<LaunchProvider>Standard Python launcher</LaunchProvider>
|
||||||
|
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
|
||||||
|
<Environment>DEBUG=True</Environment>
|
||||||
|
<PublishUrl>C:\Projects\IW4M-Admin\Publish\WindowsPrerelease\DiscordWebhook</PublishUrl>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Prerelease' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
|
||||||
|
<OutputPath>bin\Prerelease\</OutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="DiscordWebhook.py" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Interpreter Include="env\">
|
||||||
|
<Id>env</Id>
|
||||||
|
<Version>3.6</Version>
|
||||||
|
<Description>env (Python 3.6 (64-bit))</Description>
|
||||||
|
<InterpreterPath>Scripts\python.exe</InterpreterPath>
|
||||||
|
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
|
||||||
|
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
|
||||||
|
<Architecture>X64</Architecture>
|
||||||
|
</Interpreter>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="config.json">
|
||||||
|
<Publish>True</Publish>
|
||||||
|
</Content>
|
||||||
|
<Content Include="requirements.txt">
|
||||||
|
<Publish>True</Publish>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
|
||||||
|
<!-- Uncomment the CoreCompile target to enable the Build command in
|
||||||
|
Visual Studio and specify your pre- and post-build commands in
|
||||||
|
the BeforeBuild and AfterBuild targets below. -->
|
||||||
|
<!--<Target Name="CoreCompile" />-->
|
||||||
|
<Target Name="BeforeBuild">
|
||||||
|
</Target>
|
||||||
|
<Target Name="AfterBuild">
|
||||||
|
</Target>
|
||||||
|
<ProjectExtensions>
|
||||||
|
<VisualStudio>
|
||||||
|
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
|
||||||
|
<WebProjectProperties>
|
||||||
|
<AutoAssignPort>True</AutoAssignPort>
|
||||||
|
<UseCustomServer>True</UseCustomServer>
|
||||||
|
<CustomServerUrl>http://localhost</CustomServerUrl>
|
||||||
|
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||||
|
</WebProjectProperties>
|
||||||
|
</FlavorProperties>
|
||||||
|
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
|
||||||
|
<WebProjectProperties>
|
||||||
|
<StartPageUrl>
|
||||||
|
</StartPageUrl>
|
||||||
|
<StartAction>CurrentPage</StartAction>
|
||||||
|
<AspNetDebugging>True</AspNetDebugging>
|
||||||
|
<SilverlightDebugging>False</SilverlightDebugging>
|
||||||
|
<NativeDebugging>False</NativeDebugging>
|
||||||
|
<SQLDebugging>False</SQLDebugging>
|
||||||
|
<ExternalProgram>
|
||||||
|
</ExternalProgram>
|
||||||
|
<StartExternalURL>
|
||||||
|
</StartExternalURL>
|
||||||
|
<StartCmdLineArguments>
|
||||||
|
</StartCmdLineArguments>
|
||||||
|
<StartWorkingDirectory>
|
||||||
|
</StartWorkingDirectory>
|
||||||
|
<EnableENC>False</EnableENC>
|
||||||
|
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
|
||||||
|
</WebProjectProperties>
|
||||||
|
</FlavorProperties>
|
||||||
|
</VisualStudio>
|
||||||
|
</ProjectExtensions>
|
||||||
|
</Project>
|
6
DiscordWebhook/config.json
Normal file
6
DiscordWebhook/config.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"IW4MAdminUrl": "",
|
||||||
|
"DiscordWebhookNotificationUrl": "",
|
||||||
|
"DiscordWebhookInformationUrl": "",
|
||||||
|
"NotifyRoleIds": []
|
||||||
|
}
|
7
DiscordWebhook/requirements.txt
Normal file
7
DiscordWebhook/requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
certifi>=2018.4.16
|
||||||
|
chardet>=3.0.4
|
||||||
|
idna>=2.7
|
||||||
|
pip>=18.0
|
||||||
|
requests>=2.19.1
|
||||||
|
setuptools>=39.0.1
|
||||||
|
urllib3>=1.23
|
105
GameLogServer/GameLogServer.pyproj
Normal file
105
GameLogServer/GameLogServer.pyproj
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
|
||||||
|
<PropertyGroup>
|
||||||
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
<ProjectGuid>42efda12-10d3-4c40-a210-9483520116bc</ProjectGuid>
|
||||||
|
<ProjectHome>.</ProjectHome>
|
||||||
|
<ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
|
||||||
|
<StartupFile>runserver.py</StartupFile>
|
||||||
|
<SearchPath>
|
||||||
|
</SearchPath>
|
||||||
|
<WorkingDirectory>.</WorkingDirectory>
|
||||||
|
<LaunchProvider>Web launcher</LaunchProvider>
|
||||||
|
<WebBrowserUrl>http://localhost</WebBrowserUrl>
|
||||||
|
<OutputPath>.</OutputPath>
|
||||||
|
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
|
||||||
|
<Name>GameLogServer</Name>
|
||||||
|
<RootNamespace>GameLogServer</RootNamespace>
|
||||||
|
<InterpreterId>MSBuild|env|$(MSBuildProjectFullPath)</InterpreterId>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Prerelease' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
|
||||||
|
<OutputPath>bin\Prerelease\</OutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="GameLogServer\log_reader.py">
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="GameLogServer\restart_resource.py" />
|
||||||
|
<Compile Include="GameLogServer\server.py">
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="runserver.py" />
|
||||||
|
<Compile Include="GameLogServer\__init__.py" />
|
||||||
|
<Compile Include="GameLogServer\log_resource.py" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="GameLogServer\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="requirements.txt" />
|
||||||
|
<None Include="Stable.pubxml" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Interpreter Include="env\">
|
||||||
|
<Id>env</Id>
|
||||||
|
<Version>3.6</Version>
|
||||||
|
<Description>env (Python 3.6 (64-bit))</Description>
|
||||||
|
<InterpreterPath>Scripts\python.exe</InterpreterPath>
|
||||||
|
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
|
||||||
|
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
|
||||||
|
<Architecture>X64</Architecture>
|
||||||
|
</Interpreter>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
|
||||||
|
<!-- Specify pre- and post-build commands in the BeforeBuild and
|
||||||
|
AfterBuild targets below. -->
|
||||||
|
<Target Name="BeforeBuild">
|
||||||
|
</Target>
|
||||||
|
<Target Name="AfterBuild">
|
||||||
|
</Target>
|
||||||
|
<ProjectExtensions>
|
||||||
|
<VisualStudio>
|
||||||
|
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
|
||||||
|
<WebProjectProperties>
|
||||||
|
<AutoAssignPort>True</AutoAssignPort>
|
||||||
|
<UseCustomServer>True</UseCustomServer>
|
||||||
|
<CustomServerUrl>http://localhost</CustomServerUrl>
|
||||||
|
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||||
|
</WebProjectProperties>
|
||||||
|
</FlavorProperties>
|
||||||
|
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
|
||||||
|
<WebProjectProperties>
|
||||||
|
<StartPageUrl>
|
||||||
|
</StartPageUrl>
|
||||||
|
<StartAction>CurrentPage</StartAction>
|
||||||
|
<AspNetDebugging>True</AspNetDebugging>
|
||||||
|
<SilverlightDebugging>False</SilverlightDebugging>
|
||||||
|
<NativeDebugging>False</NativeDebugging>
|
||||||
|
<SQLDebugging>False</SQLDebugging>
|
||||||
|
<ExternalProgram>
|
||||||
|
</ExternalProgram>
|
||||||
|
<StartExternalURL>
|
||||||
|
</StartExternalURL>
|
||||||
|
<StartCmdLineArguments>
|
||||||
|
</StartCmdLineArguments>
|
||||||
|
<StartWorkingDirectory>
|
||||||
|
</StartWorkingDirectory>
|
||||||
|
<EnableENC>False</EnableENC>
|
||||||
|
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
|
||||||
|
</WebProjectProperties>
|
||||||
|
</FlavorProperties>
|
||||||
|
</VisualStudio>
|
||||||
|
</ProjectExtensions>
|
||||||
|
</Project>
|
9
GameLogServer/GameLogServer/__init__.py
Normal file
9
GameLogServer/GameLogServer/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
The flask application package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_restful import Api
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
api = Api(app)
|
76
GameLogServer/GameLogServer/log_reader.py
Normal file
76
GameLogServer/GameLogServer/log_reader.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import re
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
class LogReader(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.log_file_sizes = {}
|
||||||
|
# (if the file changes more than this, ignore ) - 0.125 MB
|
||||||
|
self.max_file_size_change = 125000
|
||||||
|
# (if the time between checks is greater, ignore ) - 5 minutes
|
||||||
|
self.max_file_time_change = 60
|
||||||
|
|
||||||
|
def read_file(self, path):
|
||||||
|
# prevent traversing directories
|
||||||
|
if re.search('r^.+\.\.\\.+$', path):
|
||||||
|
return False
|
||||||
|
# must be a valid log path and log file
|
||||||
|
if not re.search(r'^.+[\\|\/](userraw|mods|main)[\\|\/].+.log$', path):
|
||||||
|
return False
|
||||||
|
# set the initialze size to the current file size
|
||||||
|
file_size = 0
|
||||||
|
|
||||||
|
if path not in self.log_file_sizes:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# the log size was unable to be read (probably the wrong path)
|
||||||
|
if new_file_size < 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
file_size_difference = new_file_size - last_length
|
||||||
|
time_difference = now - last_read
|
||||||
|
|
||||||
|
# update the new size and actually read the data
|
||||||
|
self.log_file_sizes[path] = {
|
||||||
|
'length': new_file_size,
|
||||||
|
'read': now
|
||||||
|
}
|
||||||
|
|
||||||
|
# if it's been too long since we read and the amount changed is too great, discard it
|
||||||
|
# todo: do we really want old events? maybe make this an "or"
|
||||||
|
if file_size_difference > self.max_file_size_change or time_difference > self.max_file_time_change:
|
||||||
|
return True
|
||||||
|
|
||||||
|
new_log_info = self.get_file_lines(path, file_size_difference)
|
||||||
|
return new_log_info
|
||||||
|
|
||||||
|
def get_file_lines(self, path, length):
|
||||||
|
try:
|
||||||
|
file_handle = open(path, 'rb')
|
||||||
|
file_handle.seek(-length, 2)
|
||||||
|
file_data = file_handle.read(length)
|
||||||
|
file_handle.close()
|
||||||
|
return file_data.decode('utf-8')
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def file_length(self, path):
|
||||||
|
try:
|
||||||
|
return os.stat(path).st_size
|
||||||
|
except:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
reader = LogReader()
|
19
GameLogServer/GameLogServer/log_resource.py
Normal file
19
GameLogServer/GameLogServer/log_resource.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from flask_restful import Resource
|
||||||
|
from GameLogServer.log_reader import reader
|
||||||
|
from base64 import urlsafe_b64decode
|
||||||
|
|
||||||
|
class LogResource(Resource):
|
||||||
|
def get(self, path):
|
||||||
|
path = urlsafe_b64decode(path).decode('utf-8')
|
||||||
|
log_info = reader.read_file(path)
|
||||||
|
|
||||||
|
if log_info is False:
|
||||||
|
print('could not read log file ' + path)
|
||||||
|
|
||||||
|
empty_read = (log_info == False) or (log_info == True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success' : log_info is not False,
|
||||||
|
'length': -1 if empty_read else len(log_info),
|
||||||
|
'data': log_info
|
||||||
|
}
|
29
GameLogServer/GameLogServer/restart_resource.py
Normal file
29
GameLogServer/GameLogServer/restart_resource.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from flask_restful import Resource
|
||||||
|
from flask import request
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
|
||||||
|
def get_pid_of_server_windows(port):
|
||||||
|
process = subprocess.Popen('netstat -aon', shell=True, stdout=subprocess.PIPE)
|
||||||
|
output = process.communicate()[0]
|
||||||
|
matches = re.search(' *(UDP) +([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}):'+ str(port) + ' +[^\w]*([0-9]+)', output.decode('utf-8'))
|
||||||
|
if matches is not None:
|
||||||
|
return matches.group(3)
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class RestartResource(Resource):
|
||||||
|
def get(self):
|
||||||
|
try:
|
||||||
|
response = requests.get('http://' + request.remote_addr + ':1624/api/restartapproved')
|
||||||
|
if response.status_code == 200:
|
||||||
|
pid = get_pid_of_server_windows(response.json()['port'])
|
||||||
|
subprocess.check_output("Taskkill /PID %s /F" % pid)
|
||||||
|
else:
|
||||||
|
return {}, 400
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return {}, 500
|
||||||
|
return {}, 200
|
11
GameLogServer/GameLogServer/server.py
Normal file
11
GameLogServer/GameLogServer/server.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from flask import Flask
|
||||||
|
from flask_restful import Api
|
||||||
|
from .log_resource import LogResource
|
||||||
|
from .restart_resource import RestartResource
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
def init():
|
||||||
|
api = Api(app)
|
||||||
|
api.add_resource(LogResource, '/log/<string:path>')
|
||||||
|
api.add_resource(RestartResource, '/restart')
|
26
GameLogServer/requirements.txt
Normal file
26
GameLogServer/requirements.txt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
aniso8601==3.0.2
|
||||||
|
APScheduler==3.5.3
|
||||||
|
certifi==2018.10.15
|
||||||
|
chardet==3.0.4
|
||||||
|
click==6.7
|
||||||
|
Flask==1.0.2
|
||||||
|
Flask-JWT==0.3.2
|
||||||
|
Flask-JWT-Extended==3.8.1
|
||||||
|
Flask-RESTful==0.3.6
|
||||||
|
idna==2.7
|
||||||
|
itsdangerous==0.24
|
||||||
|
Jinja2==2.10
|
||||||
|
MarkupSafe==1.0
|
||||||
|
marshmallow==3.0.0b8
|
||||||
|
pip==9.0.3
|
||||||
|
psutil==5.4.8
|
||||||
|
pygal==2.4.0
|
||||||
|
PyJWT==1.4.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
|
15
GameLogServer/runserver.py
Normal file
15
GameLogServer/runserver.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"""
|
||||||
|
This script runs the GameLogServer application using a development server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from os import environ
|
||||||
|
from GameLogServer.server import app, init
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
HOST = environ.get('SERVER_HOST', '0.0.0.0')
|
||||||
|
try:
|
||||||
|
PORT = int(environ.get('SERVER_PORT', '1625'))
|
||||||
|
except ValueError:
|
||||||
|
PORT = 5555
|
||||||
|
init()
|
||||||
|
app.run(HOST, PORT, debug=False)
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio 15
|
||||||
VisualStudioVersion = 15.0.26730.16
|
VisualStudioVersion = 15.0.26730.16
|
||||||
@ -7,8 +6,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
_commands.gsc = _commands.gsc
|
||||||
_customcallbacks.gsc = _customcallbacks.gsc
|
_customcallbacks.gsc = _customcallbacks.gsc
|
||||||
README.md = README.md
|
README.md = README.md
|
||||||
|
RunPublishPre.cmd = RunPublishPre.cmd
|
||||||
|
RunPublishRelease.cmd = RunPublishRelease.cmd
|
||||||
version.txt = version.txt
|
version.txt = version.txt
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
@ -28,7 +30,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Login", "Plugins\Login\Logi
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Master", "Master\Master.pyproj", "{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}"
|
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Master", "Master\Master.pyproj", "{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
|
||||||
|
EndProject
|
||||||
|
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "DiscordWebhook", "DiscordWebhook\DiscordWebhook.pyproj", "{15A81D6E-7502-46CE-8530-0647A380B5F4}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
|
||||||
|
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "GameLogServer", "GameLogServer\GameLogServer.pyproj", "{42EFDA12-10D3-4C40-A210-9483520116BC}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -260,6 +274,66 @@ Global
|
|||||||
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x64.Build.0 = Release|Any CPU
|
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.ActiveCfg = Release|Any CPU
|
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.Build.0 = Release|Any CPU
|
{B72DEBFB-9D48-4076-8FF5-1FD72A830845}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Debug|x64.ActiveCfg = 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.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.Build.0 = 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|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x64.Build.0 = Debug|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|x86.Build.0 = Debug|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
|
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.ActiveCfg = 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.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.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.Build.0 = Debug|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Any CPU.ActiveCfg = Release|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.Build.0 = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.Build.0 = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.Build.0 = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -270,6 +344,8 @@ Global
|
|||||||
{958FF7EC-0226-4E85-A85B-B84EC768197D} = {26E8B310-269E-46D4-A612-24601F16065F}
|
{958FF7EC-0226-4E85-A85B-B84EC768197D} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F}
|
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
{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}
|
||||||
|
{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {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}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<ProjectGuid>f5051a32-6bd0-4128-abba-c202ee15fc5c</ProjectGuid>
|
<ProjectGuid>f5051a32-6bd0-4128-abba-c202ee15fc5c</ProjectGuid>
|
||||||
<ProjectHome>.</ProjectHome>
|
<ProjectHome>.</ProjectHome>
|
||||||
<ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
|
<ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
|
||||||
<StartupFile>runserver.py</StartupFile>
|
<StartupFile>master\runserver.py</StartupFile>
|
||||||
<SearchPath>
|
<SearchPath>
|
||||||
</SearchPath>
|
</SearchPath>
|
||||||
<WorkingDirectory>.</WorkingDirectory>
|
<WorkingDirectory>.</WorkingDirectory>
|
||||||
@ -17,7 +17,14 @@
|
|||||||
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
|
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
|
||||||
<Name>Master</Name>
|
<Name>Master</Name>
|
||||||
<RootNamespace>Master</RootNamespace>
|
<RootNamespace>Master</RootNamespace>
|
||||||
<InterpreterId>MSBuild|dev_env|$(MSBuildProjectFullPath)</InterpreterId>
|
<InterpreterId>MSBuild|env_master|$(MSBuildProjectFullPath)</InterpreterId>
|
||||||
|
<IsWindowsApplication>False</IsWindowsApplication>
|
||||||
|
<PythonRunWebServerCommand>
|
||||||
|
</PythonRunWebServerCommand>
|
||||||
|
<PythonDebugWebServerCommand>
|
||||||
|
</PythonDebugWebServerCommand>
|
||||||
|
<PythonRunWebServerCommandType>script</PythonRunWebServerCommandType>
|
||||||
|
<PythonDebugWebServerCommandType>script</PythonDebugWebServerCommandType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
@ -51,27 +58,36 @@
|
|||||||
<Compile Include="master\models\__init__.py">
|
<Compile Include="master\models\__init__.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Master\resources\authenticate.py">
|
<Compile Include="master\resources\authenticate.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="master\resources\history_graph.py">
|
<Compile Include="master\resources\history_graph.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Master\resources\instance.py">
|
<Compile Include="master\resources\instance.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Master\resources\null.py">
|
<Compile Include="master\resources\localization.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Master\resources\version.py">
|
<Compile Include="master\resources\null.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Master\resources\__init__.py">
|
<Compile Include="Master\resources\server.py">
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="master\resources\version.py">
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="master\resources\__init__.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="master\routes.py">
|
<Compile Include="master\routes.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="master\runserver.py">
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
<Compile Include="master\schema\instanceschema.py">
|
<Compile Include="master\schema\instanceschema.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
@ -81,33 +97,32 @@
|
|||||||
<Compile Include="master\schema\__init__.py">
|
<Compile Include="master\schema\__init__.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="runserver.py" />
|
|
||||||
<Compile Include="master\__init__.py" />
|
<Compile Include="master\__init__.py" />
|
||||||
<Compile Include="master\views.py" />
|
<Compile Include="master\views.py" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="C:\Projects\IW4M-Admin\Master\master\" />
|
|
||||||
<Folder Include="master\" />
|
<Folder Include="master\" />
|
||||||
<Folder Include="master\context\" />
|
<Folder Include="master\context\" />
|
||||||
<Folder Include="master\models\" />
|
<Folder Include="master\models\" />
|
||||||
<Folder Include="master\config\" />
|
<Folder Include="master\config\" />
|
||||||
<Folder Include="master\schema\" />
|
<Folder Include="master\schema\" />
|
||||||
<Folder Include="Master\resources\" />
|
<Folder Include="Master\resources\" />
|
||||||
<Folder Include="Master\static\" />
|
<Folder Include="master\static\" />
|
||||||
<Folder Include="Master\templates\" />
|
<Folder Include="master\templates\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="FolderProfile.pubxml" />
|
|
||||||
<Content Include="master\config\master.json" />
|
<Content Include="master\config\master.json" />
|
||||||
|
<Content Include="master\templates\serverlist.html" />
|
||||||
|
<None Include="Release.pubxml" />
|
||||||
<Content Include="requirements.txt" />
|
<Content Include="requirements.txt" />
|
||||||
<Content Include="Master\templates\index.html" />
|
<Content Include="master\templates\index.html" />
|
||||||
<Content Include="Master\templates\layout.html" />
|
<Content Include="master\templates\layout.html" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Interpreter Include="dev_env\">
|
<Interpreter Include="env_master\">
|
||||||
<Id>dev_env</Id>
|
<Id>env_master</Id>
|
||||||
<Version>3.6</Version>
|
<Version>3.6</Version>
|
||||||
<Description>dev_env (Python 3.6 (64-bit))</Description>
|
<Description>env_master (Python 3.6 (64-bit))</Description>
|
||||||
<InterpreterPath>Scripts\python.exe</InterpreterPath>
|
<InterpreterPath>Scripts\python.exe</InterpreterPath>
|
||||||
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
|
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
|
||||||
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
|
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
|
||||||
|
@ -6,7 +6,6 @@ from flask import Flask
|
|||||||
from flask_restful import Resource, Api
|
from flask_restful import Resource, Api
|
||||||
from flask_jwt_extended import JWTManager
|
from flask_jwt_extended import JWTManager
|
||||||
from master.context.base import Base
|
from master.context.base import Base
|
||||||
import json
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['JWT_SECRET_KEY'] = 'my key!'
|
app.config['JWT_SECRET_KEY'] = 'my key!'
|
||||||
@ -14,7 +13,7 @@ app.config['PROPAGATE_EXCEPTIONS'] = True
|
|||||||
jwt = JWTManager(app)
|
jwt = JWTManager(app)
|
||||||
api = Api(app)
|
api = Api(app)
|
||||||
ctx = Base()
|
ctx = Base()
|
||||||
config = json.load(open('./master/config/master.json'))
|
#config = json.load(open('./master/config/master.json'))
|
||||||
|
|
||||||
import master.routes
|
import master.routes
|
||||||
import master.views
|
import master.views
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"current-version-stable": 2.0,
|
"current-version-stable": 2.1,
|
||||||
"current-version-prerelease": 2.0
|
"current-version-prerelease": 2.1
|
||||||
}
|
}
|
@ -15,9 +15,9 @@ class Base():
|
|||||||
self.scheduler.start()
|
self.scheduler.start()
|
||||||
self.scheduler.add_job(
|
self.scheduler.add_job(
|
||||||
func=self._remove_staleinstances,
|
func=self._remove_staleinstances,
|
||||||
trigger=IntervalTrigger(seconds=120),
|
trigger=IntervalTrigger(seconds=60),
|
||||||
id='stale_instance_remover',
|
id='stale_instance_remover',
|
||||||
name='Remove stale instances if no heartbeat in 120 seconds',
|
name='Remove stale instances if no heartbeat in 60 seconds',
|
||||||
replace_existing=True
|
replace_existing=True
|
||||||
)
|
)
|
||||||
self.scheduler.add_job(
|
self.scheduler.add_job(
|
||||||
@ -37,10 +37,11 @@ class Base():
|
|||||||
client_num += server.clientnum
|
client_num += server.clientnum
|
||||||
self.history.add_client_history(client_num)
|
self.history.add_client_history(client_num)
|
||||||
self.history.add_instance_history(len(self.instance_list))
|
self.history.add_instance_history(len(self.instance_list))
|
||||||
|
self.history.add_server_history(len(servers))
|
||||||
|
|
||||||
def _remove_staleinstances(self):
|
def _remove_staleinstances(self):
|
||||||
for key, value in list(self.instance_list.items()):
|
for key, value in list(self.instance_list.items()):
|
||||||
if int(time.time()) - value.last_heartbeat > 120:
|
if int(time.time()) - value.last_heartbeat > 60:
|
||||||
print('[_remove_staleinstances] removing stale instance {id}'.format(id=key))
|
print('[_remove_staleinstances] removing stale instance {id}'.format(id=key))
|
||||||
del self.instance_list[key]
|
del self.instance_list[key]
|
||||||
del self.token_list[key]
|
del self.token_list[key]
|
||||||
|
@ -5,17 +5,26 @@ class History():
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.client_history = list()
|
self.client_history = list()
|
||||||
self.instance_history = list()
|
self.instance_history = list()
|
||||||
|
self.server_history = list()
|
||||||
|
|
||||||
def add_client_history(self, client_num):
|
def add_client_history(self, client_num):
|
||||||
if len(self.client_history) > 1440:
|
if len(self.client_history) > 2880:
|
||||||
self.client_history = self.client_history[1:]
|
self.client_history = self.client_history[1:]
|
||||||
self.client_history.append({
|
self.client_history.append({
|
||||||
'count' : client_num,
|
'count' : client_num,
|
||||||
'time' : int(time.time())
|
'time' : int(time.time())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def add_server_history(self, server_num):
|
||||||
|
if len(self.server_history) > 2880:
|
||||||
|
self.server_history = self.server_history[1:]
|
||||||
|
self.server_history.append({
|
||||||
|
'count' : server_num,
|
||||||
|
'time' : int(time.time())
|
||||||
|
})
|
||||||
|
|
||||||
def add_instance_history(self, instance_num):
|
def add_instance_history(self, instance_num):
|
||||||
if len(self.instance_history) > 1440:
|
if len(self.instance_history) > 2880:
|
||||||
self.instance_history = self.instance_history[1:]
|
self.instance_history = self.instance_history[1:]
|
||||||
self.instance_history.append({
|
self.instance_history.append({
|
||||||
'count' : instance_num,
|
'count' : instance_num,
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
|
|
||||||
class ServerModel(object):
|
class ServerModel(object):
|
||||||
def __init__(self, id, port, game, hostname, clientnum, maxclientnum, map, gametype):
|
def __init__(self, id, port, game, hostname, clientnum, maxclientnum, map, gametype, ip, version):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self.version = version
|
||||||
self.game = game
|
self.game = game
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.clientnum = clientnum
|
self.clientnum = clientnum
|
||||||
self.maxclientnum = maxclientnum
|
self.maxclientnum = maxclientnum
|
||||||
self.map = map
|
self.map = map
|
||||||
self.gametype = gametype
|
self.gametype = gametype
|
||||||
|
self.ip = ip
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<ServerModel(id={id})>'.format(id=self.id)
|
return '<ServerModel(id={id})>'.format(id=self.id)
|
||||||
|
@ -8,10 +8,11 @@ import datetime
|
|||||||
class Authenticate(Resource):
|
class Authenticate(Resource):
|
||||||
def post(self):
|
def post(self):
|
||||||
instance_id = request.json['id']
|
instance_id = request.json['id']
|
||||||
if ctx.get_token(instance_id) is not False:
|
#todo: see why this is failing
|
||||||
return { 'message' : 'that id already has a token'}, 401
|
#if ctx.get_token(instance_id) is not False:
|
||||||
else:
|
# return { 'message' : 'that id already has a token'}, 401
|
||||||
expires = datetime.timedelta(days=1)
|
#else:
|
||||||
token = create_access_token(instance_id, expires_delta=expires)
|
expires = datetime.timedelta(days=30)
|
||||||
ctx.add_token(instance_id, token)
|
token = create_access_token(instance_id, expires_delta=expires)
|
||||||
return { 'access_token' : token }, 200
|
ctx.add_token(instance_id, token)
|
||||||
|
return { 'access_token' : token }, 200
|
||||||
|
@ -11,22 +11,18 @@ class HistoryGraph(Resource):
|
|||||||
custom_style = Style(
|
custom_style = Style(
|
||||||
background='transparent',
|
background='transparent',
|
||||||
plot_background='transparent',
|
plot_background='transparent',
|
||||||
foreground='rgba(109, 118, 126, 0.3)',
|
foreground='#6c757d',
|
||||||
foreground_strong='rgba(109, 118, 126, 0.3)',
|
foreground_strong='#6c757d',
|
||||||
foreground_subtle='rgba(109, 118, 126, 0.3)',
|
foreground_subtle='#6c757d',
|
||||||
opacity='0.1',
|
opacity='0.1',
|
||||||
opacity_hover='0.2',
|
opacity_hover='0.2',
|
||||||
transition='100ms ease-in',
|
transition='0ms',
|
||||||
colors=('#007acc', '#749363')
|
colors=('#749363','#007acc'),
|
||||||
)
|
)
|
||||||
|
|
||||||
graph = pygal.StackedLine(
|
graph = pygal.Line(
|
||||||
interpolate='cubic',
|
|
||||||
interpolation_precision=3,
|
|
||||||
#x_labels_major_every=100,
|
|
||||||
#x_labels_major_count=500,
|
|
||||||
stroke_style={'width': 0.4},
|
stroke_style={'width': 0.4},
|
||||||
show_dots=False,
|
#show_dots=False,
|
||||||
show_legend=False,
|
show_legend=False,
|
||||||
fill=True,
|
fill=True,
|
||||||
style=custom_style,
|
style=custom_style,
|
||||||
@ -37,10 +33,17 @@ class HistoryGraph(Resource):
|
|||||||
if len(instance_count) > 0:
|
if len(instance_count) > 0:
|
||||||
graph.x_labels = [ timeago.format(instance_count[0])]
|
graph.x_labels = [ timeago.format(instance_count[0])]
|
||||||
|
|
||||||
graph.add('Instance Count', [history['count'] for history in ctx.history.instance_history][-history_count:])
|
instance_counts = [history['count'] for history in ctx.history.instance_history][-history_count:]
|
||||||
graph.add('Client Count', [history['count'] for history in ctx.history.client_history][-history_count:])
|
client_counts = [history['count'] for history in ctx.history.client_history][-history_count:]
|
||||||
return { 'message' : graph.render(),
|
server_counts = [history['count'] for history in ctx.history.server_history][-history_count:]
|
||||||
'data_points' : len(instance_count)
|
|
||||||
|
graph.add('Client Count', client_counts)
|
||||||
|
graph.add('Instance Count', instance_counts)
|
||||||
|
return { 'message' : graph.render().replace("<title>Pygal</title>", ""),
|
||||||
|
'data_points' : len(instance_count),
|
||||||
|
'instance_count' : 0 if len(instance_counts) is 0 else instance_counts[-1],
|
||||||
|
'client_count' : 0 if len(client_counts) is 0 else client_counts[-1],
|
||||||
|
'server_count' : 0 if len(server_counts) is 0 else server_counts[-1]
|
||||||
}, 200
|
}, 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return { 'message' : str(e) }, 500
|
return { 'message' : str(e) }, 500
|
||||||
|
@ -4,6 +4,7 @@ from flask_jwt_extended import jwt_required
|
|||||||
from marshmallow import ValidationError
|
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
|
||||||
|
|
||||||
class Instance(Resource):
|
class Instance(Resource):
|
||||||
def get(self, id=None):
|
def get(self, id=None):
|
||||||
@ -21,6 +22,11 @@ class Instance(Resource):
|
|||||||
@jwt_required
|
@jwt_required
|
||||||
def put(self, id):
|
def put(self, id):
|
||||||
try:
|
try:
|
||||||
|
for server in request.json['servers']:
|
||||||
|
if 'ip' not in server or server['ip'] == 'localhost':
|
||||||
|
server['ip'] = request.remote_addr
|
||||||
|
if 'version' not in server:
|
||||||
|
server['version'] = 'Unknown'
|
||||||
instance = InstanceSchema().load(request.json)
|
instance = InstanceSchema().load(request.json)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
return {'message' : err.messages }, 400
|
return {'message' : err.messages }, 400
|
||||||
@ -30,6 +36,11 @@ class Instance(Resource):
|
|||||||
@jwt_required
|
@jwt_required
|
||||||
def post(self):
|
def post(self):
|
||||||
try:
|
try:
|
||||||
|
for server in request.json['servers']:
|
||||||
|
if 'ip' not in server or server['ip'] == 'localhost':
|
||||||
|
server['ip'] = request.remote_addr
|
||||||
|
if 'version' not in server:
|
||||||
|
server['version'] = 'Unknown'
|
||||||
instance = InstanceSchema().load(request.json)
|
instance = InstanceSchema().load(request.json)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
return {'message' : err.messages }, 400
|
return {'message' : err.messages }, 400
|
||||||
|
59
Master/master/resources/localization.py
Normal file
59
Master/master/resources/localization.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from flask_restful import Resource
|
||||||
|
from flask import request, jsonify
|
||||||
|
from flask_jwt_extended import create_access_token
|
||||||
|
from master import app, ctx
|
||||||
|
import datetime
|
||||||
|
import urllib.request
|
||||||
|
import csv
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
class Localization(Resource):
|
||||||
|
def list(self):
|
||||||
|
response = urllib.request.urlopen('https://docs.google.com/spreadsheets/d/e/2PACX-1vRQjCqPvd0Xqcn86WqpFqp_lx4KKpel9O4OV13NycmV8rmqycorgJQm-8qXMfw37QJHun3pqVZFUKG-/pub?gid=0&single=true&output=csv')
|
||||||
|
data = response.read().decode('utf-8')
|
||||||
|
|
||||||
|
localization = []
|
||||||
|
csv_data = csv.DictReader(StringIO(data))
|
||||||
|
|
||||||
|
for language in csv_data.fieldnames[1:]:
|
||||||
|
localization.append({
|
||||||
|
'LocalizationName' : language,
|
||||||
|
'LocalizationIndex' : {
|
||||||
|
'Set' : {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for row in csv_data:
|
||||||
|
localization_string = row['STRING']
|
||||||
|
count = 0
|
||||||
|
for language in csv_data.fieldnames[1:]:
|
||||||
|
localization[count]['LocalizationIndex']['Set'][localization_string] = row[language]
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
return localization, 200
|
||||||
|
|
||||||
|
def get(self, language_tag=None):
|
||||||
|
response = urllib.request.urlopen('https://docs.google.com/spreadsheets/d/e/2PACX-1vRQjCqPvd0Xqcn86WqpFqp_lx4KKpel9O4OV13NycmV8rmqycorgJQm-8qXMfw37QJHun3pqVZFUKG-/pub?gid=0&single=true&output=csv')
|
||||||
|
data = response.read().decode('utf-8')
|
||||||
|
|
||||||
|
csv_data = csv.DictReader(StringIO(data))
|
||||||
|
|
||||||
|
|
||||||
|
if language_tag != None:
|
||||||
|
valid_language_tag = next((l for l in csv_data.fieldnames[1:] if l == language_tag), None)
|
||||||
|
if valid_language_tag is None:
|
||||||
|
valid_language_tag = next((l for l in csv_data.fieldnames[1:] if l.startswith(language_tag[:2])), None)
|
||||||
|
if valid_language_tag is None:
|
||||||
|
valid_language_tag = 'en-US'
|
||||||
|
localization = {
|
||||||
|
'LocalizationName' : valid_language_tag,
|
||||||
|
'LocalizationIndex' : {
|
||||||
|
'Set' : {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for row in csv_data:
|
||||||
|
localization_string = row['STRING']
|
||||||
|
localization['LocalizationIndex']['Set'][localization_string] = row[valid_language_tag]
|
||||||
|
return localization, 200
|
||||||
|
else:
|
||||||
|
return self.list()[0][0], 200
|
6
Master/master/resources/server.py
Normal file
6
Master/master/resources/server.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from flask_restful import Resource
|
||||||
|
|
||||||
|
class Server(Resource):
|
||||||
|
"""description of class"""
|
||||||
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
|||||||
from flask_restful import Resource
|
from flask_restful import Resource
|
||||||
from master import config
|
import json
|
||||||
|
|
||||||
class Version(Resource):
|
class Version(Resource):
|
||||||
def get(self):
|
def get(self):
|
||||||
|
config = json.load(open('./master/config/master.json'))
|
||||||
return {
|
return {
|
||||||
'current-version-stable' : config['current-version-stable'],
|
'current-version-stable' : config['current-version-stable'],
|
||||||
'current-version-prerelease' : config['current-version-prerelease']
|
'current-version-prerelease' : config['current-version-prerelease']
|
||||||
|
@ -5,9 +5,13 @@ from master.resources.instance import Instance
|
|||||||
from master.resources.authenticate import Authenticate
|
from master.resources.authenticate import Authenticate
|
||||||
from master.resources.version import Version
|
from master.resources.version import Version
|
||||||
from master.resources.history_graph import HistoryGraph
|
from master.resources.history_graph import HistoryGraph
|
||||||
|
from master.resources.localization import Localization
|
||||||
|
from master.resources.server import Server
|
||||||
|
|
||||||
api.add_resource(Null, '/null')
|
api.add_resource(Null, '/null')
|
||||||
api.add_resource(Instance, '/instance/', '/instance/<string:id>')
|
api.add_resource(Instance, '/instance/', '/instance/<string:id>')
|
||||||
api.add_resource(Version, '/version')
|
api.add_resource(Version, '/version')
|
||||||
api.add_resource(Authenticate, '/authenticate')
|
api.add_resource(Authenticate, '/authenticate')
|
||||||
api.add_resource(HistoryGraph, '/history/', '/history/<int:history_count>')
|
api.add_resource(HistoryGraph, '/history/', '/history/<int:history_count>')
|
||||||
|
api.add_resource(Localization, '/localization/', '/localization/<string:language_tag>')
|
||||||
|
api.add_resource(Server, '/server')
|
@ -6,4 +6,4 @@ from os import environ
|
|||||||
from master import app
|
from master import app
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=80, debug=True)
|
app.run(host='0.0.0.0', port=80, debug=True)
|
@ -4,19 +4,26 @@ 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, 2147483647, 'invalid id')
|
validate=validate.Range(1, 25525525525565535, 'invalid id')
|
||||||
|
)
|
||||||
|
ip = fields.Str(
|
||||||
|
required=True
|
||||||
)
|
)
|
||||||
port = fields.Int(
|
port = fields.Int(
|
||||||
required=True,
|
required=True,
|
||||||
validate=validate.Range(1, 665535, 'invalid port')
|
validate=validate.Range(1, 65535, 'invalid port')
|
||||||
|
)
|
||||||
|
version = fields.String(
|
||||||
|
required=False,
|
||||||
|
validate=validate.Length(0, 128, 'invalid server version')
|
||||||
)
|
)
|
||||||
game = fields.String(
|
game = fields.String(
|
||||||
required=True,
|
required=True,
|
||||||
validate=validate.Length(1, 8, 'invalid game name')
|
validate=validate.Length(1, 5, 'invalid game name')
|
||||||
)
|
)
|
||||||
hostname = fields.String(
|
hostname = fields.String(
|
||||||
required=True,
|
required=True,
|
||||||
validate=validate.Length(1, 48, 'invalid hostname')
|
validate=validate.Length(1, 64, 'invalid hostname')
|
||||||
)
|
)
|
||||||
clientnum = fields.Int(
|
clientnum = fields.Int(
|
||||||
required=True,
|
required=True,
|
||||||
|
@ -5,43 +5,60 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<figure>
|
<figure>
|
||||||
<div id="history_graph">{{history_graph|safe}}</div>
|
<div id="history_graph">{{history_graph|safe}}</div>
|
||||||
<figcaption class="float-right pr-3 mr-4">
|
<figcaption class="float-right">
|
||||||
<span id="history_graph_zoom_out" class="h4 oi oi-zoom-out text-muted" style="cursor:pointer;"></span>
|
<span id="history_graph_zoom_out" class="h4 oi oi-zoom-out text-muted" style="cursor:pointer;"></span>
|
||||||
<span id="history_graph_zoom_in" class="h4 oi oi-zoom-in text-muted" style="cursor:pointer;"></span>
|
<span id="history_graph_zoom_in" class="h4 oi oi-zoom-in text-muted" style="cursor:pointer;"></span>
|
||||||
</figcaption>
|
</figcaption>
|
||||||
|
<figcaption class="float-left">
|
||||||
|
<span class="h4 text-muted">{{instance_count}} instances</span>
|
||||||
|
<span class="h4 text-muted">— {{client_count}} clients</span>
|
||||||
|
<span class="h4 text-muted">— {{server_count}} servers</span>
|
||||||
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script type="text/javascript" src="http://kozea.github.com/pygal.js/latest/pygal-tooltips.min.js"></script>
|
<script type="text/javascript" src="http://kozea.github.com/pygal.js/latest/pygal-tooltips.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let dataPoints = {{data_points}};
|
let dataPoints = {{data_points}};
|
||||||
let zoomLevel = Math.ceil(dataPoints / 2);
|
let maxPoints = 2880;
|
||||||
//console.log(dataPoints);
|
maxPoints = Math.min(maxPoints, dataPoints);
|
||||||
|
let zoomLevel = Math.floor(maxPoints);
|
||||||
|
let performingZoom = false;
|
||||||
|
|
||||||
function updateHistoryGraph() {
|
function updateHistoryGraph() {
|
||||||
$.get('/history/' + zoomLevel)
|
perfomingZoom = true;
|
||||||
.done(function (content) {
|
$.get('/history/' + zoomLevel)
|
||||||
$('#history_graph').html(content.message);
|
.done(function (content) {
|
||||||
dataPoints = content.data_points
|
$('#history_graph').html(content.message);
|
||||||
});
|
//maxPoints = Math.min(maxPoints, dataPoints);
|
||||||
|
perfomingZoom = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//setInterval(updateHistoryGraph, 30000);
|
||||||
|
|
||||||
|
$('#history_graph_zoom_out').click(function () {
|
||||||
|
if (performingZoom === true) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
setInterval(updateHistoryGraph, 30000);
|
zoomLevel = Math.floor(zoomLevel * 2) <= maxPoints ? Math.floor(zoomLevel * 2) : maxPoints;
|
||||||
|
updateHistoryGraph();
|
||||||
|
});
|
||||||
|
|
||||||
$('#history_graph_zoom_out').click(function () {
|
$('#history_graph_zoom_in').click(function () {
|
||||||
// console.log(zoomLevel);
|
if (performingZoom === true) {
|
||||||
zoomLevel = zoomLevel * 2 <= 1440 ? Math.ceil(zoomLevel * 2) : dataPoints;
|
return false;
|
||||||
updateHistoryGraph();
|
}
|
||||||
});
|
zoomLevel = zoomLevel / 2 > 2 ? Math.ceil(zoomLevel / 2) : 2;
|
||||||
|
updateHistoryGraph();
|
||||||
|
});
|
||||||
|
|
||||||
$('#history_graph_zoom_in').click(function () {
|
</script>
|
||||||
// console.log(zoomLevel);
|
|
||||||
zoomLevel = zoomLevel / 2 > 2 ? Math.ceil(zoomLevel / 2) : 2;
|
|
||||||
updateHistoryGraph();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -14,6 +14,18 @@
|
|||||||
.oi:hover {
|
.oi:hover {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
.dot {
|
||||||
|
opacity: 0;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-box {
|
||||||
|
fill: #343a40 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -27,6 +39,7 @@
|
|||||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
|
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
|
||||||
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
|
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
113
Master/master/templates/serverlist.html
Normal file
113
Master/master/templates/serverlist.html
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- todo: move this! -->
|
||||||
|
<style>
|
||||||
|
.server-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content, .nav-item {
|
||||||
|
background-color: #212529;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header, .modal-footer {
|
||||||
|
border-color: #32383e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dark button.close, a.nav-link {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="modal modal-dark" id="serverModalCenter" tabindex="-1" role="dialog" aria-labelledby="serverModalCenterTitle" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="serverModalTitle">Modal title</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="h5" id="server_socket"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="connect_button" type="button" class="btn btn-dark">Connect</button>
|
||||||
|
<button type="button" class="btn btn-dark" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<nav>
|
||||||
|
<div class="nav nav-tabs" id="server_game_tabs" role="tablist">
|
||||||
|
{% for game in games %}
|
||||||
|
<a class="nav-item nav-link {{'active' if loop.first else ''}}" id="{{game}}_servers_tab" data-toggle="tab" href="#{{game}}_servers" role="tab" aria-controls="{{game}}_servers" aria-selected="{{'true' if loop.first else 'false' }}">{{game}}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="tab-content" id="server_game_tabs_content">
|
||||||
|
{% for game, servers in games.items() %}
|
||||||
|
|
||||||
|
<div class="tab-pane {{'show active' if loop.first else ''}}" id="{{game}}_servers" role="tabpanel" aria-labelledby="{{game}}_servers_tab">
|
||||||
|
|
||||||
|
<table class="table table-dark table-striped table-hover table-responsive-lg">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Server Name</th>
|
||||||
|
<th>Map Name</th>
|
||||||
|
<th>Players</th>
|
||||||
|
<th>Mode</th>
|
||||||
|
<th class="text-center">Connect</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
{% for server in servers %}
|
||||||
|
|
||||||
|
<tr class="server-row" data-toggle="modal" data-target="#serverModalCenter"
|
||||||
|
data-ip="{{server.ip}}" data-port="{{server.port}}">
|
||||||
|
<td data-hostname="{{server.hostname}}" class="server-hostname">{{server.hostname}}</td>
|
||||||
|
<td data-map="{{server.map}} " class="server-map">{{server.map}}</td>
|
||||||
|
<td data-clientnum="{{server.clientnum}}" data-maxclientnum="{{server.maxclientnum}}"
|
||||||
|
class="server-clientnum">
|
||||||
|
{{server.clientnum}}/{{server.maxclientnum}}
|
||||||
|
</td>
|
||||||
|
<td data-gametype="{{server.gametype}}" class="server-gametype">{{server.gametype}}</td>
|
||||||
|
<td class="text-center"><span class="oi oi-play-circle"></span></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="w-100 small text-right text-muted">
|
||||||
|
<span>Developed by RaidMax</span><br />
|
||||||
|
<span>PRERELEASE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(() => {
|
||||||
|
$('.server-row').off('click');
|
||||||
|
$('.server-row').on('click', function (e) {
|
||||||
|
$('#serverModalTitle').text($(this).find('.server-hostname').text());
|
||||||
|
$('#server_socket').text(`/connect ${$(this).data('ip')}:${$(this).data('port')}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#connect_button').off('click');
|
||||||
|
$('#connect_button').on('click', (e) => alert('soon...'));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -4,15 +4,32 @@ Routes and views for the flask application.
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
from master import app
|
from master import app, ctx
|
||||||
from master.resources.history_graph import HistoryGraph
|
from master.resources.history_graph import HistoryGraph
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def home():
|
def home():
|
||||||
_history_graph = HistoryGraph().get(500)
|
_history_graph = HistoryGraph().get(2880)
|
||||||
return render_template(
|
return render_template(
|
||||||
'index.html',
|
'index.html',
|
||||||
title='API Overview',
|
title='API Overview',
|
||||||
history_graph = _history_graph[0]['message'],
|
history_graph = _history_graph[0]['message'],
|
||||||
data_points = _history_graph[0]['data_points']
|
data_points = _history_graph[0]['data_points'],
|
||||||
|
instance_count = _history_graph[0]['instance_count'],
|
||||||
|
client_count = _history_graph[0]['client_count'],
|
||||||
|
server_count = _history_graph[0]['server_count']
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route('/servers')
|
||||||
|
def servers():
|
||||||
|
servers = defaultdict(list)
|
||||||
|
if len(ctx.instance_list.values()) > 0:
|
||||||
|
ungrouped_servers = [server for instance in ctx.instance_list.values() for server in instance.servers]
|
||||||
|
for server in ungrouped_servers:
|
||||||
|
servers[server.game].append(server)
|
||||||
|
return render_template(
|
||||||
|
'serverlist.html',
|
||||||
|
title = 'Server List',
|
||||||
|
games = servers
|
||||||
)
|
)
|
||||||
|
@ -1,16 +1,26 @@
|
|||||||
aniso8601==3.0.0
|
aniso8601==3.0.2
|
||||||
|
APScheduler==3.5.3
|
||||||
|
certifi==2018.10.15
|
||||||
|
chardet==3.0.4
|
||||||
click==6.7
|
click==6.7
|
||||||
Flask==0.12.2
|
Flask==1.0.2
|
||||||
Flask-JWT==0.3.2
|
Flask-JWT==0.3.2
|
||||||
Flask-JWT-Extended==3.8.1
|
Flask-JWT-Extended==3.8.1
|
||||||
Flask-RESTful==0.3.6
|
Flask-RESTful==0.3.6
|
||||||
|
idna==2.7
|
||||||
itsdangerous==0.24
|
itsdangerous==0.24
|
||||||
Jinja2==2.10
|
Jinja2==2.10
|
||||||
MarkupSafe==1.0
|
MarkupSafe==1.0
|
||||||
marshmallow==3.0.0b8
|
marshmallow==3.0.0b8
|
||||||
pip==9.0.1
|
pip==9.0.3
|
||||||
|
psutil==5.4.8
|
||||||
|
pygal==2.4.0
|
||||||
PyJWT==1.4.2
|
PyJWT==1.4.2
|
||||||
pytz==2018.4
|
pytz==2018.7
|
||||||
setuptools==39.0.1
|
requests==2.20.0
|
||||||
|
setuptools==40.5.0
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
|
timeago==1.0.8
|
||||||
|
tzlocal==1.5.1
|
||||||
|
urllib3==1.24
|
||||||
Werkzeug==0.14.1
|
Werkzeug==0.14.1
|
||||||
|
14
Plugins/IW4ScriptCommands/CommandInfo.cs
Normal file
14
Plugins/IW4ScriptCommands/CommandInfo.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace IW4ScriptCommands
|
||||||
|
{
|
||||||
|
class CommandInfo
|
||||||
|
{
|
||||||
|
public string Command { get; set; }
|
||||||
|
public int ClientNumber { get; set; }
|
||||||
|
public List<string> CommandArguments { get; set; } = new List<string>();
|
||||||
|
public override string ToString() => $"{Command};{ClientNumber},{string.Join(',', CommandArguments)}";
|
||||||
|
}
|
||||||
|
}
|
200
Plugins/IW4ScriptCommands/Commands/Balance.cs
Normal file
200
Plugins/IW4ScriptCommands/Commands/Balance.cs
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using SharedLibraryCore.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IW4ScriptCommands.Commands
|
||||||
|
{
|
||||||
|
class Balance
|
||||||
|
{
|
||||||
|
private class TeamAssignment
|
||||||
|
{
|
||||||
|
public IW4MAdmin.Plugins.Stats.IW4Info.Team CurrentTeam { get; set; }
|
||||||
|
public int Num { get; set; }
|
||||||
|
public IW4MAdmin.Plugins.Stats.Models.EFClientStatistics Stats { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetTeamAssignments(EFClient client, bool isDisconnect, Server server, string teamsString = "")
|
||||||
|
{
|
||||||
|
var scriptClientTeams = teamsString.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(c => c.Split(','))
|
||||||
|
.Select(c => new TeamAssignment()
|
||||||
|
{
|
||||||
|
CurrentTeam = (IW4MAdmin.Plugins.Stats.IW4Info.Team)Enum.Parse(typeof(IW4MAdmin.Plugins.Stats.IW4Info.Team), c[1]),
|
||||||
|
Num = server.GetClientsAsList().FirstOrDefault(p => p.ClientNumber == Int32.Parse(c[0]))?.ClientNumber ?? -1,
|
||||||
|
Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(server.Clients.FirstOrDefault(p => p.ClientNumber == Int32.Parse(c[0])).ClientId, server.EndPoint)
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// at least one team is full so we can't balance
|
||||||
|
if (scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) >= Math.Floor(server.MaxClients / 2.0)
|
||||||
|
|| scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) >= Math.Floor(server.MaxClients / 2.0))
|
||||||
|
{
|
||||||
|
// E.Origin?.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL"]);
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> teamAssignments = new List<string>();
|
||||||
|
|
||||||
|
var _c = server.GetClientsAsList();
|
||||||
|
if (isDisconnect && client != null)
|
||||||
|
{
|
||||||
|
_c = _c.Where(c => c.ClientNumber != client.ClientNumber).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeClients = _c.Select(c => new TeamAssignment()
|
||||||
|
{
|
||||||
|
Num = c.ClientNumber,
|
||||||
|
Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, server.EndPoint),
|
||||||
|
CurrentTeam = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, server.EndPoint).Team
|
||||||
|
})
|
||||||
|
.Where(c => scriptClientTeams.FirstOrDefault(sc => sc.Num == c.Num)?.CurrentTeam != IW4MAdmin.Plugins.Stats.IW4Info.Team.Spectator)
|
||||||
|
.Where(c => c.CurrentTeam != scriptClientTeams.FirstOrDefault(p => p.Num == c.Num)?.CurrentTeam)
|
||||||
|
.OrderByDescending(c => c.Stats.Performance)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var alliesTeam = scriptClientTeams
|
||||||
|
.Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies)
|
||||||
|
.Where(c => activeClients.Count(t => t.Num == c.Num) == 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var axisTeam = scriptClientTeams
|
||||||
|
.Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis)
|
||||||
|
.Where(c => activeClients.Count(t => t.Num == c.Num) == 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
while (activeClients.Count() > 0)
|
||||||
|
{
|
||||||
|
int teamSizeDifference = alliesTeam.Count - axisTeam.Count;
|
||||||
|
double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 -
|
||||||
|
axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0;
|
||||||
|
|
||||||
|
if (teamSizeDifference == 0)
|
||||||
|
{
|
||||||
|
if (performanceDisparity == 0)
|
||||||
|
{
|
||||||
|
alliesTeam.Add(activeClients.First());
|
||||||
|
activeClients.RemoveAt(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (performanceDisparity > 0)
|
||||||
|
{
|
||||||
|
axisTeam.Add(activeClients.First());
|
||||||
|
activeClients.RemoveAt(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
alliesTeam.Add(activeClients.First());
|
||||||
|
activeClients.RemoveAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (teamSizeDifference > 0)
|
||||||
|
{
|
||||||
|
if (performanceDisparity > 0)
|
||||||
|
{
|
||||||
|
axisTeam.Add(activeClients.First());
|
||||||
|
activeClients.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
axisTeam.Add(activeClients.Last());
|
||||||
|
activeClients.RemoveAt(activeClients.Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (performanceDisparity > 0)
|
||||||
|
{
|
||||||
|
alliesTeam.Add(activeClients.First());
|
||||||
|
activeClients.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
alliesTeam.Add(activeClients.Last());
|
||||||
|
activeClients.RemoveAt(activeClients.Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alliesTeam = alliesTeam.OrderByDescending(t => t.Stats.Performance)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
axisTeam = axisTeam.OrderByDescending(t => t.Stats.Performance)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
while (Math.Abs(alliesTeam.Count - axisTeam.Count) > 1)
|
||||||
|
{
|
||||||
|
int teamSizeDifference = alliesTeam.Count - axisTeam.Count;
|
||||||
|
double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 -
|
||||||
|
axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0;
|
||||||
|
|
||||||
|
if (teamSizeDifference > 0)
|
||||||
|
{
|
||||||
|
if (performanceDisparity > 0)
|
||||||
|
{
|
||||||
|
axisTeam.Add(alliesTeam.First());
|
||||||
|
alliesTeam.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
axisTeam.Add(alliesTeam.Last());
|
||||||
|
alliesTeam.RemoveAt(axisTeam.Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (performanceDisparity > 0)
|
||||||
|
{
|
||||||
|
alliesTeam.Add(axisTeam.Last());
|
||||||
|
axisTeam.RemoveAt(axisTeam.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
alliesTeam.Add(axisTeam.First());
|
||||||
|
axisTeam.RemoveAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var assignment in alliesTeam)
|
||||||
|
{
|
||||||
|
teamAssignments.Add($"{assignment.Num},2");
|
||||||
|
assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var assignment in axisTeam)
|
||||||
|
{
|
||||||
|
teamAssignments.Add($"{assignment.Num},3");
|
||||||
|
assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis;
|
||||||
|
}
|
||||||
|
|
||||||
|
//if (alliesTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0 &&
|
||||||
|
// axisTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0)
|
||||||
|
//{
|
||||||
|
// //E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL_BALANCED"]);
|
||||||
|
// return string.Empty;
|
||||||
|
//}
|
||||||
|
|
||||||
|
//if (E.Origin?.Level > Player.Permission.Administrator)
|
||||||
|
//{
|
||||||
|
// E.Origin.Tell($"Allies Elo: {(alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0)}");
|
||||||
|
// E.Origin.Tell($"Axis Elo: {(axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0)}");
|
||||||
|
//}
|
||||||
|
|
||||||
|
//E.Origin.Tell("Balance command sent");
|
||||||
|
string args = string.Join(",", teamAssignments);
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
Plugins/IW4ScriptCommands/GscApiController.cs
Normal file
54
Plugins/IW4ScriptCommands/GscApiController.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using IW4ScriptCommands.Commands;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace WebfrontCore.Controllers.API
|
||||||
|
{
|
||||||
|
[Route("api/gsc/[action]")]
|
||||||
|
public class GscApiController : ApiController
|
||||||
|
{
|
||||||
|
[HttpGet("{networkId}")]
|
||||||
|
public IActionResult ClientInfo(string networkId)
|
||||||
|
{
|
||||||
|
var clientInfo = Manager.GetActiveClients()
|
||||||
|
.FirstOrDefault(c => c.NetworkId == networkId.ConvertLong());
|
||||||
|
|
||||||
|
if (clientInfo != null)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"admin={clientInfo.IsPrivileged()}");
|
||||||
|
sb.AppendLine($"level={(int)clientInfo.Level}");
|
||||||
|
sb.AppendLine($"levelstring={clientInfo.Level.ToLocalizedLevelName()}");
|
||||||
|
sb.AppendLine($"connections={clientInfo.Connections}");
|
||||||
|
sb.AppendLine($"authenticated={clientInfo.GetAdditionalProperty<bool>("IsLoggedIn") == true}");
|
||||||
|
|
||||||
|
return Content(sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Content("");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{networkId}")]
|
||||||
|
public IActionResult GetTeamAssignments(string networkId, int serverId, string teams = "", bool isDisconnect = false)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var client = Manager.GetActiveClients()
|
||||||
|
.FirstOrDefault(c => c.NetworkId == networkId.ConvertLong());
|
||||||
|
|
||||||
|
var server = Manager.GetServers().First(c => c.EndPoint == serverId);
|
||||||
|
|
||||||
|
teams = teams ?? string.Empty;
|
||||||
|
|
||||||
|
string assignments = Balance.GetTeamAssignments(client, isDisconnect, server, teams);
|
||||||
|
|
||||||
|
return Content(assignments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj
Normal file
25
Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<RuntimeFrameworkVersion>2.1.5</RuntimeFrameworkVersion>
|
||||||
|
<ApplicationIcon />
|
||||||
|
<StartupObject />
|
||||||
|
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
|
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||||
|
<ProjectReference Include="..\Stats\Stats.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Update="Microsoft.NETCore.App" Version="2.1.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
49
Plugins/IW4ScriptCommands/Plugin.cs
Normal file
49
Plugins/IW4ScriptCommands/Plugin.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IW4ScriptCommands
|
||||||
|
{
|
||||||
|
class Plugin : IPlugin
|
||||||
|
{
|
||||||
|
public string Name => "IW4 Script Commands";
|
||||||
|
|
||||||
|
public float Version => 1.0f;
|
||||||
|
|
||||||
|
public string Author => "RaidMax";
|
||||||
|
|
||||||
|
public Task OnEventAsync(GameEvent E, Server S)
|
||||||
|
{
|
||||||
|
if (E.Type == GameEvent.EventType.Start)
|
||||||
|
{
|
||||||
|
return S.SetDvarAsync("sv_iw4madmin_serverid", S.EndPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (E.Type == GameEvent.EventType.Warn)
|
||||||
|
{
|
||||||
|
return S.SetDvarAsync("sv_iw4madmin_command", new CommandInfo()
|
||||||
|
{
|
||||||
|
ClientNumber = E.Target.ClientNumber,
|
||||||
|
Command = "alert",
|
||||||
|
CommandArguments = new List<string>()
|
||||||
|
{
|
||||||
|
"Warning",
|
||||||
|
"ui_mp_nukebomb_timer",
|
||||||
|
E.Data
|
||||||
|
}
|
||||||
|
}.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task OnTickAsync(Server S) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task OnUnloadAsync() => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -8,14 +9,15 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
|||||||
{
|
{
|
||||||
public class CLogin : Command
|
public class CLogin : Command
|
||||||
{
|
{
|
||||||
public CLogin() : base("login", "login using password", "l", Player.Permission.Trusted, false, new CommandArgument[]
|
public CLogin() : base("login", Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_DESC"], "li", EFClient.Permission.Trusted, false, new CommandArgument[]
|
||||||
{
|
{
|
||||||
new CommandArgument()
|
new CommandArgument()
|
||||||
{
|
{
|
||||||
Name = "password",
|
Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
|
||||||
Required = true
|
Required = true
|
||||||
}
|
}
|
||||||
}){ }
|
})
|
||||||
|
{ }
|
||||||
|
|
||||||
public override async Task ExecuteAsync(GameEvent E)
|
public override async Task ExecuteAsync(GameEvent E)
|
||||||
{
|
{
|
||||||
@ -25,12 +27,12 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
|||||||
if (hashedPassword[0] == client.Password)
|
if (hashedPassword[0] == client.Password)
|
||||||
{
|
{
|
||||||
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
|
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
|
||||||
await E.Origin.Tell("You are now logged in");
|
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await E.Origin.Tell("Your password is incorrect");
|
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<RuntimeFrameworkVersion>2.1.5</RuntimeFrameworkVersion>
|
||||||
<ApplicationIcon />
|
<ApplicationIcon />
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
<PackageId>RaidMax.IW4MAdmin.Plugins.Login</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Plugins.Login</PackageId>
|
||||||
@ -20,6 +21,10 @@
|
|||||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||||
</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>
|
||||||
|
@ -3,6 +3,7 @@ using System.Reflection;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Exceptions;
|
using SharedLibraryCore.Exceptions;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
@ -21,12 +22,13 @@ namespace IW4MAdmin.Plugins.Login
|
|||||||
|
|
||||||
public Task OnEventAsync(GameEvent E, Server S)
|
public Task OnEventAsync(GameEvent E, Server S)
|
||||||
{
|
{
|
||||||
if (E.Remote || Config.RequirePrivilegedClientLogin == false)
|
if (E.IsRemote || Config.RequirePrivilegedClientLogin == false)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Connect)
|
if (E.Type == GameEvent.EventType.Connect)
|
||||||
{
|
{
|
||||||
AuthorizedClients.TryAdd(E.Origin.ClientId, false);
|
AuthorizedClients.TryAdd(E.Origin.ClientId, false);
|
||||||
|
E.Origin.SetAdditionalProperty("IsLoggedIn", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Disconnect)
|
if (E.Type == GameEvent.EventType.Disconnect)
|
||||||
@ -36,18 +38,28 @@ namespace IW4MAdmin.Plugins.Login
|
|||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Command)
|
if (E.Type == GameEvent.EventType.Command)
|
||||||
{
|
{
|
||||||
if (E.Origin.Level < SharedLibraryCore.Objects.Player.Permission.Moderator)
|
if (E.Origin.Level < EFClient.Permission.Moderator ||
|
||||||
|
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 &&
|
||||||
E.Owner.Manager.GetPrivilegedClients()[E.Origin.ClientId].Password == null)
|
client?.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)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
if (!AuthorizedClients[E.Origin.ClientId])
|
if (!AuthorizedClients[E.Origin.ClientId])
|
||||||
throw new AuthorizationException("not logged in");
|
{
|
||||||
|
throw new AuthorizationException(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_AUTH"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
E.Origin.SetAdditionalProperty("IsLoggedIn", true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@ -67,12 +79,8 @@ namespace IW4MAdmin.Plugins.Login
|
|||||||
Config = cfg.Configuration();
|
Config = cfg.Configuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task OnTickAsync(Server S) => Utilities.CompletedTask;
|
public Task OnTickAsync(Server S) => Task.CompletedTask;
|
||||||
|
|
||||||
public Task OnUnloadAsync()
|
public Task OnUnloadAsync() => Task.CompletedTask;
|
||||||
{
|
|
||||||
AuthorizedClients.Clear();
|
|
||||||
return Utilities.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,14 +16,16 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
{
|
{
|
||||||
OffensiveWords = new List<string>()
|
OffensiveWords = new List<string>()
|
||||||
{
|
{
|
||||||
"nigger",
|
@"\s*n+.*i+.*g+.*e+.*r+\s*",
|
||||||
"nigga",
|
@"\s*n+.*i+.*g+.*a+\s*",
|
||||||
"fuck"
|
@"\s*f+u+.*c+.*k+.*\s*"
|
||||||
};
|
};
|
||||||
|
|
||||||
EnableProfanityDeterment = Utilities.PromptBool("Enable profanity deterring");
|
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
ProfanityWarningMessage = "Please do not use profanity on this server";
|
|
||||||
ProfanityKickMessage = "Excessive use of profanity";
|
EnableProfanityDeterment = Utilities.PromptBool(loc["PLUGINS_PROFANITY_SETUP_ENABLE"]);
|
||||||
|
ProfanityWarningMessage = loc["PLUGINS_PROFANITY_WARNMSG"];
|
||||||
|
ProfanityKickMessage = loc["PLUGINS_PROFANITY_KICKMSG"];
|
||||||
KickAfterInfringementCount = 2;
|
KickAfterInfringementCount = 2;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
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;
|
using SharedLibraryCore.Objects;
|
||||||
|
|
||||||
@ -20,12 +22,11 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
BaseConfigurationHandler<Configuration> Settings;
|
BaseConfigurationHandler<Configuration> Settings;
|
||||||
ConcurrentDictionary<int, Tracking> ProfanityCounts;
|
ConcurrentDictionary<int, Tracking> ProfanityCounts;
|
||||||
IManager Manager;
|
IManager Manager;
|
||||||
Task CompletedTask = Task.FromResult(false);
|
|
||||||
|
|
||||||
public async Task OnEventAsync(GameEvent E, Server S)
|
public Task OnEventAsync(GameEvent E, Server S)
|
||||||
{
|
{
|
||||||
if (!Settings.Configuration().EnableProfanityDeterment)
|
if (!Settings.Configuration().EnableProfanityDeterment)
|
||||||
return;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Connect)
|
if (E.Type == GameEvent.EventType.Connect)
|
||||||
{
|
{
|
||||||
@ -34,6 +35,26 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
S.Logger.WriteWarning("Could not add client to profanity tracking");
|
S.Logger.WriteWarning("Could not add client to profanity tracking");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var objectionalWords = Settings.Configuration().OffensiveWords;
|
||||||
|
bool containsObjectionalWord = objectionalWords.FirstOrDefault(w => E.Origin.Name.ToLower().Contains(w)) != null;
|
||||||
|
|
||||||
|
// we want to run regex against it just incase
|
||||||
|
if (!containsObjectionalWord)
|
||||||
|
{
|
||||||
|
foreach (string word in objectionalWords)
|
||||||
|
{
|
||||||
|
containsObjectionalWord |= Regex.IsMatch(E.Origin.Name.ToLower(), word, RegexOptions.IgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsObjectionalWord)
|
||||||
|
{
|
||||||
|
E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, new EFClient()
|
||||||
|
{
|
||||||
|
ClientId = 1,
|
||||||
|
CurrentServer = E.Owner
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Disconnect)
|
if (E.Type == GameEvent.EventType.Disconnect)
|
||||||
@ -47,30 +68,36 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
if (E.Type == GameEvent.EventType.Say)
|
if (E.Type == GameEvent.EventType.Say)
|
||||||
{
|
{
|
||||||
var objectionalWords = Settings.Configuration().OffensiveWords;
|
var objectionalWords = Settings.Configuration().OffensiveWords;
|
||||||
bool containsObjectionalWord = objectionalWords.FirstOrDefault(w => E.Data.ToLower().Contains(w)) != null;
|
bool containsObjectionalWord = false;
|
||||||
|
|
||||||
|
foreach (string word in objectionalWords)
|
||||||
|
{
|
||||||
|
containsObjectionalWord |= Regex.IsMatch(E.Data.ToLower(), word, RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
// break out early because there's at least one objectional word
|
||||||
|
if (containsObjectionalWord)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (containsObjectionalWord)
|
if (containsObjectionalWord)
|
||||||
{
|
{
|
||||||
var clientProfanity = ProfanityCounts[E.Origin.ClientId];
|
var clientProfanity = ProfanityCounts[E.Origin.ClientId];
|
||||||
if (clientProfanity.Infringements >= Settings.Configuration().KickAfterInfringementCount)
|
if (clientProfanity.Infringements >= Settings.Configuration().KickAfterInfringementCount)
|
||||||
{
|
{
|
||||||
await clientProfanity.Client.Kick(Settings.Configuration().ProfanityKickMessage, new Player()
|
clientProfanity.Client.Kick(Settings.Configuration().ProfanityKickMessage, Utilities.IW4MAdminClient(E.Owner));
|
||||||
{
|
|
||||||
ClientId = 1
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (clientProfanity.Infringements < Settings.Configuration().KickAfterInfringementCount)
|
else if (clientProfanity.Infringements < Settings.Configuration().KickAfterInfringementCount)
|
||||||
{
|
{
|
||||||
clientProfanity.Infringements++;
|
clientProfanity.Infringements++;
|
||||||
|
|
||||||
await clientProfanity.Client.Warn(Settings.Configuration().ProfanityWarningMessage, new Player()
|
clientProfanity.Client.Warn(Settings.Configuration().ProfanityWarningMessage, Utilities.IW4MAdminClient(E.Owner));
|
||||||
{
|
|
||||||
ClientId = 1
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnLoadAsync(IManager manager)
|
public async Task OnLoadAsync(IManager manager)
|
||||||
@ -87,8 +114,8 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
Manager = manager;
|
Manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task OnTickAsync(Server S) => CompletedTask;
|
public Task OnTickAsync(Server S) => Task.CompletedTask;
|
||||||
|
|
||||||
public Task OnUnloadAsync() => CompletedTask;
|
public Task OnUnloadAsync() => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<RuntimeFrameworkVersion>2.1.5</RuntimeFrameworkVersion>
|
||||||
<ApplicationIcon />
|
<ApplicationIcon />
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
<PackageId>RaidMax.IW4MAdmin.Plugins.ProfanityDeterment</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Plugins.ProfanityDeterment</PackageId>
|
||||||
@ -18,6 +19,10 @@
|
|||||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||||
</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>
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Objects;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.ProfanityDeterment
|
namespace IW4MAdmin.Plugins.ProfanityDeterment
|
||||||
{
|
{
|
||||||
class Tracking
|
class Tracking
|
||||||
{
|
{
|
||||||
public Player Client { get; private set; }
|
public EFClient Client { get; private set; }
|
||||||
public int Infringements { get; set; }
|
public int Infringements { get; set; }
|
||||||
|
|
||||||
public Tracking(Player client)
|
public Tracking(EFClient client)
|
||||||
{
|
{
|
||||||
Client = client;
|
Client = client;
|
||||||
Infringements = 0;
|
Infringements = 0;
|
||||||
|
29
Plugins/ScriptPlugins/SharedGUIDKick.js
Normal file
29
Plugins/ScriptPlugins/SharedGUIDKick.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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) {
|
||||||
|
}
|
||||||
|
};
|
62
Plugins/ScriptPlugins/VPNDetection.js
Normal file
62
Plugins/ScriptPlugins/VPNDetection.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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) {
|
||||||
|
}
|
||||||
|
};
|
@ -1,8 +1,7 @@
|
|||||||
using SharedLibraryCore.Helpers;
|
using IW4MAdmin.Plugins.Stats.Models;
|
||||||
|
using SharedLibraryCore.Helpers;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Objects;
|
||||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
|
||||||
using IW4MAdmin.Plugins.Stats.Models;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -12,89 +11,187 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
{
|
{
|
||||||
class Detection
|
class Detection
|
||||||
{
|
{
|
||||||
|
public enum DetectionType
|
||||||
|
{
|
||||||
|
Bone,
|
||||||
|
Chest,
|
||||||
|
Offset,
|
||||||
|
Strain
|
||||||
|
};
|
||||||
|
|
||||||
|
public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
|
||||||
|
public const int QUEUE_COUNT = 10;
|
||||||
|
|
||||||
|
public List<EFClientKill> QueuedHits { get; set; }
|
||||||
int Kills;
|
int Kills;
|
||||||
int AboveThresholdCount;
|
int HitCount;
|
||||||
double AverageKillTime;
|
|
||||||
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
|
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
|
||||||
|
double AngleDifferenceAverage;
|
||||||
EFClientStatistics ClientStats;
|
EFClientStatistics ClientStats;
|
||||||
DateTime LastKill;
|
long LastOffset;
|
||||||
|
IW4Info.WeaponName LastWeapon;
|
||||||
ILogger Log;
|
ILogger Log;
|
||||||
|
Strain Strain;
|
||||||
|
readonly DateTime ConnectionTime = DateTime.UtcNow;
|
||||||
|
|
||||||
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, int>();
|
||||||
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, 0);
|
||||||
LastKill = DateTime.UtcNow;
|
}
|
||||||
|
|
||||||
ClientStats = clientStats;
|
ClientStats = clientStats;
|
||||||
|
Strain = new Strain();
|
||||||
|
Tracker = new ChangeTracking<EFACSnapshot>();
|
||||||
|
QueuedHits = new List<EFClientKill>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Analyze kill and see if performed by a cheater
|
/// Analyze kill and see if performed by a cheater
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="kill">kill performed by the player</param>
|
/// <param name="hit">kill performed by the player</param>
|
||||||
/// <returns>true if detection reached thresholds, false otherwise</returns>
|
/// <returns>true if detection reached thresholds, false otherwise</returns>
|
||||||
public DetectionPenaltyResult ProcessKill(EFClientKill kill)
|
public DetectionPenaltyResult ProcessHit(EFClientKill hit, bool isDamage)
|
||||||
{
|
{
|
||||||
if ((kill.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
|
if ((hit.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
|
||||||
kill.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
|
hit.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
|
||||||
kill.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
|
hit.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
|
||||||
kill.HitLoc == IW4Info.HitLocation.none)
|
hit.HitLoc == IW4Info.HitLocation.none || hit.TimeOffset - LastOffset < 0 ||
|
||||||
|
// hack: prevents false positives
|
||||||
|
(LastWeapon != hit.Weapon && (hit.TimeOffset - LastOffset) == 50))
|
||||||
|
{
|
||||||
return new DetectionPenaltyResult()
|
return new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Any,
|
ClientPenalty = Penalty.PenaltyType.Any,
|
||||||
RatioAmount = 0
|
|
||||||
};
|
};
|
||||||
|
|
||||||
HitLocationCount[kill.HitLoc]++;
|
|
||||||
Kills++;
|
|
||||||
AverageKillTime = (AverageKillTime + (DateTime.UtcNow - LastKill).TotalSeconds) / Kills;
|
|
||||||
|
|
||||||
#region VIEWANGLES
|
|
||||||
double distance = Vector3.Distance(kill.KillOrigin, kill.DeathOrigin);
|
|
||||||
double x = kill.KillOrigin.X + distance * Math.Cos(kill.ViewAngles.X.ToRadians()) * Math.Cos(kill.ViewAngles.Y.ToRadians());
|
|
||||||
double y = kill.KillOrigin.Y + (distance * Math.Sin(kill.ViewAngles.X.ToRadians()) * Math.Cos(kill.ViewAngles.Y.ToRadians()));
|
|
||||||
double z = kill.KillOrigin.Z + distance * Math.Sin((360.0f - kill.ViewAngles.Y).ToRadians());
|
|
||||||
var trueVector = Vector3.Subtract(kill.KillOrigin, kill.DeathOrigin);
|
|
||||||
var calculatedVector = Vector3.Subtract(kill.KillOrigin, new Vector3((float)x, (float)y, (float)z));
|
|
||||||
double angle = trueVector.AngleBetween(calculatedVector);
|
|
||||||
|
|
||||||
if (kill.AdsPercent > 0.5 && kill.Distance > 3)
|
|
||||||
{
|
|
||||||
var hitLoc = ClientStats.HitLocations
|
|
||||||
.First(hl => hl.Location == kill.HitLoc);
|
|
||||||
float previousAverage = hitLoc.HitOffsetAverage;
|
|
||||||
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + angle) / hitLoc.HitCount;
|
|
||||||
hitLoc.HitOffsetAverage = (float)newAverage;
|
|
||||||
|
|
||||||
if (double.IsNaN(hitLoc.HitOffsetAverage))
|
|
||||||
{
|
|
||||||
Log.WriteWarning("[Detection::ProcessKill] HitOffsetAvgerage NaN");
|
|
||||||
Log.WriteDebug($"{previousAverage}-{hitLoc.HitCount}-{hitLoc}-{newAverage}");
|
|
||||||
hitLoc.HitOffsetAverage = 0f;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DetectionPenaltyResult result = null;
|
||||||
|
LastWeapon = hit.Weapon;
|
||||||
|
|
||||||
|
HitLocationCount[hit.HitLoc]++;
|
||||||
|
HitCount++;
|
||||||
|
|
||||||
|
if (!isDamage)
|
||||||
|
{
|
||||||
|
Kills++;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region VIEWANGLES
|
||||||
|
if (hit.AnglesList.Count >= 2)
|
||||||
|
{
|
||||||
|
double realAgainstPredict = Vector3.ViewAngleDistance(hit.AnglesList[0], hit.AnglesList[1], hit.ViewAngles);
|
||||||
|
|
||||||
|
// LIFETIME
|
||||||
|
var hitLoc = ClientStats.HitLocations
|
||||||
|
.First(hl => hl.Location == hit.HitLoc);
|
||||||
|
|
||||||
|
float previousAverage = hitLoc.HitOffsetAverage;
|
||||||
|
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + realAgainstPredict) / hitLoc.HitCount;
|
||||||
|
hitLoc.HitOffsetAverage = (float)newAverage;
|
||||||
|
|
||||||
|
if (hitLoc.HitOffsetAverage > Thresholds.MaxOffset(hitLoc.HitCount) &&
|
||||||
|
hitLoc.HitCount > 100)
|
||||||
|
{
|
||||||
|
//Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***");
|
||||||
|
//Log.WriteDebug($"Lifetime Average = {newAverage}");
|
||||||
|
//Log.WriteDebug($"Bone = {hitLoc.Location}");
|
||||||
|
//Log.WriteDebug($"HitCount = {hitLoc.HitCount}");
|
||||||
|
//Log.WriteDebug($"ID = {hit.AttackerId}");
|
||||||
|
|
||||||
|
result = new DetectionPenaltyResult()
|
||||||
|
{
|
||||||
|
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||||
|
Value = hitLoc.HitOffsetAverage,
|
||||||
|
HitCount = hitLoc.HitCount,
|
||||||
|
Type = DetectionType.Offset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// SESSION
|
||||||
|
double sessAverage = (AngleDifferenceAverage * (HitCount - 1) + realAgainstPredict) / HitCount;
|
||||||
|
AngleDifferenceAverage = sessAverage;
|
||||||
|
|
||||||
|
if (sessAverage > Thresholds.MaxOffset(HitCount) &&
|
||||||
|
HitCount > 30)
|
||||||
|
{
|
||||||
|
//Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***");
|
||||||
|
//Log.WriteDebug($"Session Average = {sessAverage}");
|
||||||
|
//Log.WriteDebug($"HitCount = {HitCount}");
|
||||||
|
//Log.WriteDebug($"ID = {hit.AttackerId}");
|
||||||
|
|
||||||
|
result = new DetectionPenaltyResult()
|
||||||
|
{
|
||||||
|
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||||
|
Value = sessAverage,
|
||||||
|
HitCount = HitCount,
|
||||||
|
Type = DetectionType.Offset,
|
||||||
|
Location = hitLoc.Location
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Log.WriteDebug($"PredictVsReal={realAgainstPredict}");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
double currentStrain = Strain.GetStrain(isDamage, hit.Damage, hit.Distance / 0.0254, hit.ViewAngles, Math.Max(50, hit.TimeOffset - LastOffset));
|
||||||
|
#if DEBUG == true
|
||||||
|
Log.WriteDebug($"Current Strain: {currentStrain}");
|
||||||
|
#endif
|
||||||
|
LastOffset = hit.TimeOffset;
|
||||||
|
|
||||||
|
if (currentStrain > ClientStats.MaxStrain)
|
||||||
|
{
|
||||||
|
ClientStats.MaxStrain = currentStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// flag
|
||||||
|
if (currentStrain > Thresholds.MaxStrainFlag)
|
||||||
|
{
|
||||||
|
result = new DetectionPenaltyResult()
|
||||||
|
{
|
||||||
|
ClientPenalty = Penalty.PenaltyType.Flag,
|
||||||
|
Value = currentStrain,
|
||||||
|
HitCount = HitCount,
|
||||||
|
Type = DetectionType.Strain
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ban
|
||||||
|
if (currentStrain > Thresholds.MaxStrainBan &&
|
||||||
|
HitCount >= 5)
|
||||||
|
{
|
||||||
|
result = new DetectionPenaltyResult()
|
||||||
|
{
|
||||||
|
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||||
|
Value = currentStrain,
|
||||||
|
HitCount = HitCount,
|
||||||
|
Type = DetectionType.Strain
|
||||||
|
};
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region SESSION_RATIOS
|
#region SESSION_RATIOS
|
||||||
if (Kills >= Thresholds.LowSampleMinKills)
|
if (Kills >= Thresholds.LowSampleMinKills)
|
||||||
{
|
{
|
||||||
double marginOfError = Thresholds.GetMarginOfError(Kills);
|
double marginOfError = Thresholds.GetMarginOfError(HitCount);
|
||||||
// determine what the max headshot percentage can be for current number of kills
|
// determine what the max headshot percentage can be for current number of kills
|
||||||
double lerpAmount = Math.Min(1.0, (Kills - Thresholds.LowSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills));
|
double lerpAmount = Math.Min(1.0, (HitCount - Thresholds.LowSampleMinKills) / (double)(/*Thresholds.HighSampleMinKills*/ 60 - Thresholds.LowSampleMinKills));
|
||||||
double maxHeadshotLerpValueForFlag = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(2.0), Thresholds.HeadshotRatioThresholdHighSample(2.0), lerpAmount) + marginOfError;
|
double maxHeadshotLerpValueForFlag = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(2.0), Thresholds.HeadshotRatioThresholdHighSample(2.0), lerpAmount) + marginOfError;
|
||||||
double maxHeadshotLerpValueForBan = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(3.0), Thresholds.HeadshotRatioThresholdHighSample(3.0), lerpAmount) + marginOfError;
|
double maxHeadshotLerpValueForBan = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(3.5), Thresholds.HeadshotRatioThresholdHighSample(3.5), lerpAmount) + marginOfError;
|
||||||
// determine what the max bone percentage can be for current number of kills
|
// determine what the max bone percentage can be for current number of kills
|
||||||
double maxBoneRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(2.25), Thresholds.BoneRatioThresholdHighSample(2.25), lerpAmount) + marginOfError;
|
double maxBoneRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(2.25), Thresholds.BoneRatioThresholdHighSample(2.25), lerpAmount) + marginOfError;
|
||||||
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]) / (double)Kills);
|
double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet] + HitLocationCount[IW4Info.HitLocation.neck]) / (double)HitCount);
|
||||||
|
|
||||||
// calculate maximum bone
|
// calculate maximum bone
|
||||||
double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v / (double)Kills).Max());
|
double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v / (double)HitCount).Max());
|
||||||
var bone = HitLocationCount.FirstOrDefault(b => b.Value == HitLocationCount.Values.Max()).Key;
|
var bone = HitLocationCount.FirstOrDefault(b => b.Value == HitLocationCount.Values.Max()).Key;
|
||||||
|
|
||||||
#region HEADSHOT_RATIO
|
#region HEADSHOT_RATIO
|
||||||
@ -102,48 +199,52 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
|
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
|
||||||
{
|
{
|
||||||
// ban on headshot
|
// ban on headshot
|
||||||
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
|
if (currentHeadshotRatio > maxHeadshotLerpValueForBan)
|
||||||
{
|
{
|
||||||
AboveThresholdCount++;
|
|
||||||
Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**");
|
Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**");
|
||||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
Log.WriteDebug($"ClientId: {hit.AttackerId}");
|
||||||
Log.WriteDebug($"**Kills: {Kills}");
|
Log.WriteDebug($"**HitCount: {HitCount}");
|
||||||
Log.WriteDebug($"**Ratio {currentHeadshotRatio}");
|
Log.WriteDebug($"**Ratio {currentHeadshotRatio}");
|
||||||
Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}");
|
Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}");
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
foreach (var kvp in HitLocationCount)
|
foreach (var kvp in HitLocationCount)
|
||||||
|
{
|
||||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
||||||
Log.WriteDebug(sb.ToString());
|
}
|
||||||
Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
|
||||||
|
|
||||||
return new DetectionPenaltyResult()
|
Log.WriteDebug(sb.ToString());
|
||||||
|
|
||||||
|
result = new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||||
RatioAmount = currentHeadshotRatio,
|
Value = currentHeadshotRatio,
|
||||||
Bone = IW4Info.HitLocation.head,
|
Location = IW4Info.HitLocation.head,
|
||||||
KillCount = Kills
|
HitCount = HitCount,
|
||||||
|
Type = DetectionType.Bone
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AboveThresholdCount++;
|
|
||||||
Log.WriteDebug("**Maximum Headshot Ratio Reached For Flag**");
|
Log.WriteDebug("**Maximum Headshot Ratio Reached For Flag**");
|
||||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
Log.WriteDebug($"ClientId: {hit.AttackerId}");
|
||||||
Log.WriteDebug($"**Kills: {Kills}");
|
Log.WriteDebug($"**HitCount: {HitCount}");
|
||||||
Log.WriteDebug($"**Ratio {currentHeadshotRatio}");
|
Log.WriteDebug($"**Ratio {currentHeadshotRatio}");
|
||||||
Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}");
|
Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}");
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
foreach (var kvp in HitLocationCount)
|
foreach (var kvp in HitLocationCount)
|
||||||
|
{
|
||||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
||||||
Log.WriteDebug(sb.ToString());
|
}
|
||||||
Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
|
||||||
|
|
||||||
return new DetectionPenaltyResult()
|
Log.WriteDebug(sb.ToString());
|
||||||
|
|
||||||
|
result = new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
ClientPenalty = Penalty.PenaltyType.Flag,
|
||||||
RatioAmount = currentHeadshotRatio,
|
Value = currentHeadshotRatio,
|
||||||
Bone = IW4Info.HitLocation.head,
|
Location = IW4Info.HitLocation.head,
|
||||||
KillCount = Kills
|
HitCount = HitCount,
|
||||||
|
Type = DetectionType.Bone
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,41 +258,49 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
if (currentMaxBoneRatio > maxBoneRatioLerpValueForBan)
|
if (currentMaxBoneRatio > maxBoneRatioLerpValueForBan)
|
||||||
{
|
{
|
||||||
Log.WriteDebug("**Maximum Bone Ratio Reached For Ban**");
|
Log.WriteDebug("**Maximum Bone Ratio Reached For Ban**");
|
||||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
Log.WriteDebug($"ClientId: {hit.AttackerId}");
|
||||||
Log.WriteDebug($"**Kills: {Kills}");
|
Log.WriteDebug($"**HitCount: {HitCount}");
|
||||||
Log.WriteDebug($"**Ratio {currentMaxBoneRatio}");
|
Log.WriteDebug($"**Ratio {currentMaxBoneRatio}");
|
||||||
Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForBan}");
|
Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForBan}");
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
foreach (var kvp in HitLocationCount)
|
foreach (var kvp in HitLocationCount)
|
||||||
|
{
|
||||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
Log.WriteDebug(sb.ToString());
|
Log.WriteDebug(sb.ToString());
|
||||||
|
|
||||||
return new DetectionPenaltyResult()
|
result = new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||||
RatioAmount = currentMaxBoneRatio,
|
Value = currentMaxBoneRatio,
|
||||||
Bone = bone,
|
Location = bone,
|
||||||
KillCount = Kills
|
HitCount = HitCount,
|
||||||
|
Type = DetectionType.Bone
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.WriteDebug("**Maximum Bone Ratio Reached For Flag**");
|
Log.WriteDebug("**Maximum Bone Ratio Reached For Flag**");
|
||||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
Log.WriteDebug($"ClientId: {hit.AttackerId}");
|
||||||
Log.WriteDebug($"**Kills: {Kills}");
|
Log.WriteDebug($"**HitCount: {HitCount}");
|
||||||
Log.WriteDebug($"**Ratio {currentMaxBoneRatio}");
|
Log.WriteDebug($"**Ratio {currentMaxBoneRatio}");
|
||||||
Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForFlag}");
|
Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForFlag}");
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
foreach (var kvp in HitLocationCount)
|
foreach (var kvp in HitLocationCount)
|
||||||
|
{
|
||||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
Log.WriteDebug(sb.ToString());
|
Log.WriteDebug(sb.ToString());
|
||||||
|
|
||||||
return new DetectionPenaltyResult()
|
result = new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
ClientPenalty = Penalty.PenaltyType.Flag,
|
||||||
RatioAmount = currentMaxBoneRatio,
|
Value = currentMaxBoneRatio,
|
||||||
Bone = bone,
|
Location = bone,
|
||||||
KillCount = Kills
|
HitCount = HitCount,
|
||||||
|
Type = DetectionType.Bone
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -199,12 +308,12 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
}
|
}
|
||||||
|
|
||||||
#region CHEST_ABDOMEN_RATIO_SESSION
|
#region CHEST_ABDOMEN_RATIO_SESSION
|
||||||
int chestKills = HitLocationCount[IW4Info.HitLocation.torso_upper];
|
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper];
|
||||||
|
|
||||||
if (chestKills >= Thresholds.MediumSampleMinKills)
|
if (chestHits >= Thresholds.MediumSampleMinKills)
|
||||||
{
|
{
|
||||||
double marginOfError = Thresholds.GetMarginOfError(chestKills);
|
double marginOfError = Thresholds.GetMarginOfError(chestHits);
|
||||||
double lerpAmount = Math.Min(1.0, (chestKills - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills));
|
double lerpAmount = Math.Min(1.0, (chestHits - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills));
|
||||||
// determine max acceptable ratio of chest to abdomen kills
|
// determine max acceptable ratio of chest to abdomen kills
|
||||||
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;
|
||||||
@ -214,72 +323,101 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag)
|
if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan && chestKills >= Thresholds.MediumSampleMinKills + 30)
|
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan && chestHits >= Thresholds.MediumSampleMinKills + 30)
|
||||||
{
|
{
|
||||||
Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Ban**");
|
//Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Ban**");
|
||||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
//Log.WriteDebug($"ClientId: {hit.AttackerId}");
|
||||||
Log.WriteDebug($"**Chest Kills: {chestKills}");
|
//Log.WriteDebug($"**Chest Hits: {chestHits}");
|
||||||
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
//Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
||||||
Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}");
|
//Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}");
|
||||||
var sb = new StringBuilder();
|
//var sb = new StringBuilder();
|
||||||
foreach (var kvp in HitLocationCount)
|
//foreach (var kvp in HitLocationCount)
|
||||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
// sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
||||||
Log.WriteDebug(sb.ToString());
|
//Log.WriteDebug(sb.ToString());
|
||||||
// Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
|
||||||
|
|
||||||
return new DetectionPenaltyResult()
|
result = new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||||
RatioAmount = currentChestAbdomenRatio,
|
Value = currentChestAbdomenRatio,
|
||||||
Bone = 0,
|
Location = IW4Info.HitLocation.torso_upper,
|
||||||
KillCount = chestKills
|
Type = DetectionType.Chest,
|
||||||
|
HitCount = chestHits
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Flag**");
|
//Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Flag**");
|
||||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
//Log.WriteDebug($"ClientId: {hit.AttackerId}");
|
||||||
Log.WriteDebug($"**Chest Kills: {chestKills}");
|
//Log.WriteDebug($"**Chest Hits: {chestHits}");
|
||||||
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
//Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
||||||
Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}");
|
//Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}");
|
||||||
var sb = new StringBuilder();
|
//var sb = new StringBuilder();
|
||||||
foreach (var kvp in HitLocationCount)
|
//foreach (var kvp in HitLocationCount)
|
||||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
// sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
||||||
Log.WriteDebug(sb.ToString());
|
//Log.WriteDebug(sb.ToString());
|
||||||
// Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
|
||||||
|
|
||||||
return new DetectionPenaltyResult()
|
result = new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
ClientPenalty = Penalty.PenaltyType.Flag,
|
||||||
RatioAmount = currentChestAbdomenRatio,
|
Value = currentChestAbdomenRatio,
|
||||||
Bone = 0,
|
Location = IW4Info.HitLocation.torso_upper,
|
||||||
KillCount = chestKills
|
Type = DetectionType.Chest,
|
||||||
|
HitCount = chestHits
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
#endregion
|
#endregion
|
||||||
return new DetectionPenaltyResult()
|
|
||||||
|
Tracker.OnChange(new EFACSnapshot()
|
||||||
|
{
|
||||||
|
When = hit.When,
|
||||||
|
ClientId = ClientStats.ClientId,
|
||||||
|
SessionAngleOffset = AngleDifferenceAverage,
|
||||||
|
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds,
|
||||||
|
CurrentStrain = currentStrain,
|
||||||
|
CurrentViewAngle = hit.ViewAngles,
|
||||||
|
Hits = HitCount,
|
||||||
|
Kills = Kills,
|
||||||
|
Deaths = ClientStats.SessionDeaths,
|
||||||
|
HitDestinationId = hit.DeathOrigin.Vector3Id,
|
||||||
|
HitDestination = hit.DeathOrigin,
|
||||||
|
HitOriginId = hit.KillOrigin.Vector3Id,
|
||||||
|
HitOrigin = hit.KillOrigin,
|
||||||
|
EloRating = ClientStats.EloRating,
|
||||||
|
HitLocation = hit.HitLoc,
|
||||||
|
LastStrainAngle = Strain.LastAngle,
|
||||||
|
PredictedViewAngles = hit.AnglesList,
|
||||||
|
// this is in "meters"
|
||||||
|
Distance = hit.Distance,
|
||||||
|
SessionScore = ClientStats.SessionScore,
|
||||||
|
HitType = hit.DeathType,
|
||||||
|
SessionSPM = ClientStats.SessionSPM,
|
||||||
|
StrainAngleBetween = Strain.LastDistance,
|
||||||
|
TimeSinceLastEvent = (int)Strain.LastDeltaTime,
|
||||||
|
WeaponId = hit.Weapon
|
||||||
|
});
|
||||||
|
|
||||||
|
return result ?? new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Any,
|
ClientPenalty = Penalty.PenaltyType.Any,
|
||||||
RatioAmount = 0
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public DetectionPenaltyResult ProcessTotalRatio(EFClientStatistics stats)
|
public DetectionPenaltyResult ProcessTotalRatio(EFClientStatistics stats)
|
||||||
{
|
{
|
||||||
int totalChestKills = stats.HitLocations.Single(c => c.Location == IW4Info.HitLocation.torso_upper).HitCount;
|
int totalChestHits = stats.HitLocations.Single(c => c.Location == IW4Info.HitLocation.torso_upper).HitCount;
|
||||||
|
|
||||||
if (totalChestKills >= 60)
|
if (totalChestHits >= 60)
|
||||||
{
|
{
|
||||||
double marginOfError = Thresholds.GetMarginOfError(totalChestKills);
|
double marginOfError = Thresholds.GetMarginOfError(totalChestHits);
|
||||||
double lerpAmount = Math.Min(1.0, (totalChestKills - 60) / 250.0);
|
double lerpAmount = Math.Min(1.0, (totalChestHits - 60) / 250.0);
|
||||||
// determine max acceptable ratio of chest to abdomen kills
|
// determine max acceptable ratio of chest to abdomen kills
|
||||||
double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(3.0), Thresholds.ChestAbdomenRatioThresholdHighSample(2), lerpAmount) + marginOfError;
|
double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(3.0), Thresholds.ChestAbdomenRatioThresholdHighSample(2.0), lerpAmount) + marginOfError;
|
||||||
double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(4.0), Thresholds.ChestAbdomenRatioThresholdHighSample(4.0), lerpAmount) + marginOfError;
|
double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(4.0), Thresholds.ChestAbdomenRatioThresholdHighSample(3.0), lerpAmount) + marginOfError;
|
||||||
|
|
||||||
double currentChestAbdomenRatio = totalChestKills /
|
double currentChestAbdomenRatio = totalChestHits /
|
||||||
stats.HitLocations.Single(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount;
|
stats.HitLocations.Single(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount;
|
||||||
|
|
||||||
if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag)
|
if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag)
|
||||||
@ -287,44 +425,44 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
|
|
||||||
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan)
|
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan)
|
||||||
{
|
{
|
||||||
Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Ban**");
|
//Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Ban**");
|
||||||
Log.WriteDebug($"ClientId: {stats.ClientId}");
|
//Log.WriteDebug($"ClientId: {stats.ClientId}");
|
||||||
Log.WriteDebug($"**Total Chest Kills: {totalChestKills}");
|
//Log.WriteDebug($"**Total Chest Hits: {totalChestHits}");
|
||||||
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
//Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
||||||
Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}");
|
//Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}");
|
||||||
var sb = new StringBuilder();
|
//var sb = new StringBuilder();
|
||||||
foreach (var location in stats.HitLocations)
|
//foreach (var location in stats.HitLocations)
|
||||||
sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
|
// sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
|
||||||
Log.WriteDebug(sb.ToString());
|
//Log.WriteDebug(sb.ToString());
|
||||||
// Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
|
||||||
|
|
||||||
return new DetectionPenaltyResult()
|
return new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||||
RatioAmount = currentChestAbdomenRatio,
|
Value = currentChestAbdomenRatio,
|
||||||
Bone = IW4Info.HitLocation.torso_upper,
|
Location = IW4Info.HitLocation.torso_upper,
|
||||||
KillCount = totalChestKills
|
HitCount = totalChestHits,
|
||||||
|
Type = DetectionType.Chest
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Flag**");
|
//Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Flag**");
|
||||||
Log.WriteDebug($"ClientId: {stats.ClientId}");
|
//Log.WriteDebug($"ClientId: {stats.ClientId}");
|
||||||
Log.WriteDebug($"**Total Chest Kills: {totalChestKills}");
|
//Log.WriteDebug($"**Total Chest Hits: {totalChestHits}");
|
||||||
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
//Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
||||||
Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}");
|
//Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}");
|
||||||
var sb = new StringBuilder();
|
//var sb = new StringBuilder();
|
||||||
foreach (var location in stats.HitLocations)
|
//foreach (var location in stats.HitLocations)
|
||||||
sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
|
// sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
|
||||||
Log.WriteDebug(sb.ToString());
|
//Log.WriteDebug(sb.ToString());
|
||||||
// Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
|
||||||
|
|
||||||
return new DetectionPenaltyResult()
|
return new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
ClientPenalty = Penalty.PenaltyType.Flag,
|
||||||
RatioAmount = currentChestAbdomenRatio,
|
Value = currentChestAbdomenRatio,
|
||||||
Bone = IW4Info.HitLocation.torso_upper,
|
Location = IW4Info.HitLocation.torso_upper,
|
||||||
KillCount = totalChestKills
|
HitCount = totalChestHits,
|
||||||
|
Type = DetectionType.Chest
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -332,7 +470,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
|
|
||||||
return new DetectionPenaltyResult()
|
return new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
Bone = IW4Info.HitLocation.none,
|
|
||||||
ClientPenalty = Penalty.PenaltyType.Any
|
ClientPenalty = Penalty.PenaltyType.Any
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Objects;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||||
{
|
{
|
||||||
class DetectionPenaltyResult
|
class DetectionPenaltyResult
|
||||||
{
|
{
|
||||||
|
public Detection.DetectionType Type { get; set; }
|
||||||
public Penalty.PenaltyType ClientPenalty { get; set; }
|
public Penalty.PenaltyType ClientPenalty { get; set; }
|
||||||
public double RatioAmount { get; set; }
|
public double Value { get; set; }
|
||||||
public IW4Info.HitLocation Bone { get; set; }
|
public IW4Info.HitLocation Location { get; set; }
|
||||||
public int KillCount { get; set; }
|
public int HitCount { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
54
Plugins/Stats/Cheat/Strain.cs
Normal file
54
Plugins/Stats/Cheat/Strain.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using SharedLibraryCore.Helpers;
|
||||||
|
using SharedLibraryCore.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||||
|
{
|
||||||
|
class Strain
|
||||||
|
{
|
||||||
|
private const double StrainDecayBase = 0.9;
|
||||||
|
private double CurrentStrain;
|
||||||
|
public double LastDistance { get; private set; }
|
||||||
|
public Vector3 LastAngle { get; private set; }
|
||||||
|
public double LastDeltaTime { get; private set; }
|
||||||
|
|
||||||
|
public double GetStrain(bool isDamage, int damage, double killDistance, Vector3 newAngle, double deltaTime)
|
||||||
|
{
|
||||||
|
if (LastAngle == null)
|
||||||
|
LastAngle = newAngle;
|
||||||
|
|
||||||
|
LastDeltaTime = deltaTime;
|
||||||
|
|
||||||
|
double decayFactor = GetDecay(deltaTime);
|
||||||
|
CurrentStrain *= decayFactor;
|
||||||
|
double[] distance = Helpers.Extensions.AngleStuff(newAngle, LastAngle);
|
||||||
|
LastDistance = distance[0] + distance[1];
|
||||||
|
|
||||||
|
#if DEBUG == true
|
||||||
|
Console.WriteLine($"Angle Between = {LastDistance}");
|
||||||
|
Console.WriteLine($"Distance From Target = {killDistance}");
|
||||||
|
Console.WriteLine($"Time Offset = {deltaTime}");
|
||||||
|
Console.WriteLine($"Decay Factor = {decayFactor} ");
|
||||||
|
#endif
|
||||||
|
// this happens on first kill
|
||||||
|
if ((distance[0] == 0 && distance[1] == 0) ||
|
||||||
|
deltaTime == 0 ||
|
||||||
|
double.IsNaN(CurrentStrain))
|
||||||
|
{
|
||||||
|
return CurrentStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
double newStrain = Math.Pow(LastDistance, 0.99) / deltaTime;
|
||||||
|
newStrain *= killDistance / 1000.0;
|
||||||
|
|
||||||
|
CurrentStrain += newStrain;
|
||||||
|
|
||||||
|
LastAngle = newAngle;
|
||||||
|
return CurrentStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GetDecay(double deltaTime) => Math.Pow(StrainDecayBase, Math.Pow(2.0, deltaTime / 250.0) / 1000.0);
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||||
{
|
{
|
||||||
@ -31,6 +27,11 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
public const int HighSampleMinKills = 100;
|
public const int HighSampleMinKills = 100;
|
||||||
public const double KillTimeThreshold = 0.2;
|
public const double KillTimeThreshold = 0.2;
|
||||||
|
|
||||||
|
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));
|
||||||
|
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);
|
||||||
|
|
||||||
public static double Lerp(double v1, double v2, double amount)
|
public static double Lerp(double v1, double v2, double amount)
|
||||||
|
82
Plugins/Stats/Commands/MostPlayed.cs
Normal file
82
Plugins/Stats/Commands/MostPlayed.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Objects;
|
||||||
|
using IW4MAdmin.Plugins.Stats.Models;
|
||||||
|
using SharedLibraryCore.Database;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||||
|
|
||||||
|
namespace IW4MAdmin.Plugins.Stats.Commands
|
||||||
|
{
|
||||||
|
class MostPlayed : Command
|
||||||
|
{
|
||||||
|
public static async Task<List<string>> GetMostPlayed(Server s)
|
||||||
|
{
|
||||||
|
long serverId = await StatManager.GetIdForServer(s);
|
||||||
|
|
||||||
|
List<string> mostPlayed = new List<string>()
|
||||||
|
{
|
||||||
|
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--"
|
||||||
|
};
|
||||||
|
|
||||||
|
using (var db = new DatabaseContext())
|
||||||
|
{
|
||||||
|
db.ChangeTracker.AutoDetectChangesEnabled = false;
|
||||||
|
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||||
|
|
||||||
|
var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1);
|
||||||
|
|
||||||
|
var iqStats = (from stats in db.Set<EFClientStatistics>()
|
||||||
|
join client in db.Clients
|
||||||
|
on stats.ClientId equals client.ClientId
|
||||||
|
join alias in db.Aliases
|
||||||
|
on client.CurrentAliasId equals alias.AliasId
|
||||||
|
where stats.ServerId == serverId
|
||||||
|
where client.Level != EFClient.Permission.Banned
|
||||||
|
where client.LastConnection >= thirtyDaysAgo
|
||||||
|
orderby stats.Kills descending
|
||||||
|
select new
|
||||||
|
{
|
||||||
|
alias.Name,
|
||||||
|
client.TotalConnectionTime,
|
||||||
|
stats.Kills
|
||||||
|
})
|
||||||
|
.Take(5);
|
||||||
|
|
||||||
|
var iqList = await iqStats.ToListAsync();
|
||||||
|
|
||||||
|
mostPlayed.AddRange(iqList.Select(stats =>
|
||||||
|
$"^3{stats.Name}^7 - ^5{stats.Kills} ^7{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"]} | ^5{Utilities.GetTimePassed(DateTime.UtcNow.AddSeconds(-stats.TotalConnectionTime), false)} ^7{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PLAYER"].ToLower()}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return mostPlayed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MostPlayed() : base("mostplayed", Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC"], "mp", EFClient.Permission.User, false) { }
|
||||||
|
|
||||||
|
public override async Task ExecuteAsync(GameEvent E)
|
||||||
|
{
|
||||||
|
var topStats = await GetMostPlayed(E.Owner);
|
||||||
|
if (!E.Message.IsBroadcastCommand())
|
||||||
|
{
|
||||||
|
foreach (var stat in topStats)
|
||||||
|
{
|
||||||
|
E.Origin.Tell(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var stat in topStats)
|
||||||
|
{
|
||||||
|
E.Owner.Broadcast(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,42 +1,52 @@
|
|||||||
using SharedLibraryCore;
|
using IW4MAdmin.Plugins.Stats.Models;
|
||||||
using SharedLibraryCore.Objects;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using IW4MAdmin.Plugins.Stats.Models;
|
using SharedLibraryCore;
|
||||||
using System;
|
using SharedLibraryCore.Database;
|
||||||
using System.Collections.Generic;
|
using SharedLibraryCore.Database.Models;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Commands
|
namespace IW4MAdmin.Plugins.Stats.Commands
|
||||||
{
|
{
|
||||||
public class ResetStats : Command
|
public class ResetStats : Command
|
||||||
{
|
{
|
||||||
public ResetStats() : base("resetstats", "reset your stats to factory-new", "rs", Player.Permission.User, false) { }
|
public ResetStats() : base("resetstats", Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_RESET_DESC"], "rs", EFClient.Permission.User, false) { }
|
||||||
|
|
||||||
public override async Task ExecuteAsync(GameEvent E)
|
public override async Task ExecuteAsync(GameEvent E)
|
||||||
{
|
{
|
||||||
if (E.Origin.ClientNumber >= 0)
|
if (E.Origin.ClientNumber >= 0)
|
||||||
{
|
{
|
||||||
var svc = new SharedLibraryCore.Services.GenericRepository<EFClientStatistics>();
|
|
||||||
int serverId = E.Owner.GetHashCode();
|
|
||||||
var stats = svc.Find(s => s.ClientId == E.Origin.ClientId && s.ServerId == serverId).First();
|
|
||||||
|
|
||||||
stats.Deaths = 0;
|
long serverId = await Helpers.StatManager.GetIdForServer(E.Owner);
|
||||||
stats.Kills = 0;
|
|
||||||
stats.SPM = 0.0;
|
|
||||||
stats.Skill = 0.0;
|
|
||||||
|
|
||||||
// reset the cached version
|
EFClientStatistics clientStats;
|
||||||
Plugin.Manager.ResetStats(E.Origin.ClientId, E.Owner.GetHashCode());
|
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||||
|
{
|
||||||
|
clientStats = await ctx.Set<EFClientStatistics>()
|
||||||
|
.Where(s => s.ClientId == E.Origin.ClientId)
|
||||||
|
.Where(s => s.ServerId == serverId)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
// fixme: this doesn't work properly when another context exists
|
clientStats.Deaths = 0;
|
||||||
await svc.SaveChangesAsync();
|
clientStats.Kills = 0;
|
||||||
await E.Origin.Tell("Your stats for this server have been reset");
|
clientStats.SPM = 0.0;
|
||||||
|
clientStats.Skill = 0.0;
|
||||||
|
clientStats.TimePlayed = 0;
|
||||||
|
// todo: make this more dynamic
|
||||||
|
clientStats.EloRating = 200.0;
|
||||||
|
|
||||||
|
// reset the cached version
|
||||||
|
Plugin.Manager.ResetStats(E.Origin.ClientId, serverId);
|
||||||
|
|
||||||
|
// fixme: this doesn't work properly when another context exists
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_RESET_SUCCESS"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await E.Origin.Tell("You must be connected to a server to reset your stats");
|
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_RESET_FAIL"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,52 +8,84 @@ 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Commands
|
namespace IW4MAdmin.Plugins.Stats.Commands
|
||||||
{
|
{
|
||||||
class TopStats : Command
|
class TopStats : Command
|
||||||
{
|
{
|
||||||
public TopStats() : base("topstats", "view the top 5 players on this server", "ts", Player.Permission.User, false) { }
|
public static async Task<List<string>> GetTopStats(Server s)
|
||||||
|
{
|
||||||
|
long serverId = await StatManager.GetIdForServer(s);
|
||||||
|
List<string> topStatsText = new List<string>()
|
||||||
|
{
|
||||||
|
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
|
||||||
|
};
|
||||||
|
|
||||||
|
using (var db = new DatabaseContext(true))
|
||||||
|
{
|
||||||
|
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
|
||||||
|
|
||||||
|
var iqStats = (from stats in db.Set<EFClientStatistics>()
|
||||||
|
join client in db.Clients
|
||||||
|
on stats.ClientId equals client.ClientId
|
||||||
|
join alias in db.Aliases
|
||||||
|
on client.CurrentAliasId equals alias.AliasId
|
||||||
|
where stats.ServerId == serverId
|
||||||
|
where stats.TimePlayed >= Plugin.Config.Configuration().TopPlayersMinPlayTime
|
||||||
|
where client.Level != EFClient.Permission.Banned
|
||||||
|
where client.LastConnection >= fifteenDaysAgo
|
||||||
|
orderby stats.Performance descending
|
||||||
|
select new
|
||||||
|
{
|
||||||
|
stats.KDR,
|
||||||
|
stats.Performance,
|
||||||
|
alias.Name
|
||||||
|
})
|
||||||
|
.Take(5);
|
||||||
|
|
||||||
|
#if DEBUG == true
|
||||||
|
var statsSql = iqStats.ToSql();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var statsList = (await iqStats.ToListAsync())
|
||||||
|
.Select(stats => $"^3{stats.Name}^7 - ^5{stats.KDR} ^7{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"]} | ^5{stats.Performance} ^7{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_PERFORMANCE"]}");
|
||||||
|
|
||||||
|
topStatsText.AddRange(statsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// no one qualified
|
||||||
|
if (topStatsText.Count == 1)
|
||||||
|
{
|
||||||
|
topStatsText = new List<string>()
|
||||||
|
{
|
||||||
|
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_NOQUALIFY"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return topStatsText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TopStats() : base("topstats", Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_DESC"], "ts", EFClient.Permission.User, false) { }
|
||||||
|
|
||||||
public override async Task ExecuteAsync(GameEvent E)
|
public override async Task ExecuteAsync(GameEvent E)
|
||||||
{
|
{
|
||||||
var statsSvc = new GenericRepository<EFClientStatistics>();
|
var topStats = await GetTopStats(E.Owner);
|
||||||
int serverId = E.Owner.GetHashCode();
|
if (!E.Message.IsBroadcastCommand())
|
||||||
|
|
||||||
using (var db = new DatabaseContext())
|
|
||||||
{
|
{
|
||||||
var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1);
|
foreach (var stat in topStats)
|
||||||
var topStats = await (from stats in db.Set<EFClientStatistics>()
|
|
||||||
join client in db.Clients
|
|
||||||
on stats.ClientId equals client.ClientId
|
|
||||||
join alias in db.Aliases
|
|
||||||
on client.CurrentAliasId equals alias.AliasId
|
|
||||||
where stats.TimePlayed >= 3600
|
|
||||||
where client.Level != Player.Permission.Banned
|
|
||||||
where client.LastConnection >= thirtyDaysAgo
|
|
||||||
orderby stats.Skill descending
|
|
||||||
select new
|
|
||||||
{
|
|
||||||
alias.Name,
|
|
||||||
stats.KDR,
|
|
||||||
stats.Skill
|
|
||||||
})
|
|
||||||
.Take(5)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
|
|
||||||
if (!E.Message.IsBroadcastCommand())
|
|
||||||
{
|
{
|
||||||
await E.Origin.Tell("^5--Top Players--");
|
E.Origin.Tell(stat);
|
||||||
|
|
||||||
foreach (var stat in topStats)
|
|
||||||
await E.Origin.Tell($"^3{stat.Name}^7 - ^5{stat.KDR} ^7KDR | ^5{stat.Skill} ^7SKILL");
|
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var stat in topStats)
|
||||||
{
|
{
|
||||||
await E.Owner.Broadcast("^5--Top Players--");
|
E.Owner.Broadcast(stat);
|
||||||
|
|
||||||
foreach (var stat in topStats)
|
|
||||||
await E.Owner.Broadcast($"^3{stat.Name}^7 - ^5{stat.KDR} ^7KDR | ^5{stat.Skill} ^7SKILL");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,16 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using SharedLibraryCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||||
|
using SharedLibraryCore.Database.Models;
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Commands
|
namespace IW4MAdmin.Plugins.Stats.Commands
|
||||||
{
|
{
|
||||||
public class CViewStats : Command
|
public class CViewStats : Command
|
||||||
{
|
{
|
||||||
public CViewStats() : base("stats", "view your stats", "xlrstats", Player.Permission.User, false, new CommandArgument[]
|
public CViewStats() : base("stats", Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_VIEW_DESC"], "xlrstats", EFClient.Permission.User, false, new CommandArgument[]
|
||||||
{
|
{
|
||||||
new CommandArgument()
|
new CommandArgument()
|
||||||
{
|
{
|
||||||
@ -24,54 +28,59 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
|
|
||||||
public override async Task ExecuteAsync(GameEvent E)
|
public override async Task ExecuteAsync(GameEvent E)
|
||||||
{
|
{
|
||||||
if (E.Target?.ClientNumber < 0)
|
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
{
|
|
||||||
await E.Origin.Tell("The specified player must be ingame");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (E.Origin.ClientNumber < 0 && E.Target == null)
|
|
||||||
{
|
|
||||||
await E.Origin.Tell("You must be ingame to view your stats");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String statLine;
|
String statLine;
|
||||||
EFClientStatistics pStats;
|
EFClientStatistics pStats;
|
||||||
|
|
||||||
if (E.Data.Length > 0 && E.Target == null)
|
if (E.Data.Length > 0 && E.Target == null)
|
||||||
{
|
{
|
||||||
await E.Origin.Tell("Cannot find the player you specified");
|
E.Target = E.Owner.GetClientByName(E.Data).FirstOrDefault();
|
||||||
return;
|
|
||||||
|
if (E.Target == null)
|
||||||
|
{
|
||||||
|
E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientStats = new GenericRepository<EFClientStatistics>();
|
long serverId = await StatManager.GetIdForServer(E.Owner);
|
||||||
int serverId = E.Owner.GetHashCode();
|
|
||||||
|
|
||||||
if (E.Target != null)
|
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||||
{
|
{
|
||||||
pStats = clientStats.Find(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId).First();
|
if (E.Target != null)
|
||||||
statLine = String.Format("^5{0} ^7KILLS | ^5{1} ^7DEATHS | ^5{2} ^7KDR | ^5{3} ^7SKILL", pStats.Kills, pStats.Deaths, pStats.KDR, pStats.Skill);
|
{
|
||||||
}
|
int performanceRanking = await StatManager.GetClientOverallRanking(E.Target.ClientId);
|
||||||
|
string performanceRankingString = performanceRanking == 0 ? loc["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{loc["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}";
|
||||||
|
|
||||||
else
|
pStats = (await ctx.Set<EFClientStatistics>().FirstAsync(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId));
|
||||||
{
|
statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}";
|
||||||
pStats = pStats = clientStats.Find(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId).First();
|
}
|
||||||
statLine = String.Format("^5{0} ^7KILLS | ^5{1} ^7DEATHS | ^5{2} ^7KDR | ^5{3} ^7SKILL", pStats.Kills, pStats.Deaths, pStats.KDR, pStats.Skill);
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int performanceRanking = await StatManager.GetClientOverallRanking(E.Origin.ClientId);
|
||||||
|
string performanceRankingString = performanceRanking == 0 ? loc["WEBFRONT_STATS_INDEX_UNRANKED"] : $"{loc["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}";
|
||||||
|
|
||||||
|
pStats = (await ctx.Set<EFClientStatistics>().FirstAsync((c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId)));
|
||||||
|
statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()} | {performanceRankingString}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
await E.Owner.Broadcast($"Stats for ^5{name}^7");
|
E.Owner.Broadcast($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{name}^7");
|
||||||
await E.Owner.Broadcast(statLine);
|
E.Owner.Broadcast(statLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (E.Target != null)
|
if (E.Target != null)
|
||||||
await E.Origin.Tell($"Stats for ^5{E.Target.Name}^7");
|
{
|
||||||
await E.Origin.Tell(statLine);
|
E.Origin.Tell($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{E.Target.Name}^7");
|
||||||
|
}
|
||||||
|
|
||||||
|
E.Origin.Tell(statLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,21 @@
|
|||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Config
|
namespace IW4MAdmin.Plugins.Stats.Config
|
||||||
{
|
{
|
||||||
class StatsConfiguration : IBaseConfiguration
|
public class StatsConfiguration : IBaseConfiguration
|
||||||
{
|
{
|
||||||
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 int TopPlayersMinPlayTime { get; set; }
|
||||||
|
public bool StoreClientKills { get; set; }
|
||||||
public string Name() => "Stats";
|
public string Name() => "Stats";
|
||||||
public IBaseConfiguration Generate()
|
public IBaseConfiguration Generate()
|
||||||
{
|
{
|
||||||
var config = new StatsConfiguration();
|
EnableAntiCheat = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]);
|
||||||
|
KillstreakMessages = new List<StreakMessageConfiguration>()
|
||||||
Console.Write("Enable server-side anti-cheat? [y/n]: ");
|
|
||||||
config.EnableAntiCheat = (Console.ReadLine().ToLower().FirstOrDefault() as char?) == 'y';
|
|
||||||
|
|
||||||
config.KillstreakMessages = new List<StreakMessageConfiguration>()
|
|
||||||
{
|
{
|
||||||
new StreakMessageConfiguration(){
|
new StreakMessageConfiguration(){
|
||||||
Count = -1,
|
Count = -1,
|
||||||
@ -42,7 +36,7 @@ namespace IW4MAdmin.Plugins.Stats.Config
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
config.DeathstreakMessages = new List<StreakMessageConfiguration>()
|
DeathstreakMessages = new List<StreakMessageConfiguration>()
|
||||||
{
|
{
|
||||||
new StreakMessageConfiguration()
|
new StreakMessageConfiguration()
|
||||||
{
|
{
|
||||||
@ -55,7 +49,10 @@ namespace IW4MAdmin.Plugins.Stats.Config
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return config;
|
TopPlayersMinPlayTime = 3600 * 3;
|
||||||
|
StoreClientKills = false;
|
||||||
|
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,5 +22,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
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;
|
||||||
|
|
||||||
public static float ToDegrees(this float value) => value * 180.0f / (float)Math.PI;
|
public static float ToDegrees(this float value) => value * 180.0f / (float)Math.PI;
|
||||||
|
|
||||||
|
public static double[] AngleStuff(Vector3 a, Vector3 b)
|
||||||
|
{
|
||||||
|
double deltaX = 180.0 - Math.Abs(Math.Abs(a.X - b.X) - 180.0);
|
||||||
|
double deltaY = 180.0 - Math.Abs(Math.Abs(a.Y - b.Y) - 180.0);
|
||||||
|
|
||||||
|
return new[] { deltaX, deltaY };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
using SharedLibraryCore;
|
using IW4MAdmin.Plugins.Stats.Cheat;
|
||||||
using IW4MAdmin.Plugins.Stats.Cheat;
|
|
||||||
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;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Helpers
|
namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||||
{
|
{
|
||||||
@ -15,6 +11,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
public ConcurrentDictionary<int, Detection> PlayerDetections { get; set; }
|
public ConcurrentDictionary<int, Detection> PlayerDetections { get; 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 ServerStats(EFServer sv, EFServerStatistics st)
|
public ServerStats(EFServer sv, EFServerStatistics st)
|
||||||
{
|
{
|
||||||
@ -23,5 +20,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
ServerStatistics = st;
|
ServerStatistics = st;
|
||||||
Server = sv;
|
Server = sv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int TeamCount(IW4Info.Team teamName)
|
||||||
|
{
|
||||||
|
if (PlayerStats.Count(p => p.Value.Team == IW4Info.Team.None) / (double)PlayerStats.Count <= 0.25)
|
||||||
|
{
|
||||||
|
return IsTeamBased ? Math.Max(PlayerStats.Count(p => p.Value.Team == teamName), 1) : Math.Max(PlayerStats.Count - 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return IsTeamBased ? (int)Math.Max(Math.Floor(PlayerStats.Count / 2.0), 1) : Math.Max(PlayerStats.Count - 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user