Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
7ec02499a6 | |||
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 |
@ -46,7 +46,7 @@ namespace IW4MAdmin.Application.API
|
||||
|
||||
FlaggedMessageCount = 0;
|
||||
|
||||
E.Owner.Broadcast("If you suspect someone of ^5CHEATING ^7use the ^5!report ^7command").Wait();
|
||||
E.Owner.Broadcast(Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_REPORT"]).Wait(5000);
|
||||
Events.Enqueue(new EventInfo(
|
||||
EventInfo.EventType.ALERT,
|
||||
EventInfo.EventVersion.IW4MAdmin,
|
||||
|
@ -8,9 +8,13 @@ using SharedLibraryCore;
|
||||
|
||||
namespace IW4MAdmin.Application.API.Master
|
||||
{
|
||||
public class HeartbeatState
|
||||
{
|
||||
public bool Connected { get; set; }
|
||||
}
|
||||
|
||||
public class Heartbeat
|
||||
{
|
||||
|
||||
public static async Task Send(ApplicationManager mgr, bool firstHeartbeat = false)
|
||||
{
|
||||
var api = Endpoint.Get();
|
||||
|
@ -8,7 +8,7 @@ using RestEase;
|
||||
namespace IW4MAdmin.Application.API.Master
|
||||
{
|
||||
public class AuthenticationId
|
||||
{
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
@ -60,5 +60,11 @@ namespace IW4MAdmin.Application.API.Master
|
||||
|
||||
[Get("version")]
|
||||
Task<VersionInfo> GetVersion();
|
||||
|
||||
[Get("localization")]
|
||||
Task<List<SharedLibraryCore.Localization.Layout>> GetLocalization();
|
||||
|
||||
[Get("localization/{languageTag}")]
|
||||
Task<SharedLibraryCore.Localization.Layout> GetLocalization([Path("languageTag")] string languageTag);
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@
|
||||
<AssemblyName>IW4MAdmin</AssemblyName>
|
||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||
<Win32Resource />
|
||||
<RootNamespace>IW4MAdmin.Application</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -26,6 +27,10 @@
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj">
|
||||
<Private>true</Private>
|
||||
@ -58,6 +63,19 @@
|
||||
<None Update="Localization\IW4MAdmin.en-US.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\IW4MAdmin.es-EC.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\IW4MAdmin.pt-BR.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Localization\IW4MAdmin.ru-RU.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||
|
@ -15,4 +15,6 @@ if not exist "%TargetDir%Plugins" (
|
||||
xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
|
||||
|
||||
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\"
|
@ -3,6 +3,8 @@ set ProjectDir=%2
|
||||
set TargetDir=%3
|
||||
|
||||
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\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'
|
||||
@ -13,6 +15,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-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
|
||||
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'
|
||||
@ -24,6 +37,23 @@ 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-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
|
||||
if exist "%SolutionDir%Publish\Windows\web.config" del "%SolutionDir%Publish\Windows\web.config"
|
||||
del "%SolutionDir%Publish\Windows\*pdb"
|
||||
|
||||
if exist "%SolutionDir%Publish\WindowsPrerelease\web.config" del "%SolutionDir%Publish\WindowsPrerelease\web.config"
|
||||
del "%SolutionDir%Publish\WindowsPrerelease\*pdb"
|
||||
|
||||
echo making start script
|
||||
@echo dotnet IW4MAdmin.dll > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.cmd"
|
||||
@echo dotnet IW4MAdmin.dll > "%SolutionDir%Publish\Windows\StartIW4MAdmin.cmd"
|
||||
|
@ -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": [
|
||||
"This server uses ^5IW4M Admin v{{VERSION}} ^7get it at ^5raidmax.org/IW4MAdmin",
|
||||
"^5IW4M Admin ^7sees ^5YOU!",
|
||||
"{{TOPSTATS}}",
|
||||
"This server has seen a total of ^5{{TOTALPLAYERS}} ^7players!",
|
||||
"Cheaters are ^1unwelcome ^7 on this server",
|
||||
"Did you know 8/10 people agree with unverified statistics?"
|
||||
|
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,18 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Objects;
|
||||
|
||||
namespace Application.EventParsers
|
||||
namespace IW4MAdmin.Application.EventParsers
|
||||
{
|
||||
class IW4EventParser : IEventParser
|
||||
{
|
||||
public GameEvent GetEvent(Server server, string logLine)
|
||||
public virtual GameEvent GetEvent(Server server, string logLine)
|
||||
{
|
||||
string[] lineSplit = logLine.Split(';');
|
||||
string cleanedEventLine = Regex.Replace(lineSplit[0], @"[0-9]+:[0-9]+\ ", "").Trim();
|
||||
string cleanedEventLine = Regex.Replace(lineSplit[0], @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim();
|
||||
|
||||
if (cleanedEventLine[0] == 'K')
|
||||
{
|
||||
@ -20,7 +21,7 @@ namespace Application.EventParsers
|
||||
{
|
||||
return new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Script,
|
||||
Type = GameEvent.EventType.Kill,
|
||||
Data = logLine,
|
||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
|
||||
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
||||
@ -31,13 +32,27 @@ namespace Application.EventParsers
|
||||
|
||||
if (cleanedEventLine == "say" || cleanedEventLine == "sayteam")
|
||||
{
|
||||
string message = lineSplit[4].Replace("\x15", "");
|
||||
|
||||
if (message[0] == '!' || message[0] == '@')
|
||||
{
|
||||
return new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Command,
|
||||
Data = message,
|
||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
||||
Owner = server,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
return new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Say,
|
||||
Data = lineSplit[4].Replace("\x15", ""),
|
||||
Data = message,
|
||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
||||
Owner = server,
|
||||
Message = lineSplit[4].Replace("\x15", "")
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
@ -45,7 +60,7 @@ namespace Application.EventParsers
|
||||
{
|
||||
return new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Script,
|
||||
Type = GameEvent.EventType.ScriptKill,
|
||||
Data = logLine,
|
||||
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
|
||||
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong()),
|
||||
@ -53,6 +68,33 @@ namespace Application.EventParsers
|
||||
};
|
||||
}
|
||||
|
||||
if (cleanedEventLine.Contains("ScriptDamage"))
|
||||
{
|
||||
return new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.ScriptDamage,
|
||||
Data = logLine,
|
||||
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
|
||||
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong()),
|
||||
Owner = server
|
||||
};
|
||||
}
|
||||
|
||||
if (cleanedEventLine[0] == 'D')
|
||||
{
|
||||
if (Regex.Match(cleanedEventLine, @"^(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)
|
||||
{
|
||||
return new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Damage,
|
||||
Data = cleanedEventLine,
|
||||
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[5].ConvertLong()),
|
||||
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
|
||||
Owner = server
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedEventLine.Contains("ExitLevel"))
|
||||
{
|
||||
return new GameEvent()
|
||||
@ -73,6 +115,8 @@ namespace Application.EventParsers
|
||||
|
||||
if (cleanedEventLine.Contains("InitGame"))
|
||||
{
|
||||
string dump = cleanedEventLine.Replace("InitGame: ", "");
|
||||
|
||||
return new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.MapChange,
|
||||
@ -85,7 +129,8 @@ namespace Application.EventParsers
|
||||
{
|
||||
ClientId = 1
|
||||
},
|
||||
Owner = server
|
||||
Owner = server,
|
||||
Extra = dump.DictionaryFromKeyValue()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,53 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Application.EventParsers
|
||||
namespace IW4MAdmin.Application.EventParsers
|
||||
{
|
||||
class IW5EventParser : IW4EventParser
|
||||
{
|
||||
public override string GetGameDir() => "rzodemo";
|
||||
public override string GetGameDir() => "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 Player()
|
||||
{
|
||||
NetworkId = lineSplit[1].ConvertLong(),
|
||||
ClientNumber = clientNum,
|
||||
Name = lineSplit[3]
|
||||
};
|
||||
|
||||
return new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Join,
|
||||
Origin = new Player()
|
||||
{
|
||||
ClientId = 1
|
||||
},
|
||||
Target = new Player()
|
||||
{
|
||||
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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@ -6,21 +7,21 @@ using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Objects;
|
||||
|
||||
namespace Application.EventParsers
|
||||
namespace IW4MAdmin.Application.EventParsers
|
||||
{
|
||||
class T6MEventParser : IEventParser
|
||||
class T6MEventParser : IW4EventParser
|
||||
{
|
||||
public GameEvent GetEvent(Server server, string logLine)
|
||||
/*public GameEvent GetEvent(Server server, string logLine)
|
||||
{
|
||||
string cleanedLogLine = Regex.Replace(logLine, @"^ *[0-9]+:[0-9]+ *", "");
|
||||
string[] lineSplit = cleanedLogLine.Split(';');
|
||||
string cleanedEventLine = Regex.Replace(logLine, @"^ *[0-9]+:[0-9]+ *", "").Trim();
|
||||
string[] lineSplit = cleanedEventLine.Split(';');
|
||||
|
||||
if (lineSplit[0][0] == 'K')
|
||||
{
|
||||
return new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Script,
|
||||
Data = cleanedLogLine,
|
||||
Type = GameEvent.EventType.Kill,
|
||||
Data = cleanedEventLine,
|
||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
|
||||
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
||||
Owner = server
|
||||
@ -32,7 +33,7 @@ namespace Application.EventParsers
|
||||
return new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Damage,
|
||||
Data = cleanedLogLine,
|
||||
Data = cleanedEventLine,
|
||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
|
||||
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
||||
Owner = server
|
||||
@ -69,13 +70,10 @@ namespace Application.EventParsers
|
||||
};
|
||||
}
|
||||
|
||||
/*if (lineSplit[0].Contains("ShutdownGame"))
|
||||
{
|
||||
|
||||
}*/
|
||||
|
||||
if (lineSplit[0].Contains("InitGame"))
|
||||
{
|
||||
string dump = cleanedEventLine.Replace("InitGame: ", "");
|
||||
|
||||
return new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.MapChange,
|
||||
@ -88,7 +86,8 @@ namespace Application.EventParsers
|
||||
{
|
||||
ClientId = 1
|
||||
},
|
||||
Owner = server
|
||||
Owner = server,
|
||||
Extra = dump.DictionaryFromKeyValue()
|
||||
};
|
||||
}
|
||||
|
||||
@ -105,8 +104,8 @@ namespace Application.EventParsers
|
||||
},
|
||||
Owner = server
|
||||
};
|
||||
}
|
||||
}*/
|
||||
|
||||
public string GetGameDir() => $"t6r{Path.DirectorySeparatorChar}data";
|
||||
public override string GetGameDir() => $"t6r{Path.DirectorySeparatorChar}data";
|
||||
}
|
||||
}
|
||||
|
105
Application/GameEventHandler.cs
Normal file
105
Application/GameEventHandler.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace IW4MAdmin.Application
|
||||
{
|
||||
class GameEventHandler : IEventHandler
|
||||
{
|
||||
private ConcurrentQueue<GameEvent> EventQueue;
|
||||
private Queue<GameEvent> StatusSensitiveQueue;
|
||||
private IManager Manager;
|
||||
|
||||
public GameEventHandler(IManager mgr)
|
||||
{
|
||||
EventQueue = new ConcurrentQueue<GameEvent>();
|
||||
StatusSensitiveQueue = new Queue<GameEvent>();
|
||||
|
||||
Manager = mgr;
|
||||
}
|
||||
|
||||
public void AddEvent(GameEvent gameEvent)
|
||||
{
|
||||
#if DEBUG
|
||||
Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner}");
|
||||
#endif
|
||||
// we need this to keep accurate track of the score
|
||||
if (gameEvent.Type == GameEvent.EventType.Kill ||
|
||||
gameEvent.Type == GameEvent.EventType.Damage ||
|
||||
gameEvent.Type == GameEvent.EventType.ScriptDamage ||
|
||||
gameEvent.Type == GameEvent.EventType.ScriptKill ||
|
||||
gameEvent.Type == GameEvent.EventType.MapChange)
|
||||
{
|
||||
#if DEBUG
|
||||
Manager.GetLogger().WriteDebug($"Added sensitive event to queue");
|
||||
#endif
|
||||
lock (StatusSensitiveQueue)
|
||||
{
|
||||
StatusSensitiveQueue.Enqueue(gameEvent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
EventQueue.Enqueue(gameEvent);
|
||||
Manager.SetHasEvent();
|
||||
}
|
||||
#if DEBUG
|
||||
Manager.GetLogger().WriteDebug($"There are now {EventQueue.Count} events in queue");
|
||||
#endif
|
||||
}
|
||||
|
||||
public string[] GetEventOutput()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public GameEvent GetNextSensitiveEvent()
|
||||
{
|
||||
if (StatusSensitiveQueue.Count > 0)
|
||||
{
|
||||
lock (StatusSensitiveQueue)
|
||||
{
|
||||
if (!StatusSensitiveQueue.TryDequeue(out GameEvent newEvent))
|
||||
{
|
||||
Manager.GetLogger().WriteWarning("Could not dequeue time sensitive event for processing");
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
return newEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public GameEvent GetNextEvent()
|
||||
{
|
||||
if (EventQueue.Count > 0)
|
||||
{
|
||||
#if DEBUG
|
||||
Manager.GetLogger().WriteDebug("Getting next event to be processed");
|
||||
#endif
|
||||
if (!EventQueue.TryDequeue(out GameEvent newEvent))
|
||||
{
|
||||
Manager.GetLogger().WriteWarning("Could not dequeue event for processing");
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
return newEvent;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
78
Application/IO/GameLogEvent.cs
Normal file
78
Application/IO/GameLogEvent.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Application.IO
|
||||
{
|
||||
class GameLogEvent
|
||||
{
|
||||
Server Server;
|
||||
long PreviousFileSize;
|
||||
GameLogReader Reader;
|
||||
string GameLogFile;
|
||||
|
||||
class EventState
|
||||
{
|
||||
public ILogger Log { get; set; }
|
||||
public string ServerId { get; set; }
|
||||
}
|
||||
|
||||
public GameLogEvent(Server server, string gameLogPath, string gameLogName)
|
||||
{
|
||||
GameLogFile = gameLogPath;
|
||||
Reader = new GameLogReader(gameLogPath, server.EventParser);
|
||||
Server = server;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (!server.Manager.ShutdownRequested())
|
||||
{
|
||||
OnEvent(new EventState()
|
||||
{
|
||||
Log = server.Manager.GetLogger(),
|
||||
ServerId = server.ToString()
|
||||
});
|
||||
await Task.Delay(100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnEvent(object state)
|
||||
{
|
||||
long newLength = new FileInfo(GameLogFile).Length;
|
||||
|
||||
try
|
||||
{
|
||||
UpdateLogEvents(newLength);
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
((EventState)state).Log.WriteWarning($"Failed to update log event for {((EventState)state).ServerId}");
|
||||
((EventState)state).Log.WriteDebug($"Exception: {e.Message}");
|
||||
((EventState)state).Log.WriteDebug($"StackTrace: {e.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLogEvents(long fileSize)
|
||||
{
|
||||
if (PreviousFileSize == 0)
|
||||
PreviousFileSize = fileSize;
|
||||
|
||||
long fileDiff = fileSize - PreviousFileSize;
|
||||
|
||||
if (fileDiff < 1)
|
||||
return;
|
||||
|
||||
PreviousFileSize = fileSize;
|
||||
|
||||
var events = Reader.EventsFromLog(Server, fileDiff, 0);
|
||||
foreach (var ev in events)
|
||||
Server.Manager.GetEventHandler().AddEvent(ev);
|
||||
|
||||
PreviousFileSize = fileSize;
|
||||
}
|
||||
}
|
||||
}
|
64
Application/IO/GameLogReader.cs
Normal file
64
Application/IO/GameLogReader.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace IW4MAdmin.Application.IO
|
||||
{
|
||||
class GameLogReader
|
||||
{
|
||||
IEventParser Parser;
|
||||
string LogFile;
|
||||
|
||||
public GameLogReader(string logFile, IEventParser parser)
|
||||
{
|
||||
LogFile = logFile;
|
||||
Parser = parser;
|
||||
}
|
||||
|
||||
public ICollection<GameEvent> EventsFromLog(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))
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
Program.ServerManager.GetLogger().WriteWarning("Could not properly parse event line");
|
||||
Program.ServerManager.GetLogger().WriteDebug(e.Message);
|
||||
Program.ServerManager.GetLogger().WriteDebug(eventLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +1,75 @@
|
||||
using SharedLibraryCore;
|
||||
using IW4MAdmin.Application.API.Master;
|
||||
using SharedLibraryCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Application.Localization
|
||||
{
|
||||
public class Configure
|
||||
{
|
||||
public static void Initialize()
|
||||
public static void Initialize(string customLocale)
|
||||
{
|
||||
string currentLocal = CultureInfo.CurrentCulture.Name;
|
||||
string localizationFile = $"Localization{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocal}.json";
|
||||
string localizationContents;
|
||||
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
|
||||
string[] localizationFiles = Directory.GetFiles("Localization", $"*.{currentLocale}.json");
|
||||
|
||||
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";
|
||||
localizationContents = File.ReadAllText(localizationFile);
|
||||
// the online localization failed so will default to local files
|
||||
}
|
||||
|
||||
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("Localization", $"*.{currentLocale.Substring(0, 2)}*.json");
|
||||
}
|
||||
|
||||
// language doesn't exist either so defaulting to english
|
||||
if (localizationFiles.Length == 0)
|
||||
{
|
||||
localizationFiles = Directory.GetFiles("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().WriteError($"Could not add locale string {item.Key} to localization");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string localizationFile = $"Localization{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocale}-{currentLocale.ToUpper()}.json";
|
||||
|
||||
Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict)
|
||||
{
|
||||
LocalizationName = currentLocale,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,109 +1,269 @@
|
||||
{
|
||||
"LocalizationName": "en-US",
|
||||
"LocalizationSet": {
|
||||
"MANAGER_VERSION_FAIL": "Could not get latest IW4MAdmin version",
|
||||
"MANAGER_VERSION_UPDATE": "has an update. Latest version is",
|
||||
"MANAGER_VERSION_CURRENT": "Your version is",
|
||||
"MANAGER_VERSION_SUCCESS": "IW4MAdmin is up to date",
|
||||
"MANAGER_INIT_FAIL": "Fatal error during initialization",
|
||||
"MANAGER_EXIT": "Press any key to exit...",
|
||||
"SETUP_ENABLE_WEBFRONT": "Enable webfront",
|
||||
"SETUP_ENABLE_MULTIOWN": "Enable multiple owners",
|
||||
"SETUP_ENABLE_STEPPEDPRIV": "Enable stepped privilege hierarchy",
|
||||
"SETUP_ENABLE_CUSTOMSAY": "Enable custom say name",
|
||||
"SETUP_SAY_NAME": "Enter custom say name",
|
||||
"SETUP_USE_CUSTOMENCODING": "Use custom encoding parser",
|
||||
"SETUP_ENCODING_STRING": "Enter encoding string",
|
||||
"SETUP_ENABLE_VPNS": "Enable client VPNs",
|
||||
"SETUP_IPHUB_KEY": "Enter iphub.info api key",
|
||||
"SETUP_DISPLAY_DISCORD": "Display discord link on webfront",
|
||||
"SETUP_DISCORD_INVITE": "Enter discord invite link",
|
||||
"SETUP_SERVER_USET6M": "Use T6M parser",
|
||||
"SETUP_SERVER_IP": "Enter server IP Address",
|
||||
"SETUP_SERVER_PORT": "Enter server port",
|
||||
"SETUP_SERVER_RCON": "Enter server RCon password",
|
||||
"SETUP_SERVER_SAVE": "Configuration saved, add another",
|
||||
"SERVER_KICK_VPNS_NOTALLOWED": "VPNs are not allowed on this server",
|
||||
"SERVER_KICK_TEXT": "You were kicked",
|
||||
"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_GENERICNAME": "Please change your name using /name",
|
||||
"SERVER_KICK_CONTROLCHARS": "Your name cannot contain control characters",
|
||||
"SERVER_TB_TEXT": "You're temporarily banned",
|
||||
"SERVER_TB_REMAIN": "You are temporarily banned",
|
||||
"SERVER_BAN_TEXT": "You're banned",
|
||||
"SERVER_BAN_PREV": "Previously banned for",
|
||||
"SERVER_BAN_APPEAL": "appeal at",
|
||||
"SERVER_REPORT_COUNT": "There are ^5{0} ^7recent reports",
|
||||
"SERVER_WARNLIMT_REACHED": "Too many warnings",
|
||||
"SERVER_WARNING": "Warning",
|
||||
"SERVER_WEBSITE_GENERIC": "this server's website",
|
||||
"BROADCAST_ONLINE": "^5IW4MADMIN ^7is now ^2ONLINE",
|
||||
"BROADCAST_OFFLINE": "IW4MAdmin is going offline",
|
||||
"COMMAND_HELP_SYNTAX": "syntax:",
|
||||
"COMMAND_HELP_OPTIONAL": "optional",
|
||||
"COMMAND_UNKNOWN": "You entered an unknown command",
|
||||
"COMMAND_NOACCESS": "You do not have access to that command",
|
||||
"COMMAND_NOTAUTHORIZED": "You are not authorized to execute that command",
|
||||
"COMMAND_MISSINGARGS": "Not enough arguments supplied",
|
||||
"COMMAND_TARGET_MULTI": "Multiple players match that name",
|
||||
"COMMAND_TARGET_NOTFOUND": "Unable to find specified player",
|
||||
"PLUGIN_IMPORTER_NOTFOUND": "No plugins found to load",
|
||||
"PLUGIN_IMPORTER_REGISTERCMD": "Registered command",
|
||||
"COMMANDS_OWNER_SUCCESS": "Congratulations, you have claimed ownership of this server!",
|
||||
"COMMANDS_OWNER_FAIL": "This server already has an owner",
|
||||
"COMMANDS_WARN_FAIL": "You do not have the required privileges to warn",
|
||||
"COMMANDS_WARNCLEAR_SUCCESS": "All warning cleared for",
|
||||
"COMMANDS_KICK_SUCCESS": "has been kicked",
|
||||
"COMMANDS_KICK_FAIL": "You do not have the required privileges to kick",
|
||||
"COMMANDS_TEMPBAN_SUCCESS": "has been temporarily banned for",
|
||||
"COMMANDS_TEMPBAN_FAIL": "You cannot temporarily ban",
|
||||
"COMMANDS_BAN_SUCCESS": "has been permanently banned",
|
||||
"COMMANDS_BAN_FAIL": "You cannot ban",
|
||||
"COMMANDS_UNBAN_SUCCESS": "Successfully unbanned",
|
||||
"COMMANDS_UNBAN_FAIL": "is not banned",
|
||||
"COMMANDS_HELP_NOTFOUND": "Could not find that command",
|
||||
"COMMANDS_HELP_MOREINFO": "Type !help <command name> to get command usage syntax",
|
||||
"COMMANDS_FASTRESTART_UNMASKED": "fast restarted the map",
|
||||
"COMMANDS_FASTRESTART_MASKED": "The map has been fast restarted",
|
||||
"COMMANDS_MAPROTATE": "Map rotating in ^55 ^7seconds",
|
||||
"COMMANDS_SETLEVEL_SELF": "You cannot change your own level",
|
||||
"COMMANDS_SETLEVEL_OWNER": "There can only be 1 owner. Modify your settings if multiple owners are required",
|
||||
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "This server does not allow you to promote",
|
||||
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "You can only promote ^5{0} ^7to ^5{1} ^7or lower privilege",
|
||||
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "Congratulations! You have been promoted to",
|
||||
"COMMANDS_SETLEVEL_SUCCESS": "was successfully promoted",
|
||||
"COMMANDS_SETLEVEL_FAIL": "Invalid group specified",
|
||||
"COMMANDS_ADMINS_NONE": "No visible administrators online",
|
||||
"COMMANDS_MAP_SUCCESS": "Changing to map",
|
||||
"COMMANDS_MAP_UKN": "Attempting to change to unknown map",
|
||||
"COMMANDS_FIND_MIN": "Please enter at least 3 characters",
|
||||
"COMMANDS_FIND_EMPTY": "No players found",
|
||||
"COMMANDS_RULES_NONE": "The server owner has not set any rules",
|
||||
"COMMANDS_FLAG_SUCCESS": "You have flagged",
|
||||
"COMMANDS_FLAG_UNFLAG": "You have unflagged",
|
||||
"COMMANDS_FLAG_FAIL": "You cannot flag",
|
||||
"COMMANDS_REPORT_FAIL_CAMP": "You cannot report an player for camping",
|
||||
"COMMANDS_REPORT_FAIL_DUPLICATE": "You have already reported this player",
|
||||
"COMMANDS_REPORT_FAIL_SELF": "You cannot report yourself",
|
||||
"COMMANDS_REPORT_FAIL": "You cannot report",
|
||||
"COMMANDS_REPORT_SUCCESS": "Thank you for your report, an administrator has been notified",
|
||||
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Reports successfully cleared",
|
||||
"COMMANDS_REPORTS_NONE": "No players reported yet",
|
||||
"COMMANDS_MASK_ON": "You are now masked",
|
||||
"COMMANDS_MASK_OFF": "You are now unmasked",
|
||||
"COMMANDS_BANINFO_NONE": "No active ban was found for that player",
|
||||
"COMMANDS_BANINO_SUCCESS": "was banned by ^5{0} ^7for:",
|
||||
"COMMANDS_ALIAS_ALIASES": "Aliases",
|
||||
"COMMANDS_ALIAS_IPS": "IPs",
|
||||
"COMMANDS_RCON_SUCCESS": "Successfully sent RCon command",
|
||||
"COMMANDS_PLUGINS_LOADED": "Loaded Plugins",
|
||||
"COMMANDS_IP_SUCCESS": "Your external IP is",
|
||||
"COMMANDS_PRUNE_FAIL": "Invalid number of inactive days",
|
||||
"COMMANDS_PRUNE_SUCCESS": "inactive privileged users were pruned",
|
||||
"COMMANDS_PASSWORD_FAIL": "Your password must be at least 5 characters long",
|
||||
"COMMANDS_PASSWORD_SUCCESS": "Your password has been set successfully",
|
||||
"COMMANDS_PING_TARGET": "ping is",
|
||||
"COMMANDS_PING_SELF": "Your ping is"
|
||||
}
|
||||
"LocalizationName": "en-US",
|
||||
"LocalizationIndex": {
|
||||
"Set": {
|
||||
"BROADCAST_OFFLINE": "^5IW4MAdmin ^7is going ^1OFFLINE",
|
||||
"BROADCAST_ONLINE": "^5IW4MADMIN ^7is now ^2ONLINE",
|
||||
"COMMAND_HELP_OPTIONAL": "optional",
|
||||
"COMMAND_HELP_SYNTAX": "syntax:",
|
||||
"COMMAND_MISSINGARGS": "Not enough arguments supplied",
|
||||
"COMMAND_NOACCESS": "You do not have access to that command",
|
||||
"COMMAND_NOTAUTHORIZED": "You are not authorized to execute that command",
|
||||
"COMMAND_TARGET_MULTI": "Multiple players match that name",
|
||||
"COMMAND_TARGET_NOTFOUND": "Unable to find specified player",
|
||||
"COMMAND_UNKNOWN": "You entered an unknown command",
|
||||
"COMMANDS_ADMINS_DESC": "list currently connected privileged clients",
|
||||
"COMMANDS_ADMINS_NONE": "No visible administrators online",
|
||||
"COMMANDS_ALIAS_ALIASES": "Aliases",
|
||||
"COMMANDS_ALIAS_DESC": "get past aliases and ips of a client",
|
||||
"COMMANDS_ALIAS_IPS": "IPs",
|
||||
"COMMANDS_ARGS_CLEAR": "clear",
|
||||
"COMMANDS_ARGS_CLIENTID": "client id",
|
||||
"COMMANDS_ARGS_COMMANDS": "commands",
|
||||
"COMMANDS_ARGS_DURATION": "duration (m|h|d|w|y)",
|
||||
"COMMANDS_ARGS_INACTIVE": "inactive days",
|
||||
"COMMANDS_ARGS_LEVEL": "level",
|
||||
"COMMANDS_ARGS_MAP": "map",
|
||||
"COMMANDS_ARGS_MESSAGE": "message",
|
||||
"COMMANDS_ARGS_PASSWORD": "password",
|
||||
"COMMANDS_ARGS_PLAYER": "player",
|
||||
"COMMANDS_ARGS_REASON": "reason",
|
||||
"COMMANDS_BAN_DESC": "permanently ban a client from the server",
|
||||
"COMMANDS_BAN_FAIL": "You cannot ban",
|
||||
"COMMANDS_BAN_SUCCESS": "has been permanently banned",
|
||||
"COMMANDS_BANINFO_DESC": "get information about a ban for a client",
|
||||
"COMMANDS_BANINFO_NONE": "No active ban was found for that player",
|
||||
"COMMANDS_BANINO_SUCCESS": "was banned by ^5{0} ^7for:",
|
||||
"COMMANDS_FASTRESTART_DESC": "fast restart current map",
|
||||
"COMMANDS_FASTRESTART_MASKED": "The map has been fast restarted",
|
||||
"COMMANDS_FASTRESTART_UNMASKED": "fast restarted the map",
|
||||
"COMMANDS_FIND_DESC": "find client in database",
|
||||
"COMMANDS_FIND_EMPTY": "No players found",
|
||||
"COMMANDS_FIND_MIN": "Please enter at least 3 characters",
|
||||
"COMMANDS_FLAG_DESC": "flag a suspicious client and announce to admins on join",
|
||||
"COMMANDS_FLAG_FAIL": "You cannot flag",
|
||||
"COMMANDS_FLAG_SUCCESS": "You have flagged",
|
||||
"COMMANDS_FLAG_UNFLAG": "You have unflagged",
|
||||
"COMMANDS_HELP_DESC": "list all available commands",
|
||||
"COMMANDS_HELP_MOREINFO": "Type !help <command name> to get command usage syntax",
|
||||
"COMMANDS_HELP_NOTFOUND": "Could not find that command",
|
||||
"COMMANDS_IP_DESC": "view your external IP address",
|
||||
"COMMANDS_IP_SUCCESS": "Your external IP is",
|
||||
"COMMANDS_KICK_DESC": "kick a client by name",
|
||||
"COMMANDS_KICK_FAIL": "You do not have the required privileges to kick",
|
||||
"COMMANDS_KICK_SUCCESS": "has been kicked",
|
||||
"COMMANDS_LIST_DESC": "list active clients",
|
||||
"COMMANDS_MAP_DESC": "change to specified map",
|
||||
"COMMANDS_MAP_SUCCESS": "Changing to map",
|
||||
"COMMANDS_MAP_UKN": "Attempting to change to unknown map",
|
||||
"COMMANDS_MAPROTATE": "Map rotating in ^55 ^7seconds",
|
||||
"COMMANDS_MAPROTATE_DESC": "cycle to the next map in rotation",
|
||||
"COMMANDS_MASK_DESC": "hide your presence as a privileged client",
|
||||
"COMMANDS_MASK_OFF": "You are now unmasked",
|
||||
"COMMANDS_MASK_ON": "You are now masked",
|
||||
"COMMANDS_OWNER_DESC": "claim ownership of the server",
|
||||
"COMMANDS_OWNER_FAIL": "This server already has an owner",
|
||||
"COMMANDS_OWNER_SUCCESS": "Congratulations, you have claimed ownership of this server!",
|
||||
"COMMANDS_PASSWORD_FAIL": "Your password must be at least 5 characters long",
|
||||
"COMMANDS_PASSWORD_SUCCESS": "Your password has been set successfully",
|
||||
"COMMANDS_PING_DESC": "get client's latency",
|
||||
"COMMANDS_PING_SELF": "Your latency is",
|
||||
"COMMANDS_PING_TARGET": "latency is",
|
||||
"COMMANDS_PLUGINS_DESC": "view all loaded plugins",
|
||||
"COMMANDS_PLUGINS_LOADED": "Loaded Plugins",
|
||||
"COMMANDS_PM_DESC": "send message to other client",
|
||||
"COMMANDS_PRUNE_DESC": "demote any privileged clients that have not connected recently (defaults to 30 days)",
|
||||
"COMMANDS_PRUNE_FAIL": "Invalid number of inactive days",
|
||||
"COMMANDS_PRUNE_SUCCESS": "inactive privileged users were pruned",
|
||||
"COMMANDS_QUIT_DESC": "quit IW4MAdmin",
|
||||
"COMMANDS_RCON_DESC": "send rcon command to server",
|
||||
"COMMANDS_RCON_SUCCESS": "Successfully sent RCon command",
|
||||
"COMMANDS_REPORT_DESC": "report a client for suspicious behavior",
|
||||
"COMMANDS_REPORT_FAIL": "You cannot report",
|
||||
"COMMANDS_REPORT_FAIL_CAMP": "You cannot report an player for camping",
|
||||
"COMMANDS_REPORT_FAIL_DUPLICATE": "You have already reported this player",
|
||||
"COMMANDS_REPORT_FAIL_SELF": "You cannot report yourself",
|
||||
"COMMANDS_REPORT_SUCCESS": "Thank you for your report, an administrator has been notified",
|
||||
"COMMANDS_REPORTS_CLEAR_SUCCESS": "Reports successfully cleared",
|
||||
"COMMANDS_REPORTS_DESC": "get or clear recent reports",
|
||||
"COMMANDS_REPORTS_NONE": "No players reported yet",
|
||||
"COMMANDS_RULES_DESC": "list server rules",
|
||||
"COMMANDS_RULES_NONE": "The server owner has not set any rules",
|
||||
"COMMANDS_SAY_DESC": "broadcast message to all clients",
|
||||
"COMMANDS_SETLEVEL_DESC": "set client to specified privilege level",
|
||||
"COMMANDS_SETLEVEL_FAIL": "Invalid group specified",
|
||||
"COMMANDS_SETLEVEL_LEVELTOOHIGH": "You can only promote ^5{0} ^7to ^5{1} ^7or lower privilege",
|
||||
"COMMANDS_SETLEVEL_OWNER": "There can only be 1 owner. Modify your settings if multiple owners are required",
|
||||
"COMMANDS_SETLEVEL_SELF": "You cannot change your own level",
|
||||
"COMMANDS_SETLEVEL_STEPPEDDISABLED": "This server does not allow you to promote",
|
||||
"COMMANDS_SETLEVEL_SUCCESS": "was successfully promoted",
|
||||
"COMMANDS_SETLEVEL_SUCCESS_TARGET": "Congratulations! You have been promoted to",
|
||||
"COMMANDS_SETPASSWORD_DESC": "set your authentication password",
|
||||
"COMMANDS_TEMPBAN_DESC": "temporarily ban a client for specified time (defaults to 1 hour)",
|
||||
"COMMANDS_TEMPBAN_FAIL": "You cannot temporarily ban",
|
||||
"COMMANDS_TEMPBAN_SUCCESS": "has been temporarily banned for",
|
||||
"COMMANDS_UNBAN_DESC": "unban client by client id",
|
||||
"COMMANDS_UNBAN_FAIL": "is not banned",
|
||||
"COMMANDS_UNBAN_SUCCESS": "Successfully unbanned",
|
||||
"COMMANDS_UPTIME_DESC": "get current application running time",
|
||||
"COMMANDS_UPTIME_TEXT": "has been online for",
|
||||
"COMMANDS_USAGE_DESC": "get application memory usage",
|
||||
"COMMANDS_USAGE_TEXT": "is using",
|
||||
"COMMANDS_WARN_DESC": "warn client for infringing rules",
|
||||
"COMMANDS_WARN_FAIL": "You do not have the required privileges to warn",
|
||||
"COMMANDS_WARNCLEAR_DESC": "remove all warnings for a client",
|
||||
"COMMANDS_WARNCLEAR_SUCCESS": "All warning cleared for",
|
||||
"COMMANDS_WHO_DESC": "give information about yourself",
|
||||
"GLOBAL_DAYS": "days",
|
||||
"GLOBAL_ERROR": "Error",
|
||||
"GLOBAL_HOURS": "hours",
|
||||
"GLOBAL_INFO": "Info",
|
||||
"GLOBAL_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_DAYS": "días",
|
||||
"GLOBAL_ERROR": "Error",
|
||||
"GLOBAL_HOURS": "horas",
|
||||
"GLOBAL_INFO": "Información",
|
||||
"GLOBAL_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_DAYS": "dias",
|
||||
"GLOBAL_ERROR": "Erro",
|
||||
"GLOBAL_HOURS": "horas",
|
||||
"GLOBAL_INFO": "Informação",
|
||||
"GLOBAL_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_DAYS": "дней",
|
||||
"GLOBAL_ERROR": "Ошибка",
|
||||
"GLOBAL_HOURS": "часов",
|
||||
"GLOBAL_INFO": "Информация",
|
||||
"GLOBAL_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,4 +1,6 @@
|
||||
using System;
|
||||
using SharedLibraryCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace IW4MAdmin.Application
|
||||
@ -6,7 +8,7 @@ namespace IW4MAdmin.Application
|
||||
class Logger : SharedLibraryCore.Interfaces.ILogger
|
||||
{
|
||||
enum LogType
|
||||
{
|
||||
{
|
||||
Verbose,
|
||||
Info,
|
||||
Debug,
|
||||
@ -28,7 +30,16 @@ namespace IW4MAdmin.Application
|
||||
|
||||
void Write(string msg, LogType type)
|
||||
{
|
||||
string LogLine = $"[{DateTime.Now.ToString("HH:mm:ss")}] - {type}: {msg}";
|
||||
string stringType = type.ToString();
|
||||
|
||||
try
|
||||
{
|
||||
stringType = Utilities.CurrentLocalization.LocalizationIndex[$"GLOBAL_{type.ToString().ToUpper()}"];
|
||||
}
|
||||
|
||||
catch (Exception) { }
|
||||
|
||||
string LogLine = $"[{DateTime.Now.ToString("HH:mm:ss")}] - {stringType}: {msg}";
|
||||
lock (ThreadLock)
|
||||
{
|
||||
#if DEBUG
|
||||
|
@ -6,6 +6,10 @@ using System.Reflection;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Objects;
|
||||
using SharedLibraryCore.Database;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using SharedLibraryCore.Localization;
|
||||
|
||||
namespace IW4MAdmin.Application
|
||||
{
|
||||
@ -14,15 +18,18 @@ namespace IW4MAdmin.Application
|
||||
static public double Version { get; private set; }
|
||||
static public ApplicationManager ServerManager = ApplicationManager.GetInstance();
|
||||
public static string OperatingDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
|
||||
private static ManualResetEventSlim OnShutdownComplete = new ManualResetEventSlim();
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
AppDomain.CurrentDomain.SetData("DataDirectory", OperatingDirectory);
|
||||
System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.BelowNormal;
|
||||
Localization.Configure.Initialize();
|
||||
var loc = Utilities.CurrentLocalization.LocalizationSet;
|
||||
//System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.BelowNormal;
|
||||
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
|
||||
Version = Assembly.GetExecutingAssembly().GetName().Version.Major + Assembly.GetExecutingAssembly().GetName().Version.Minor / 10.0f;
|
||||
Version = Math.Round(Version, 2);
|
||||
|
||||
Console.WriteLine("=====================================================");
|
||||
Console.WriteLine(" IW4M ADMIN");
|
||||
@ -30,16 +37,22 @@ namespace IW4MAdmin.Application
|
||||
Console.WriteLine($" Version {Version.ToString("0.0")}");
|
||||
Console.WriteLine("=====================================================");
|
||||
|
||||
Index loc = null;
|
||||
|
||||
try
|
||||
{
|
||||
using (var db = new DatabaseContext())
|
||||
new ContextSeed(db).Seed().Wait();
|
||||
|
||||
CheckDirectories();
|
||||
|
||||
ServerManager = ApplicationManager.GetInstance();
|
||||
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
|
||||
Localization.Configure.Initialize(ServerManager.GetApplicationSettings().Configuration()?.CustomLocale);
|
||||
loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
|
||||
using (var db = new DatabaseContext(ServerManager.GetApplicationSettings().Configuration()?.ConnectionString))
|
||||
new ContextSeed(db).Seed().Wait();
|
||||
|
||||
var api = API.Master.Endpoint.Get();
|
||||
|
||||
var version = new API.Master.VersionInfo()
|
||||
{
|
||||
CurrentVersionStable = 99.99f
|
||||
@ -65,7 +78,7 @@ namespace IW4MAdmin.Application
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine(loc["MANAGER_VERSION_FAIL"]);
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
}
|
||||
|
||||
#if !PRERELEASE
|
||||
@ -74,7 +87,7 @@ namespace IW4MAdmin.Application
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine($"IW4MAdmin {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionStable.ToString("0.0")}]");
|
||||
Console.WriteLine($"{loc["MANAGER_VERSION_CURRENT"]} [v{Version.ToString("0.0")}]");
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
}
|
||||
#else
|
||||
else if (version.CurrentVersionPrerelease > Version)
|
||||
@ -82,14 +95,14 @@ namespace IW4MAdmin.Application
|
||||
Console.ForegroundColor = ConsoleColor.DarkYellow;
|
||||
Console.WriteLine($"IW4MAdmin-Prerelease {loc["MANAGER_VERSION_UPDATE"]} [v{version.CurrentVersionPrerelease.ToString("0.0")}-pr]");
|
||||
Console.WriteLine($"{loc["MANAGER_VERSION_CURRENT"]} [v{Version.ToString("0.0")}-pr]");
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
}
|
||||
#endif
|
||||
else
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
}
|
||||
|
||||
ServerManager.Init().Wait();
|
||||
@ -108,26 +121,28 @@ namespace IW4MAdmin.Application
|
||||
|
||||
if (ServerManager.Servers.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No servers are currently being monitored");
|
||||
Console.WriteLine(loc["MANAGER_CONSOLE_NOSERV"]);
|
||||
continue;
|
||||
}
|
||||
|
||||
Origin.CurrentServer = ServerManager.Servers[0];
|
||||
GameEvent E = new GameEvent(GameEvent.EventType.Say, userInput, Origin, null, ServerManager.Servers[0]);
|
||||
ServerManager.Servers[0].ExecuteEvent(E);
|
||||
if (userInput?.Length > 0)
|
||||
{
|
||||
Origin.CurrentServer = ServerManager.Servers[0];
|
||||
GameEvent E = new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Command,
|
||||
Data = userInput,
|
||||
Origin = Origin,
|
||||
Owner = ServerManager.Servers[0]
|
||||
};
|
||||
|
||||
ServerManager.GetEventHandler().AddEvent(E);
|
||||
E.OnProcessed.Wait(5000);
|
||||
}
|
||||
Console.Write('>');
|
||||
|
||||
} while (ServerManager.Running);
|
||||
});
|
||||
|
||||
if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront)
|
||||
{
|
||||
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
|
||||
}
|
||||
|
||||
ServerManager.Start();
|
||||
ServerManager.Logger.WriteVerbose("Shutdown complete");
|
||||
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
@ -140,7 +155,24 @@ namespace IW4MAdmin.Application
|
||||
Console.WriteLine($"Exception: {e.Message}");
|
||||
Console.WriteLine(loc["MANAGER_EXIT"]);
|
||||
Console.ReadKey();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ServerManager.GetApplicationSettings().Configuration().EnableWebFront)
|
||||
{
|
||||
Task.Run(() => WebfrontCore.Program.Init(ServerManager));
|
||||
}
|
||||
|
||||
OnShutdownComplete.Reset();
|
||||
ServerManager.Start().Wait();
|
||||
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
|
||||
OnShutdownComplete.Set();
|
||||
}
|
||||
|
||||
private static void OnCancelKey(object sender, ConsoleCancelEventArgs e)
|
||||
{
|
||||
ServerManager.Stop();
|
||||
OnShutdownComplete.Wait(5000);
|
||||
}
|
||||
|
||||
static void CheckDirectories()
|
||||
|
@ -19,6 +19,7 @@ using SharedLibraryCore.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Text;
|
||||
using IW4MAdmin.Application.API.Master;
|
||||
|
||||
namespace IW4MAdmin.Application
|
||||
{
|
||||
@ -41,11 +42,8 @@ namespace IW4MAdmin.Application
|
||||
PenaltyService PenaltySvc;
|
||||
BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
||||
EventApi Api;
|
||||
#if FTP_LOG
|
||||
const int UPDATE_FREQUENCY = 700;
|
||||
#else
|
||||
const int UPDATE_FREQUENCY = 450;
|
||||
#endif
|
||||
GameEventHandler Handler;
|
||||
ManualResetEventSlim OnEvent;
|
||||
|
||||
private ApplicationManager()
|
||||
{
|
||||
@ -61,16 +59,10 @@ namespace IW4MAdmin.Application
|
||||
Api = new EventApi();
|
||||
ServerEventOccurred += Api.OnServerEvent;
|
||||
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
||||
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
|
||||
StartTime = DateTime.UtcNow;
|
||||
OnEvent = new ManualResetEventSlim();
|
||||
}
|
||||
|
||||
private void OnCancelKey(object sender, ConsoleCancelEventArgs args)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
|
||||
public IList<Server> GetServers()
|
||||
{
|
||||
return Servers;
|
||||
@ -86,8 +78,74 @@ namespace IW4MAdmin.Application
|
||||
return Instance ?? (Instance = new ApplicationManager());
|
||||
}
|
||||
|
||||
public async Task UpdateStatus(object state)
|
||||
{
|
||||
var taskList = new List<Task>();
|
||||
|
||||
while (Running)
|
||||
{
|
||||
taskList.Clear();
|
||||
foreach (var server in Servers)
|
||||
{
|
||||
taskList.Add(Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await server.ProcessUpdatesAsync(new CancellationToken());
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.WriteWarning($"Failed to update status for {server}");
|
||||
Logger.WriteDebug($"Exception: {e.Message}");
|
||||
Logger.WriteDebug($"StackTrace: {e.StackTrace}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
#if DEBUG
|
||||
Logger.WriteDebug($"{taskList.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.WhenAll(taskList.ToArray());
|
||||
|
||||
GameEvent sensitiveEvent;
|
||||
while ((sensitiveEvent = Handler.GetNextSensitiveEvent()) != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await sensitiveEvent.Owner.ExecuteEvent(sensitiveEvent);
|
||||
#if DEBUG
|
||||
Logger.WriteDebug($"Processed Sensitive Event {sensitiveEvent.Type}");
|
||||
#endif
|
||||
}
|
||||
|
||||
catch (NetworkException e)
|
||||
{
|
||||
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]);
|
||||
Logger.WriteDebug(e.Message);
|
||||
}
|
||||
|
||||
catch (Exception E)
|
||||
{
|
||||
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {sensitiveEvent.Owner}");
|
||||
Logger.WriteDebug("Error Message: " + E.Message);
|
||||
Logger.WriteDebug("Error Trace: " + E.StackTrace);
|
||||
}
|
||||
|
||||
sensitiveEvent.OnProcessed.Set();
|
||||
}
|
||||
|
||||
await Task.Delay(2500);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Init()
|
||||
{
|
||||
Running = true;
|
||||
|
||||
#region DATABASE
|
||||
var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted))
|
||||
.Select(c => new
|
||||
@ -138,7 +196,13 @@ namespace IW4MAdmin.Application
|
||||
if (newConfig.Servers == null)
|
||||
{
|
||||
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;
|
||||
await ConfigHandler.Save();
|
||||
}
|
||||
@ -178,7 +242,7 @@ namespace IW4MAdmin.Application
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
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($"Stack Trace: {e.StackTrace}");
|
||||
}
|
||||
@ -211,6 +275,7 @@ namespace IW4MAdmin.Application
|
||||
Commands.Add(new CListRules());
|
||||
Commands.Add(new CPrivateMessage());
|
||||
Commands.Add(new CFlag());
|
||||
Commands.Add(new CUnflag());
|
||||
Commands.Add(new CReport());
|
||||
Commands.Add(new CListReports());
|
||||
Commands.Add(new CListBanInfo());
|
||||
@ -231,6 +296,8 @@ namespace IW4MAdmin.Application
|
||||
#region INIT
|
||||
async Task Init(ServerConfiguration Conf)
|
||||
{
|
||||
// setup the event handler after the class is initialized
|
||||
Handler = new GameEventHandler(this);
|
||||
try
|
||||
{
|
||||
var ServerInstance = new IW4MServer(this, Conf);
|
||||
@ -241,21 +308,16 @@ namespace IW4MAdmin.Application
|
||||
_servers.Add(ServerInstance);
|
||||
}
|
||||
|
||||
Logger.WriteVerbose($"Now monitoring {ServerInstance.Hostname}");
|
||||
|
||||
// this way we can keep track of execution time and see if problems arise.
|
||||
var Status = new AsyncStatus(ServerInstance, UPDATE_FREQUENCY);
|
||||
lock (TaskStatuses)
|
||||
{
|
||||
TaskStatuses.Add(Status);
|
||||
}
|
||||
Logger.WriteVerbose($"{Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"]} {ServerInstance.Hostname}");
|
||||
// add the start event for this server
|
||||
Handler.AddEvent(new GameEvent(GameEvent.EventType.Start, "Server started", null, null, ServerInstance));
|
||||
}
|
||||
|
||||
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))
|
||||
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))
|
||||
{
|
||||
Logger.WriteDebug(e.Message);
|
||||
@ -267,113 +329,141 @@ namespace IW4MAdmin.Application
|
||||
}
|
||||
|
||||
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
|
||||
|
||||
#endregion
|
||||
|
||||
Running = true;
|
||||
}
|
||||
|
||||
private void HeartBeatThread()
|
||||
private async Task SendHeartbeat(object state)
|
||||
{
|
||||
bool successfulConnection = false;
|
||||
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);
|
||||
}
|
||||
var heartbeatState = (HeartbeatState)state;
|
||||
|
||||
while (Running)
|
||||
{
|
||||
Logger.WriteDebug("Sending heartbeat...");
|
||||
try
|
||||
if (!heartbeatState.Connected)
|
||||
{
|
||||
API.Master.Heartbeat.Send(this).Wait();
|
||||
}
|
||||
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)
|
||||
try
|
||||
{
|
||||
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;
|
||||
goto restartConnection;
|
||||
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
heartbeatState.Connected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
catch (RestEase.ApiException e)
|
||||
{
|
||||
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
||||
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
catch (RestEase.ApiException e)
|
||||
{
|
||||
successfulConnection = false;
|
||||
goto restartConnection;
|
||||
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
|
||||
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 async Task Start()
|
||||
{
|
||||
Task.Run(() => HeartBeatThread());
|
||||
while (Running || TaskStatuses.Count > 0)
|
||||
// this needs to be run seperately from the main thread
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
#if !DEBUG
|
||||
// start heartbeat
|
||||
Task.Run(() => SendHeartbeat(new HeartbeatState()));
|
||||
#endif
|
||||
Task.Run(() => UpdateStatus(null));
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
|
||||
var eventList = new List<Task>();
|
||||
|
||||
async Task processEvent(GameEvent newEvent)
|
||||
{
|
||||
for (int i = 0; i < TaskStatuses.Count; i++)
|
||||
try
|
||||
{
|
||||
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();
|
||||
}
|
||||
await newEvent.Owner.ExecuteEvent(newEvent);
|
||||
#if DEBUG
|
||||
Logger.WriteDebug("Processed Event");
|
||||
#endif
|
||||
}
|
||||
|
||||
Thread.Sleep(UPDATE_FREQUENCY);
|
||||
// this happens if a plugin requires login
|
||||
catch (AuthorizationException e)
|
||||
{
|
||||
await newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
|
||||
}
|
||||
|
||||
catch (NetworkException e)
|
||||
{
|
||||
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]);
|
||||
Logger.WriteDebug(e.Message);
|
||||
}
|
||||
|
||||
catch (Exception E)
|
||||
{
|
||||
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}");
|
||||
Logger.WriteDebug("Error Message: " + E.Message);
|
||||
Logger.WriteDebug("Error Trace: " + E.StackTrace);
|
||||
}
|
||||
// tell anyone waiting for the output that we're done
|
||||
newEvent.OnProcessed.Set();
|
||||
};
|
||||
|
||||
GameEvent queuedEvent = null;
|
||||
|
||||
while (Running)
|
||||
{
|
||||
// wait for new event to be added
|
||||
OnEvent.Wait();
|
||||
|
||||
// todo: sequencially or parallelize?
|
||||
while ((queuedEvent = Handler.GetNextEvent()) != null)
|
||||
{
|
||||
await processEvent(queuedEvent);
|
||||
}
|
||||
|
||||
// this should allow parallel processing of events
|
||||
// await Task.WhenAll(eventList);
|
||||
|
||||
// signal that all events have been processed
|
||||
OnEvent.Reset();
|
||||
}
|
||||
#if !DEBUG
|
||||
foreach (var S in Servers)
|
||||
S.Broadcast(Utilities.CurrentLocalization.LocalizationSet["BROADCAST_OFFLINE"]).Wait();
|
||||
foreach (var S in _servers)
|
||||
await S.Broadcast("^1" + Utilities.CurrentLocalization.LocalizationIndex["BROADCAST_OFFLINE"]);
|
||||
#endif
|
||||
_servers.Clear();
|
||||
}
|
||||
@ -382,6 +472,9 @@ namespace IW4MAdmin.Application
|
||||
public void Stop()
|
||||
{
|
||||
Running = false;
|
||||
|
||||
// trigger the event processing loop to end
|
||||
SetHasEvent();
|
||||
}
|
||||
|
||||
public ILogger GetLogger()
|
||||
@ -411,5 +504,11 @@ namespace IW4MAdmin.Application
|
||||
public IDictionary<int, Player> GetPrivilegedClients() => PrivilegedClients;
|
||||
public IEventApi GetEventApi() => Api;
|
||||
public bool ShutdownRequested() => !Running;
|
||||
public IEventHandler GetEventHandler() => Handler;
|
||||
|
||||
public void SetHasEvent()
|
||||
{
|
||||
OnEvent.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,11 @@ namespace Application.Misc
|
||||
string response = await RequestClient.GetStringAsync($"http://v2.api.iphub.info/ip/{ip}");
|
||||
var responseJson = JsonConvert.DeserializeObject<JObject>(response);
|
||||
int blockType = Convert.ToInt32(responseJson["block"]);
|
||||
/*if (responseJson.ContainsKey("isp"))
|
||||
{
|
||||
if (responseJson["isp"].ToString() == "TSF-IP-CORE")
|
||||
return true;
|
||||
}*/
|
||||
return blockType == 1;
|
||||
}
|
||||
}
|
||||
|
22
Application/RconParsers/IW3RConParser.cs
Normal file
22
Application/RconParsers/IW3RConParser.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Application.RconParsers;
|
||||
using SharedLibraryCore.RCon;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Application.RconParsers
|
||||
{
|
||||
class IW3RConParser : IW4RConParser
|
||||
{
|
||||
private static 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;
|
||||
}
|
||||
}
|
@ -22,10 +22,13 @@ namespace Application.RconParsers
|
||||
Ban = "clientkick {0} \"{1}\"",
|
||||
TempBan = "tempbanclient {0} \"{1}\""
|
||||
};
|
||||
|
||||
|
||||
private static 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)
|
||||
{
|
||||
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)
|
||||
@ -69,7 +72,7 @@ namespace Application.RconParsers
|
||||
return (await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"set {dvarName} {dvarValue}")).Length > 0;
|
||||
}
|
||||
|
||||
public CommandPrefix GetCommandPrefixes() => Prefixes;
|
||||
public virtual CommandPrefix GetCommandPrefixes() => Prefixes;
|
||||
|
||||
private List<Player> ClientsFromStatus(string[] Status)
|
||||
{
|
||||
@ -78,37 +81,57 @@ namespace Application.RconParsers
|
||||
if (Status.Length < 4)
|
||||
throw new ServerException("Unexpected status response received");
|
||||
|
||||
int validMatches = 0;
|
||||
foreach (String S in Status)
|
||||
{
|
||||
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);
|
||||
int cID = -1;
|
||||
int Ping = -1;
|
||||
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())));
|
||||
long npID = Regex.Match(responseLine, @"([a-z]|[0-9]){16}", RegexOptions.IgnoreCase).Value.ConvertLong();
|
||||
int.TryParse(playerInfo[0], out cID);
|
||||
var regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}");
|
||||
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]);
|
||||
validMatches++;
|
||||
int clientNumber = int.Parse(regex.Groups[1].Value);
|
||||
int score = int.Parse(regex.Groups[2].Value);
|
||||
|
||||
int ping = 999;
|
||||
|
||||
// their state can be CNCT, ZMBI etc
|
||||
if (regex.Groups[3].Value.Length <= 3)
|
||||
{
|
||||
ping = int.Parse(regex.Groups[3].Value);
|
||||
}
|
||||
|
||||
long networkId = regex.Groups[4].Value.ConvertLong();
|
||||
string name = regex.Groups[5].Value.StripColors().Trim();
|
||||
int ip = regex.Groups[7].Value.Split(':')[0].ConvertToIP();
|
||||
|
||||
Player P = new Player()
|
||||
{
|
||||
Name = cName,
|
||||
NetworkId = npID,
|
||||
ClientNumber = cID,
|
||||
IPAddress = cIP,
|
||||
Ping = Ping,
|
||||
Name = name,
|
||||
NetworkId = networkId,
|
||||
ClientNumber = clientNumber,
|
||||
IPAddress = ip,
|
||||
Ping = ping,
|
||||
Score = score,
|
||||
IsBot = npID == -1
|
||||
IsBot = ip == 0
|
||||
};
|
||||
|
||||
if (P.IsBot)
|
||||
{
|
||||
P.IPAddress = P.ClientNumber + 1;
|
||||
}
|
||||
|
||||
StatusPlayers.Add(P);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
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 SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Objects;
|
||||
using SharedLibraryCore.RCon;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Application.RconParsers
|
||||
{
|
||||
public class IW5MRConParser : IRConParser
|
||||
{
|
||||
private static 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<Player>> 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<Player> ClientsFromStatus(string[] status)
|
||||
{
|
||||
List<Player> StatusPlayers = new List<Player>();
|
||||
|
||||
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 Player()
|
||||
{
|
||||
Name = name,
|
||||
NetworkId = networkId,
|
||||
ClientNumber = clientId,
|
||||
IPAddress = ipAddress,
|
||||
Ping = Ping,
|
||||
Score = score,
|
||||
IsBot = false
|
||||
};
|
||||
|
||||
StatusPlayers.Add(p);
|
||||
|
||||
if (p.IsBot)
|
||||
p.NetworkId = -p.ClientNumber;
|
||||
}
|
||||
}
|
||||
|
||||
return StatusPlayers;
|
||||
}
|
||||
}
|
||||
}
|
@ -149,7 +149,6 @@ namespace Application.RconParsers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private List<Player> ClientsFromStatus(string[] status)
|
||||
{
|
||||
List<Player> StatusPlayers = new List<Player>();
|
||||
@ -178,8 +177,7 @@ namespace Application.RconParsers
|
||||
int score = 0;
|
||||
// todo: fix this when T6M score is valid ;)
|
||||
//int score = Int32.Parse(playerInfo[1]);
|
||||
|
||||
StatusPlayers.Add(new Player()
|
||||
var p = new Player()
|
||||
{
|
||||
Name = name,
|
||||
NetworkId = networkId,
|
||||
@ -187,8 +185,13 @@ namespace Application.RconParsers
|
||||
IPAddress = ipAddress,
|
||||
Ping = Ping,
|
||||
Score = score,
|
||||
IsBot = networkId < 1
|
||||
});
|
||||
IsBot = networkId == 0
|
||||
};
|
||||
|
||||
if (p.IsBot)
|
||||
p.NetworkId = -p.ClientNumber;
|
||||
|
||||
StatusPlayers.Add(p);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,15 +17,16 @@ using SharedLibraryCore.Exceptions;
|
||||
|
||||
using Application.Misc;
|
||||
using Application.RconParsers;
|
||||
using Application.EventParsers;
|
||||
|
||||
using IW4MAdmin.Application.EventParsers;
|
||||
using IW4MAdmin.Application.IO;
|
||||
using SharedLibraryCore.Localization;
|
||||
|
||||
namespace IW4MAdmin
|
||||
{
|
||||
public class IW4MServer : Server
|
||||
{
|
||||
private CancellationToken cts;
|
||||
private static Dictionary<string, string> loc = Utilities.CurrentLocalization.LocalizationSet;
|
||||
private static Index loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
private GameLogEvent LogEvent;
|
||||
|
||||
public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg) { }
|
||||
|
||||
@ -34,7 +35,7 @@ namespace IW4MAdmin
|
||||
// todo: make this better with collisions
|
||||
int id = Math.Abs($"{IP}:{Port.ToString()}".Select(a => (int)a).Sum());
|
||||
|
||||
// this is a nasty fix for get hashcode being changed
|
||||
// hack: this is a nasty fix for get hashcode being changed
|
||||
switch (id)
|
||||
{
|
||||
case 765:
|
||||
@ -50,9 +51,8 @@ namespace IW4MAdmin
|
||||
|
||||
override public async Task<bool> AddPlayer(Player polledPlayer)
|
||||
{
|
||||
|
||||
if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot)||
|
||||
polledPlayer.Ping < 1 || polledPlayer.ClientNumber > (MaxClients) ||
|
||||
if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot) ||
|
||||
polledPlayer.Ping < 1 ||
|
||||
polledPlayer.ClientNumber < 0)
|
||||
{
|
||||
//Logger.WriteDebug($"Skipping client not in connected state {P}");
|
||||
@ -159,6 +159,15 @@ namespace IW4MAdmin
|
||||
|
||||
var activePenalties = await Manager.GetPenaltyService().GetActivePenaltiesAsync(player.AliasLinkId, player.IPAddress);
|
||||
var currentBan = activePenalties.FirstOrDefault(b => b.Expires > DateTime.UtcNow);
|
||||
var currentAutoFlag = activePenalties.Where(p => p.Type == Penalty.PenaltyType.Flag && p.PunisherId == 1)
|
||||
.OrderByDescending(p => p.When)
|
||||
.FirstOrDefault();
|
||||
|
||||
// remove their auto flag status after a week
|
||||
if (currentAutoFlag != null && (DateTime.Now - currentAutoFlag.When).TotalDays > 7)
|
||||
{
|
||||
player.Level = Player.Permission.User;
|
||||
}
|
||||
|
||||
if (currentBan != null)
|
||||
{
|
||||
@ -176,26 +185,36 @@ namespace IW4MAdmin
|
||||
|
||||
if (player.Level != Player.Permission.Banned && currentBan.Type == Penalty.PenaltyType.Ban)
|
||||
await player.Ban($"{loc["SERVER_BAN_PREV"]} {currentBan.Offense}", autoKickClient);
|
||||
|
||||
// they didn't fully connect so empty their slot
|
||||
Players[player.ClientNumber] = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
Logger.WriteInfo($"Client {player} connecting...");
|
||||
|
||||
await ExecuteEvent(new GameEvent(GameEvent.EventType.Connect, "", player, null, this));
|
||||
|
||||
|
||||
if (!Manager.GetApplicationSettings().Configuration().EnableClientVPNs &&
|
||||
await VPNCheck.UsingVPN(player.IPAddressString, Manager.GetApplicationSettings().Configuration().IPHubAPIKey))
|
||||
{
|
||||
await player.Kick(Utilities.CurrentLocalization.LocalizationSet["SERVER_KICK_VPNS_NOTALLOWED"], new Player() { ClientId = 1 });
|
||||
await player.Kick(Utilities.CurrentLocalization.LocalizationIndex["SERVER_KICK_VPNS_NOTALLOWED"], new Player() { ClientId = 1 });
|
||||
return true;
|
||||
}
|
||||
|
||||
var e = new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Connect,
|
||||
Origin = player,
|
||||
Owner = this
|
||||
};
|
||||
Manager.GetEventHandler().AddEvent(e);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
catch (Exception E)
|
||||
{
|
||||
Manager.GetLogger().WriteError($"Unable to add player {polledPlayer.Name}::{polledPlayer.NetworkId}");
|
||||
Manager.GetLogger().WriteError($"{loc["SERVER_ERROR_ADDPLAYER"]} {polledPlayer.Name}::{polledPlayer.NetworkId}");
|
||||
Manager.GetLogger().WriteDebug(E.StackTrace);
|
||||
return false;
|
||||
}
|
||||
@ -209,7 +228,11 @@ namespace IW4MAdmin
|
||||
Player Leaving = Players[cNum];
|
||||
Logger.WriteInfo($"Client {Leaving} disconnecting...");
|
||||
|
||||
await ExecuteEvent(new GameEvent(GameEvent.EventType.Disconnect, "", Leaving, null, this));
|
||||
var e = new GameEvent(GameEvent.EventType.Disconnect, "", Leaving, null, this);
|
||||
Manager.GetEventHandler().AddEvent(e);
|
||||
|
||||
// wait until the disconnect event is complete
|
||||
e.OnProcessed.Wait();
|
||||
|
||||
Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds;
|
||||
Leaving.LastConnection = DateTime.UtcNow;
|
||||
@ -256,16 +279,8 @@ namespace IW4MAdmin
|
||||
|
||||
if (C.RequiresTarget || Args.Length > 0)
|
||||
{
|
||||
int cNum = -1;
|
||||
try
|
||||
{
|
||||
cNum = Convert.ToInt32(Args[0]);
|
||||
}
|
||||
|
||||
catch (FormatException)
|
||||
{
|
||||
|
||||
}
|
||||
if (!Int32.TryParse(Args[0], out int cNum))
|
||||
cNum = -1;
|
||||
|
||||
if (Args[0][0] == '@') // user specifying target by database ID
|
||||
{
|
||||
@ -360,27 +375,48 @@ namespace IW4MAdmin
|
||||
|
||||
public override async Task ExecuteEvent(GameEvent E)
|
||||
{
|
||||
if (Throttled)
|
||||
return;
|
||||
|
||||
bool canExecuteCommand = true;
|
||||
await ProcessEvent(E);
|
||||
Manager.GetEventApi().OnServerEvent(this, E);
|
||||
|
||||
foreach (IPlugin P in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
|
||||
{
|
||||
#if !DEBUG
|
||||
try
|
||||
#endif
|
||||
{
|
||||
if (cts.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
await P.OnEventAsync(E, this);
|
||||
Command C = null;
|
||||
if (E.Type == GameEvent.EventType.Command)
|
||||
{
|
||||
try
|
||||
{
|
||||
C = await ValidateCommand(E);
|
||||
}
|
||||
#if !DEBUG
|
||||
|
||||
catch (CommandException e)
|
||||
{
|
||||
Logger.WriteInfo(e.Message);
|
||||
}
|
||||
|
||||
if (C != null)
|
||||
{
|
||||
E.Extra = C;
|
||||
}
|
||||
}
|
||||
|
||||
// this allows us to catch exceptions but still run it parallel
|
||||
async Task pluginHandlingAsync(Task onEvent, string pluginName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await onEvent;
|
||||
}
|
||||
|
||||
// this happens if a plugin (login) wants to stop commands from executing
|
||||
catch (AuthorizationException e)
|
||||
{
|
||||
await E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
|
||||
canExecuteCommand = false;
|
||||
}
|
||||
|
||||
catch (Exception Except)
|
||||
{
|
||||
Logger.WriteError(String.Format("The plugin \"{0}\" generated an error. ( see log )", P.Name));
|
||||
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{pluginName}]");
|
||||
Logger.WriteDebug(String.Format("Error Message: {0}", Except.Message));
|
||||
Logger.WriteDebug(String.Format("Error Trace: {0}", Except.StackTrace));
|
||||
while (Except.InnerException != null)
|
||||
@ -388,12 +424,162 @@ namespace IW4MAdmin
|
||||
Except = Except.InnerException;
|
||||
Logger.WriteDebug($"Inner exception: {Except.Message}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var pluginTasks = SharedLibraryCore.Plugins.PluginImporter.ActivePlugins.
|
||||
Select(p => pluginHandlingAsync(p.OnEventAsync(E, this), p.Name));
|
||||
|
||||
// execute all the plugin updates simultaneously
|
||||
await Task.WhenAll(pluginTasks);
|
||||
|
||||
// hack: this prevents commands from getting executing that 'shouldn't' be
|
||||
if (E.Type == GameEvent.EventType.Command &&
|
||||
E.Extra != null &&
|
||||
(canExecuteCommand ||
|
||||
E.Origin?.Level == Player.Permission.Console))
|
||||
{
|
||||
await (((Command)E.Extra).ExecuteAsync(E));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server specific tasks when an event occurs
|
||||
/// </summary>
|
||||
/// <param name="E"></param>
|
||||
/// <returns></returns>
|
||||
override protected async Task ProcessEvent(GameEvent E)
|
||||
{
|
||||
if (E.Type == GameEvent.EventType.Connect)
|
||||
{
|
||||
// this may be a fix for a hard to reproduce null exception error
|
||||
lock (ChatHistory)
|
||||
{
|
||||
ChatHistory.Add(new ChatInfo()
|
||||
{
|
||||
Name = E.Origin?.Name ?? "ERROR!",
|
||||
Message = "CONNECTED",
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
if (E.Origin.Level > Player.Permission.Moderator)
|
||||
await E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.Join)
|
||||
{
|
||||
// special case for IW5 when connect is from the log
|
||||
if (E.Extra != null && GameName == Game.IW5)
|
||||
{
|
||||
var logClient = (Player)E.Extra;
|
||||
var client = (await this.GetStatusAsync())
|
||||
.Single(c => c.ClientNumber == logClient.ClientNumber &&
|
||||
c.Name == logClient.Name);
|
||||
client.NetworkId = logClient.NetworkId;
|
||||
|
||||
await AddPlayer(client);
|
||||
}
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.Disconnect)
|
||||
{
|
||||
// this may be a fix for a hard to reproduce null exception error
|
||||
lock (ChatHistory)
|
||||
{
|
||||
ChatHistory.Add(new ChatInfo()
|
||||
{
|
||||
Name = E.Origin.Name,
|
||||
Message = "DISCONNECTED",
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.Say)
|
||||
{
|
||||
E.Data = E.Data.StripColors();
|
||||
|
||||
if (E.Data.Length > 0)
|
||||
{
|
||||
// this may be a fix for a hard to reproduce null exception error
|
||||
lock (ChatHistory)
|
||||
{
|
||||
ChatHistory.Add(new ChatInfo()
|
||||
{
|
||||
Name = E.Origin.Name,
|
||||
Message = E.Data ?? "NULL",
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.MapChange)
|
||||
{
|
||||
Logger.WriteInfo($"New map loaded - {ClientNum} active players");
|
||||
|
||||
// iw4 doesn't log the game info
|
||||
if (E.Extra == null)
|
||||
{
|
||||
var dict = await this.GetInfoAsync();
|
||||
|
||||
if (dict == null)
|
||||
{
|
||||
Logger.WriteWarning("Map change event response doesn't have any data");
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
Gametype = dict["gametype"].StripColors();
|
||||
Hostname = dict["hostname"]?.StripColors();
|
||||
|
||||
string mapname = dict["mapname"]?.StripColors() ?? CurrentMap.Name;
|
||||
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname };
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
var dict = (Dictionary<string, string>)E.Extra;
|
||||
Gametype = dict["g_gametype"].StripColors();
|
||||
Hostname = dict["sv_hostname"].StripColors();
|
||||
|
||||
string mapname = dict["mapname"].StripColors();
|
||||
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map()
|
||||
{
|
||||
Alias = mapname,
|
||||
Name = mapname
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.MapEnd)
|
||||
{
|
||||
Logger.WriteInfo("Game ending...");
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.Tell)
|
||||
{
|
||||
await Tell(E.Message, E.Target);
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.Broadcast)
|
||||
{
|
||||
// this is a little ugly but I don't want to change the abstract class
|
||||
await E.Owner.ExecuteCommandAsync(E.Message);
|
||||
}
|
||||
|
||||
while (ChatHistory.Count > Math.Ceiling((double)ClientNum / 2))
|
||||
ChatHistory.RemoveAt(0);
|
||||
|
||||
// the last client hasn't fully disconnected yet
|
||||
// so there will still be at least 1 client left
|
||||
if (ClientNum < 2)
|
||||
ChatHistory.Clear();
|
||||
}
|
||||
|
||||
|
||||
async Task<int> PollPlayersAsync()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
@ -414,44 +600,52 @@ namespace IW4MAdmin
|
||||
Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms");
|
||||
#endif
|
||||
Throttled = false;
|
||||
for (int i = 0; i < Players.Count; i++)
|
||||
|
||||
var clients = GetPlayersAsList();
|
||||
foreach (var client in clients)
|
||||
{
|
||||
if (CurrentPlayers.Find(p => p.ClientNumber == i) == null && Players[i] != null)
|
||||
await RemovePlayer(i);
|
||||
if (GameName == Game.IW5)
|
||||
{
|
||||
if (!CurrentPlayers.Select(c => c.ClientNumber).Contains(client.ClientNumber))
|
||||
await RemovePlayer(client.ClientNumber);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (!CurrentPlayers.Select(c => c.NetworkId).Contains(client.NetworkId))
|
||||
await RemovePlayer(client.ClientNumber);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < CurrentPlayers.Count; i++)
|
||||
{
|
||||
await AddPlayer(CurrentPlayers[i]);
|
||||
// todo: wait til GUID is included in status to fix this
|
||||
if (GameName != Game.IW5)
|
||||
await AddPlayer(CurrentPlayers[i]);
|
||||
}
|
||||
|
||||
return CurrentPlayers.Count;
|
||||
}
|
||||
|
||||
long l_size = -1;
|
||||
String[] lines = new String[8];
|
||||
String[] oldLines = new String[8];
|
||||
DateTime start = DateTime.Now;
|
||||
DateTime playerCountStart = DateTime.Now;
|
||||
DateTime lastCount = DateTime.Now;
|
||||
DateTime tickTime = DateTime.Now;
|
||||
bool firstRun = true;
|
||||
int count = 0;
|
||||
|
||||
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
||||
{
|
||||
this.cts = cts;
|
||||
//#if DEBUG == false
|
||||
try
|
||||
//#endif
|
||||
{
|
||||
// first start
|
||||
if (firstRun)
|
||||
if (Manager.ShutdownRequested())
|
||||
{
|
||||
await ExecuteEvent(new GameEvent(GameEvent.EventType.Start, "Server started", null, null, this));
|
||||
firstRun = false;
|
||||
for (int i = 0; i < Players.Count; i++)
|
||||
await RemovePlayer(i);
|
||||
|
||||
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
|
||||
await plugin.OnUnloadAsync();
|
||||
}
|
||||
|
||||
// only check every 2 minutes if the server doesn't seem to be responding
|
||||
if ((DateTime.Now - LastPoll).TotalMinutes < 2 && ConnectionErrors >= 1)
|
||||
return true;
|
||||
|
||||
@ -461,7 +655,7 @@ namespace IW4MAdmin
|
||||
|
||||
if (ConnectionErrors > 0)
|
||||
{
|
||||
Logger.WriteVerbose($"Connection has been reestablished with {IP}:{Port}");
|
||||
Logger.WriteVerbose($"{loc["MANAGER_CONNECTION_REST"]} {IP}:{Port}");
|
||||
Throttled = false;
|
||||
}
|
||||
ConnectionErrors = 0;
|
||||
@ -473,7 +667,7 @@ namespace IW4MAdmin
|
||||
ConnectionErrors++;
|
||||
if (ConnectionErrors == 1)
|
||||
{
|
||||
Logger.WriteError($"{e.Message} {IP}:{Port}, reducing polling rate");
|
||||
Logger.WriteError($"{e.Message} {IP}:{Port}, {loc["SERVER_ERROR_POLLING"]}");
|
||||
Logger.WriteDebug($"Internal Exception: {e.Data["internal_exception"]}");
|
||||
Throttled = true;
|
||||
}
|
||||
@ -483,6 +677,8 @@ namespace IW4MAdmin
|
||||
LastMessage = DateTime.Now - start;
|
||||
lastCount = DateTime.Now;
|
||||
|
||||
// todo: re-enable on tick
|
||||
/*
|
||||
if ((DateTime.Now - tickTime).TotalMilliseconds >= 1000)
|
||||
{
|
||||
foreach (var Plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
|
||||
@ -493,8 +689,9 @@ namespace IW4MAdmin
|
||||
await Plugin.OnTickAsync(this);
|
||||
}
|
||||
tickTime = DateTime.Now;
|
||||
}
|
||||
}*/
|
||||
|
||||
// update the player history
|
||||
if ((lastCount - playerCountStart).TotalMinutes >= SharedLibraryCore.Helpers.PlayerHistory.UpdateInterval)
|
||||
{
|
||||
while (PlayerHistory.Count > ((60 / SharedLibraryCore.Helpers.PlayerHistory.UpdateInterval) * 12)) // 12 times a hour for 12 hours
|
||||
@ -503,118 +700,94 @@ namespace IW4MAdmin
|
||||
playerCountStart = DateTime.Now;
|
||||
}
|
||||
|
||||
// send out broadcast messages
|
||||
if (LastMessage.TotalSeconds > Manager.GetApplicationSettings().Configuration().AutoMessagePeriod
|
||||
&& BroadcastMessages.Count > 0
|
||||
&& ClientNum > 0)
|
||||
{
|
||||
await Broadcast(Utilities.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage]));
|
||||
string[] messages = this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage]).Split(Environment.NewLine);
|
||||
|
||||
foreach (string message in messages)
|
||||
await Broadcast(message);
|
||||
|
||||
NextMessage = NextMessage == (BroadcastMessages.Count - 1) ? 0 : NextMessage + 1;
|
||||
start = DateTime.Now;
|
||||
}
|
||||
|
||||
if (LogFile == null)
|
||||
return true;
|
||||
|
||||
if (l_size != LogFile.Length())
|
||||
{
|
||||
lines = l_size != -1 ? await LogFile.Tail(12) : lines;
|
||||
if (lines != oldLines)
|
||||
{
|
||||
l_size = LogFile.Length();
|
||||
int end = (lines.Length == oldLines.Length) ? lines.Length - 1 : Math.Abs((lines.Length - oldLines.Length)) - 1;
|
||||
|
||||
for (count = 0; count < lines.Length; count++)
|
||||
{
|
||||
if (lines.Length < 1 && oldLines.Length < 1)
|
||||
continue;
|
||||
|
||||
if (lines[count] == oldLines[oldLines.Length - 1])
|
||||
continue;
|
||||
|
||||
if (lines[count].Length < 10) // it's not a needed line
|
||||
continue;
|
||||
|
||||
else
|
||||
{
|
||||
GameEvent event_ = EventParser.GetEvent(this, lines[count]);
|
||||
if (event_ != null)
|
||||
{
|
||||
if (event_.Origin == null)
|
||||
continue;
|
||||
|
||||
await ExecuteEvent(event_);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
oldLines = lines;
|
||||
l_size = LogFile.Length();
|
||||
if (Manager.ShutdownRequested())
|
||||
{
|
||||
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
|
||||
await plugin.OnUnloadAsync();
|
||||
|
||||
for (int i = 0; i < Players.Count; i++)
|
||||
await RemovePlayer(i);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
//#if !DEBUG
|
||||
catch (NetworkException)
|
||||
{
|
||||
Logger.WriteError($"Could not communicate with {IP}:{Port}");
|
||||
return false;
|
||||
}
|
||||
|
||||
catch (InvalidOperationException)
|
||||
// this one is ok
|
||||
catch (ServerException e)
|
||||
{
|
||||
Logger.WriteWarning("Event could not parsed properly");
|
||||
Logger.WriteDebug($"Log Line: {lines[count]}");
|
||||
if (e is NetworkException)
|
||||
{
|
||||
Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
catch (Exception E)
|
||||
{
|
||||
Logger.WriteError($"Encountered error on {IP}:{Port}");
|
||||
Logger.WriteError($"{loc["SERVER_ERROR_EXCEPTION"]} {IP}:{Port}");
|
||||
Logger.WriteDebug("Error Message: " + E.Message);
|
||||
Logger.WriteDebug("Error Trace: " + E.StackTrace);
|
||||
return false;
|
||||
}
|
||||
//#endif
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
{
|
||||
RconParser = ServerConfig.UseT6MParser ? (IRConParser)new T6MRConParser() : new IW4RConParser();
|
||||
RconParser = ServerConfig.UseT6MParser ? (IRConParser)new T6MRConParser() : new IW3RConParser();
|
||||
if (ServerConfig.UseIW5MParser)
|
||||
RconParser = new IW5MRConParser();
|
||||
|
||||
var version = await this.GetDvarAsync<string>("version");
|
||||
GameName = Utilities.GetGame(version.Value);
|
||||
|
||||
if (GameName == Game.IW4)
|
||||
{
|
||||
EventParser = new IW4EventParser();
|
||||
RconParser = new IW4RConParser();
|
||||
}
|
||||
else if (GameName == Game.IW5)
|
||||
EventParser = new IW5EventParser();
|
||||
else if (GameName == Game.T5M)
|
||||
EventParser = new T5MEventParser();
|
||||
else if (GameName == Game.T6M)
|
||||
EventParser = new T6MEventParser();
|
||||
else if (GameName == Game.UKN)
|
||||
Logger.WriteWarning($"Game name not recognized: {version}");
|
||||
else
|
||||
EventParser = new IW4EventParser();
|
||||
EventParser = new IW3EventParser(); // this uses the 'main' folder for log paths
|
||||
|
||||
var shortversion = await this.GetDvarAsync<string>("shortversion");
|
||||
var hostname = await this.GetDvarAsync<string>("sv_hostname");
|
||||
var mapname = await this.GetDvarAsync<string>("mapname");
|
||||
var maxplayers = (GameName == Game.IW4) ? // gotta love IW4 idiosyncrasies
|
||||
await this.GetDvarAsync<int>("party_maxplayers") :
|
||||
await this.GetDvarAsync<int>("sv_maxclients");
|
||||
var gametype = await this.GetDvarAsync<string>("g_gametype");
|
||||
if (GameName == Game.UKN)
|
||||
Logger.WriteWarning($"Game name not recognized: {version}");
|
||||
|
||||
var infoResponse = await this.GetInfoAsync();
|
||||
// this is normally slow, but I'm only doing it because different games have different prefixes
|
||||
var hostname = infoResponse == null ?
|
||||
(await this.GetDvarAsync<string>("sv_hostname")).Value :
|
||||
infoResponse.Where(kvp => kvp.Key.Contains("hostname")).Select(kvp => kvp.Value).First();
|
||||
var mapname = infoResponse == null ?
|
||||
(await this.GetDvarAsync<string>("mapname")).Value :
|
||||
infoResponse["mapname"];
|
||||
int maxplayers = (GameName == Game.IW4) ? // gotta love IW4 idiosyncrasies
|
||||
(await this.GetDvarAsync<int>("party_maxplayers")).Value :
|
||||
infoResponse == null ?
|
||||
(await this.GetDvarAsync<int>("sv_maxclients")).Value :
|
||||
Convert.ToInt32(infoResponse["sv_maxclients"]);
|
||||
var gametype = infoResponse == null ?
|
||||
(await this.GetDvarAsync<string>("g_gametype")).Value :
|
||||
infoResponse.Where(kvp => kvp.Key.Contains("gametype")).Select(kvp => kvp.Value).First();
|
||||
var basepath = await this.GetDvarAsync<string>("fs_basepath");
|
||||
WorkingDirectory = basepath.Value;
|
||||
var game = await this.GetDvarAsync<string>("fs_game");
|
||||
var game = infoResponse == null || !infoResponse.ContainsKey("fs_game") ?
|
||||
(await this.GetDvarAsync<string>("fs_game")).Value :
|
||||
infoResponse["fs_game"];
|
||||
var logfile = await this.GetDvarAsync<string>("g_log");
|
||||
var logsync = await this.GetDvarAsync<int>("g_logsync");
|
||||
|
||||
WorkingDirectory = basepath.Value;
|
||||
|
||||
try
|
||||
{
|
||||
var website = await this.GetDvarAsync<string>("_website");
|
||||
@ -628,13 +801,13 @@ namespace IW4MAdmin
|
||||
|
||||
InitializeMaps();
|
||||
|
||||
this.Hostname = hostname.Value.StripColors();
|
||||
this.CurrentMap = Maps.Find(m => m.Name == mapname.Value) ?? new Map() { Alias = mapname.Value, Name = mapname.Value };
|
||||
this.MaxClients = maxplayers.Value;
|
||||
this.FSGame = game.Value;
|
||||
this.Gametype = (await this.GetDvarAsync<string>("g_gametype")).Value;
|
||||
this.Hostname = hostname.StripColors();
|
||||
this.CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname };
|
||||
this.MaxClients = maxplayers;
|
||||
this.FSGame = game;
|
||||
this.Gametype = gametype;
|
||||
|
||||
await this.SetDvarAsync("sv_kickbantime", 60);
|
||||
//wait this.SetDvarAsync("sv_kickbantime", 60);
|
||||
|
||||
if (logsync.Value == 0 || logfile.Value == string.Empty)
|
||||
{
|
||||
@ -650,11 +823,19 @@ namespace IW4MAdmin
|
||||
CustomCallback = await ScriptLoaded();
|
||||
string mainPath = EventParser.GetGameDir();
|
||||
#if DEBUG
|
||||
basepath.Value = @"\\192.168.88.253\Call of Duty Black Ops II";
|
||||
basepath.Value = @"\\192.168.88.253\mw2";
|
||||
#endif
|
||||
string logPath = game.Value == string.Empty ?
|
||||
$"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{mainPath}{Path.DirectorySeparatorChar}{logfile.Value}" :
|
||||
$"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{game.Value.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{logfile.Value}";
|
||||
string logPath;
|
||||
if (GameName == Game.IW5)
|
||||
{
|
||||
logPath = ServerConfig.ManualLogPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
logPath = game == string.Empty ?
|
||||
$"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{mainPath}{Path.DirectorySeparatorChar}{logfile.Value}" :
|
||||
$"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{game.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{logfile.Value}";
|
||||
}
|
||||
|
||||
// hopefully fix wine drive name mangling
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
@ -664,155 +845,22 @@ namespace IW4MAdmin
|
||||
|
||||
if (!File.Exists(logPath))
|
||||
{
|
||||
Logger.WriteError($"Gamelog {logPath} does not exist!");
|
||||
Logger.WriteError($"{logPath} {loc["SERVER_ERROR_DNE"]}");
|
||||
#if !DEBUG
|
||||
throw new ServerException($"Invalid gamelog file {logPath}");
|
||||
throw new ServerException($"{loc["SERVER_ERROR_LOG"]} {logPath}");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
LogFile = new IFile(logPath);
|
||||
LogEvent = new GameLogEvent(this, logPath, logfile.Value);
|
||||
}
|
||||
|
||||
Logger.WriteInfo($"Log file is {logPath}");
|
||||
#if DEBUG
|
||||
// LogFile = new RemoteFile("https://raidmax.org/IW4MAdmin/getlog.php");
|
||||
#else
|
||||
#if !DEBUG
|
||||
await Broadcast(loc["BROADCAST_ONLINE"]);
|
||||
#endif
|
||||
}
|
||||
|
||||
//Process any server event
|
||||
override protected async Task ProcessEvent(GameEvent E)
|
||||
{
|
||||
if (E.Type == GameEvent.EventType.Connect)
|
||||
{
|
||||
ChatHistory.Add(new ChatInfo()
|
||||
{
|
||||
Name = E.Origin.Name,
|
||||
Message = "CONNECTED",
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
|
||||
if (E.Origin.Level > Player.Permission.Moderator)
|
||||
await E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.Disconnect)
|
||||
{
|
||||
ChatHistory.Add(new ChatInfo()
|
||||
{
|
||||
Name = E.Origin.Name,
|
||||
Message = "DISCONNECTED",
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.Script)
|
||||
{
|
||||
await ExecuteEvent(new GameEvent(GameEvent.EventType.Kill, E.Data, E.Origin, E.Target, this));
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.Say && E.Data.Length >= 2)
|
||||
{
|
||||
if (E.Data.Substring(0, 1) == "!" || E.Data.Substring(0, 1) == "@" || E.Origin.Level == Player.Permission.Console)
|
||||
{
|
||||
Command C = null;
|
||||
|
||||
try
|
||||
{
|
||||
C = await ValidateCommand(E);
|
||||
}
|
||||
|
||||
catch (CommandException e)
|
||||
{
|
||||
Logger.WriteInfo(e.Message);
|
||||
}
|
||||
|
||||
if (C != null)
|
||||
{
|
||||
if (C.RequiresTarget && E.Target == null)
|
||||
{
|
||||
Logger.WriteWarning("Requested event (command) requiring target does not have a target!");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!E.Remote && E.Origin.Level != Player.Permission.Console)
|
||||
{
|
||||
await ExecuteEvent(new GameEvent()
|
||||
{
|
||||
Type = GameEvent.EventType.Command,
|
||||
Data = string.Empty,
|
||||
Origin = E.Origin,
|
||||
Target = E.Target,
|
||||
Owner = this,
|
||||
Extra = C,
|
||||
Remote = E.Remote
|
||||
});
|
||||
}
|
||||
|
||||
await C.ExecuteAsync(E);
|
||||
}
|
||||
|
||||
catch (AuthorizationException e)
|
||||
{
|
||||
await E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
|
||||
}
|
||||
|
||||
catch (Exception Except)
|
||||
{
|
||||
Logger.WriteError(String.Format("A command request \"{0}\" generated an error.", C.Name));
|
||||
Logger.WriteDebug(String.Format("Error Message: {0}", Except.Message));
|
||||
Logger.WriteDebug(String.Format("Error Trace: {0}", Except.StackTrace));
|
||||
await E.Origin.Tell("^1An internal error occured while processing your command^7");
|
||||
#if DEBUG
|
||||
await E.Origin.Tell(Except.Message);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else // Not a command
|
||||
{
|
||||
E.Data = E.Data.StripColors();
|
||||
|
||||
ChatHistory.Add(new ChatInfo()
|
||||
{
|
||||
Name = E.Origin.Name,
|
||||
Message = E.Data,
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.MapChange)
|
||||
{
|
||||
Logger.WriteInfo($"New map loaded - {ClientNum} active players");
|
||||
|
||||
Gametype = (await this.GetDvarAsync<string>("g_gametype")).Value.StripColors();
|
||||
Hostname = (await this.GetDvarAsync<string>("sv_hostname")).Value.StripColors();
|
||||
FSGame = (await this.GetDvarAsync<string>("fs_game")).Value.StripColors();
|
||||
|
||||
string mapname = this.GetDvarAsync<string>("mapname").Result.Value;
|
||||
CurrentMap = Maps.Find(m => m.Name == mapname) ?? new Map() { Alias = mapname, Name = mapname };
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.MapEnd)
|
||||
{
|
||||
Logger.WriteInfo("Game ending...");
|
||||
}
|
||||
|
||||
//todo: move
|
||||
while (ChatHistory.Count > Math.Ceiling((double)ClientNum / 2))
|
||||
ChatHistory.RemoveAt(0);
|
||||
|
||||
// the last client hasn't fully disconnected yet
|
||||
// so there will still be at least 1 client left
|
||||
if (ClientNum < 2)
|
||||
ChatHistory.Clear();
|
||||
}
|
||||
|
||||
public override async Task Warn(String Reason, Player Target, Player Origin)
|
||||
{
|
||||
// ensure player gets warned if command not performed on them in game
|
||||
@ -980,6 +1028,8 @@ namespace IW4MAdmin
|
||||
};
|
||||
|
||||
await Manager.GetPenaltyService().Create(newPenalty);
|
||||
// prevent them from logging in again
|
||||
Manager.GetPrivilegedClients().Remove(Target.ClientId);
|
||||
}
|
||||
|
||||
override public async Task Unban(string reason, Player Target, Player Origin)
|
||||
@ -996,14 +1046,14 @@ namespace IW4MAdmin
|
||||
Link = Target.AliasLink
|
||||
};
|
||||
|
||||
await Manager.GetPenaltyService().Create(unbanPenalty);
|
||||
await Manager.GetPenaltyService().RemoveActivePenalties(Target.AliasLink.AliasLinkId);
|
||||
await Manager.GetPenaltyService().Create(unbanPenalty);
|
||||
}
|
||||
|
||||
override public void InitializeTokens()
|
||||
{
|
||||
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("TOTALPLAYERS", Manager.GetClientService().GetTotalClientsAsync().Result.ToString));
|
||||
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("VERSION", Application.Program.Version.ToString));
|
||||
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("TOTALPLAYERS", (Server s) => Manager.GetClientService().GetTotalClientsAsync().Result.ToString()));
|
||||
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("VERSION", (Server s) => Application.Program.Version.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
_commands.gsc = _commands.gsc
|
||||
_customcallbacks.gsc = _customcallbacks.gsc
|
||||
README.md = README.md
|
||||
version.txt = version.txt
|
||||
@ -28,7 +29,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Login", "Plugins\Login\Logi
|
||||
EndProject
|
||||
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Master", "Master\Master.pyproj", "{F5051A32-6BD0-4128-ABBA-C202EE15FC5C}"
|
||||
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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -260,6 +263,30 @@ Global
|
||||
{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.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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -270,6 +297,7 @@ Global
|
||||
{958FF7EC-0226-4E85-A85B-B84EC768197D} = {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}
|
||||
{6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}
|
||||
|
@ -7,7 +7,7 @@
|
||||
<ProjectGuid>f5051a32-6bd0-4128-abba-c202ee15fc5c</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>
|
||||
<StartupFile>master\runserver.py</StartupFile>
|
||||
<SearchPath>
|
||||
</SearchPath>
|
||||
<WorkingDirectory>.</WorkingDirectory>
|
||||
@ -18,6 +18,13 @@
|
||||
<Name>Master</Name>
|
||||
<RootNamespace>Master</RootNamespace>
|
||||
<InterpreterId>MSBuild|dev_env|$(MSBuildProjectFullPath)</InterpreterId>
|
||||
<IsWindowsApplication>False</IsWindowsApplication>
|
||||
<PythonRunWebServerCommand>
|
||||
</PythonRunWebServerCommand>
|
||||
<PythonDebugWebServerCommand>
|
||||
</PythonDebugWebServerCommand>
|
||||
<PythonRunWebServerCommandType>script</PythonRunWebServerCommandType>
|
||||
<PythonDebugWebServerCommandType>script</PythonDebugWebServerCommandType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
@ -51,27 +58,33 @@
|
||||
<Compile Include="master\models\__init__.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Master\resources\authenticate.py">
|
||||
<Compile Include="master\resources\authenticate.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="master\resources\history_graph.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Master\resources\instance.py">
|
||||
<Compile Include="master\resources\instance.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Master\resources\null.py">
|
||||
<Compile Include="master\resources\localization.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Master\resources\version.py">
|
||||
<Compile Include="master\resources\null.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Master\resources\__init__.py">
|
||||
<Compile Include="master\resources\version.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="master\resources\__init__.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="master\routes.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="master\runserver.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="master\schema\instanceschema.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
@ -81,27 +94,25 @@
|
||||
<Compile Include="master\schema\__init__.py">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="runserver.py" />
|
||||
<Compile Include="master\__init__.py" />
|
||||
<Compile Include="master\views.py" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="C:\Projects\IW4M-Admin\Master\master\" />
|
||||
<Folder Include="master\" />
|
||||
<Folder Include="master\context\" />
|
||||
<Folder Include="master\models\" />
|
||||
<Folder Include="master\config\" />
|
||||
<Folder Include="master\schema\" />
|
||||
<Folder Include="Master\resources\" />
|
||||
<Folder Include="Master\static\" />
|
||||
<Folder Include="Master\templates\" />
|
||||
<Folder Include="master\static\" />
|
||||
<Folder Include="master\templates\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="FolderProfile.pubxml" />
|
||||
<Content Include="master\config\master.json" />
|
||||
<Content Include="requirements.txt" />
|
||||
<Content Include="Master\templates\index.html" />
|
||||
<Content Include="Master\templates\layout.html" />
|
||||
<Content Include="master\templates\index.html" />
|
||||
<Content Include="master\templates\layout.html" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Interpreter Include="dev_env\">
|
||||
|
@ -6,7 +6,6 @@ from flask import Flask
|
||||
from flask_restful import Resource, Api
|
||||
from flask_jwt_extended import JWTManager
|
||||
from master.context.base import Base
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['JWT_SECRET_KEY'] = 'my key!'
|
||||
@ -14,7 +13,7 @@ app.config['PROPAGATE_EXCEPTIONS'] = True
|
||||
jwt = JWTManager(app)
|
||||
api = Api(app)
|
||||
ctx = Base()
|
||||
config = json.load(open('./master/config/master.json'))
|
||||
#config = json.load(open('./master/config/master.json'))
|
||||
|
||||
import master.routes
|
||||
import master.views
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"current-version-stable": 2.0,
|
||||
"current-version-prerelease": 2.0
|
||||
"current-version-stable": 2.1,
|
||||
"current-version-prerelease": 2.1
|
||||
}
|
@ -37,6 +37,7 @@ class Base():
|
||||
client_num += server.clientnum
|
||||
self.history.add_client_history(client_num)
|
||||
self.history.add_instance_history(len(self.instance_list))
|
||||
self.history.add_server_history(len(servers))
|
||||
|
||||
def _remove_staleinstances(self):
|
||||
for key, value in list(self.instance_list.items()):
|
||||
|
@ -5,17 +5,26 @@ class History():
|
||||
def __init__(self):
|
||||
self.client_history = list()
|
||||
self.instance_history = list()
|
||||
self.server_history = list()
|
||||
|
||||
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.append({
|
||||
'count' : client_num,
|
||||
'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):
|
||||
if len(self.instance_history) > 1440:
|
||||
if len(self.instance_history) > 2880:
|
||||
self.instance_history = self.instance_history[1:]
|
||||
self.instance_history.append({
|
||||
'count' : instance_num,
|
||||
|
@ -11,22 +11,18 @@ class HistoryGraph(Resource):
|
||||
custom_style = Style(
|
||||
background='transparent',
|
||||
plot_background='transparent',
|
||||
foreground='rgba(109, 118, 126, 0.3)',
|
||||
foreground_strong='rgba(109, 118, 126, 0.3)',
|
||||
foreground_subtle='rgba(109, 118, 126, 0.3)',
|
||||
foreground='#6c757d',
|
||||
foreground_strong='#6c757d',
|
||||
foreground_subtle='#6c757d',
|
||||
opacity='0.1',
|
||||
opacity_hover='0.2',
|
||||
transition='100ms ease-in',
|
||||
colors=('#007acc', '#749363')
|
||||
transition='0ms',
|
||||
colors=('#749363','#007acc'),
|
||||
)
|
||||
|
||||
graph = pygal.StackedLine(
|
||||
interpolate='cubic',
|
||||
interpolation_precision=3,
|
||||
#x_labels_major_every=100,
|
||||
#x_labels_major_count=500,
|
||||
graph = pygal.Line(
|
||||
stroke_style={'width': 0.4},
|
||||
show_dots=False,
|
||||
#show_dots=False,
|
||||
show_legend=False,
|
||||
fill=True,
|
||||
style=custom_style,
|
||||
@ -37,10 +33,17 @@ class HistoryGraph(Resource):
|
||||
if len(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:])
|
||||
graph.add('Client Count', [history['count'] for history in ctx.history.client_history][-history_count:])
|
||||
return { 'message' : graph.render(),
|
||||
'data_points' : len(instance_count)
|
||||
instance_counts = [history['count'] for history in ctx.history.instance_history][-history_count:]
|
||||
client_counts = [history['count'] for history in ctx.history.client_history][-history_count:]
|
||||
server_counts = [history['count'] for history in ctx.history.server_history][-history_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
|
||||
except Exception as e:
|
||||
return { 'message' : str(e) }, 500
|
||||
|
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
|
@ -1,8 +1,9 @@
|
||||
from flask_restful import Resource
|
||||
from master import config
|
||||
import json
|
||||
|
||||
class Version(Resource):
|
||||
def get(self):
|
||||
config = json.load(open('./master/config/master.json'))
|
||||
return {
|
||||
'current-version-stable' : config['current-version-stable'],
|
||||
'current-version-prerelease' : config['current-version-prerelease']
|
||||
|
@ -5,9 +5,11 @@ from master.resources.instance import Instance
|
||||
from master.resources.authenticate import Authenticate
|
||||
from master.resources.version import Version
|
||||
from master.resources.history_graph import HistoryGraph
|
||||
from master.resources.localization import Localization
|
||||
|
||||
api.add_resource(Null, '/null')
|
||||
api.add_resource(Instance, '/instance/', '/instance/<string:id>')
|
||||
api.add_resource(Version, '/version')
|
||||
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>')
|
@ -6,4 +6,4 @@ from os import environ
|
||||
from master import app
|
||||
|
||||
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)
|
@ -5,43 +5,60 @@
|
||||
<div class="col-12">
|
||||
<figure>
|
||||
<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_in" class="h4 oi oi-zoom-in text-muted" style="cursor:pointer;"></span>
|
||||
</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>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-12">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script type="text/javascript" src="http://kozea.github.com/pygal.js/latest/pygal-tooltips.min.js"></script>
|
||||
<script>
|
||||
let dataPoints = {{data_points}};
|
||||
let zoomLevel = Math.ceil(dataPoints / 2);
|
||||
//console.log(dataPoints);
|
||||
<script type="text/javascript" src="http://kozea.github.com/pygal.js/latest/pygal-tooltips.min.js"></script>
|
||||
<script>
|
||||
let dataPoints = {{data_points}};
|
||||
let maxPoints = 2880;
|
||||
maxPoints = Math.min(maxPoints, dataPoints);
|
||||
let zoomLevel = Math.floor(maxPoints);
|
||||
let performingZoom = false;
|
||||
|
||||
function updateHistoryGraph() {
|
||||
$.get('/history/' + zoomLevel)
|
||||
.done(function (content) {
|
||||
$('#history_graph').html(content.message);
|
||||
dataPoints = content.data_points
|
||||
});
|
||||
function updateHistoryGraph() {
|
||||
perfomingZoom = true;
|
||||
$.get('/history/' + zoomLevel)
|
||||
.done(function (content) {
|
||||
$('#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 () {
|
||||
// console.log(zoomLevel);
|
||||
zoomLevel = zoomLevel * 2 <= 1440 ? Math.ceil(zoomLevel * 2) : dataPoints;
|
||||
updateHistoryGraph();
|
||||
});
|
||||
$('#history_graph_zoom_in').click(function () {
|
||||
if (performingZoom === true) {
|
||||
return false;
|
||||
}
|
||||
zoomLevel = zoomLevel / 2 > 2 ? Math.ceil(zoomLevel / 2) : 2;
|
||||
updateHistoryGraph();
|
||||
});
|
||||
|
||||
$('#history_graph_zoom_in').click(function () {
|
||||
// console.log(zoomLevel);
|
||||
zoomLevel = zoomLevel / 2 > 2 ? Math.ceil(zoomLevel / 2) : 2;
|
||||
updateHistoryGraph();
|
||||
});
|
||||
|
||||
</script>
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -14,6 +14,18 @@
|
||||
.oi:hover {
|
||||
color: #fff !important;
|
||||
}
|
||||
.dot {
|
||||
opacity: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dot:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltip-box {
|
||||
fill: #343a40 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
@ -9,10 +9,13 @@ from master.resources.history_graph import HistoryGraph
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
_history_graph = HistoryGraph().get(500)
|
||||
_history_graph = HistoryGraph().get(2880)
|
||||
return render_template(
|
||||
'index.html',
|
||||
title='API Overview',
|
||||
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']
|
||||
)
|
||||
|
19
Plugins/IW4ScriptCommands/Commands/Balance.cs
Normal file
19
Plugins/IW4ScriptCommands/Commands/Balance.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Objects;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4ScriptCommands.Commands
|
||||
{
|
||||
class Balance : Command
|
||||
{
|
||||
public Balance() : base("balance", "balance teams", "bal", Player.Permission.Trusted, false, null)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
await E.Owner.ExecuteCommandAsync("sv_iw4madmin_command balance");
|
||||
await E.Origin.Tell("Balance command sent");
|
||||
}
|
||||
}
|
||||
}
|
22
Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj
Normal file
22
Plugins/IW4ScriptCommands/IW4ScriptCommands.csproj
Normal file
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<ApplicationIcon />
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
26
Plugins/IW4ScriptCommands/Plugin.cs
Normal file
26
Plugins/IW4ScriptCommands/Plugin.cs
Normal file
@ -0,0 +1,26 @@
|
||||
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) => Task.CompletedTask;
|
||||
|
||||
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;
|
||||
|
||||
public Task OnTickAsync(Server S) => Task.CompletedTask;
|
||||
|
||||
public Task OnUnloadAsync() => Task.CompletedTask;
|
||||
}
|
||||
}
|
@ -8,11 +8,11 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
||||
{
|
||||
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", Player.Permission.Trusted, false, new CommandArgument[]
|
||||
{
|
||||
new CommandArgument()
|
||||
{
|
||||
Name = "password",
|
||||
Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
|
||||
Required = true
|
||||
}
|
||||
}){ }
|
||||
@ -25,12 +25,12 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
||||
if (hashedPassword[0] == client.Password)
|
||||
{
|
||||
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
|
||||
await E.Origin.Tell("You are now logged in");
|
||||
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
await E.Origin.Tell("Your password is incorrect");
|
||||
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,10 @@
|
||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||
</Target>
|
||||
|
@ -5,6 +5,7 @@ using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Objects;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Login
|
||||
{
|
||||
@ -36,18 +37,21 @@ namespace IW4MAdmin.Plugins.Login
|
||||
|
||||
if (E.Type == GameEvent.EventType.Command)
|
||||
{
|
||||
if (E.Origin.Level < SharedLibraryCore.Objects.Player.Permission.Moderator)
|
||||
if (E.Origin.Level < Player.Permission.Moderator ||
|
||||
E.Origin.Level == Player.Permission.Console)
|
||||
return Task.CompletedTask;
|
||||
|
||||
E.Owner.Manager.GetPrivilegedClients().TryGetValue(E.Origin.ClientId, out Player client);
|
||||
|
||||
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;
|
||||
|
||||
if (((Command)E.Extra).Name == new Commands.CLogin().Name)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (!AuthorizedClients[E.Origin.ClientId])
|
||||
throw new AuthorizationException("not logged in");
|
||||
throw new AuthorizationException(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_AUTH"]);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
@ -67,12 +71,8 @@ namespace IW4MAdmin.Plugins.Login
|
||||
Config = cfg.Configuration();
|
||||
}
|
||||
|
||||
public Task OnTickAsync(Server S) => Utilities.CompletedTask;
|
||||
public Task OnTickAsync(Server S) => Task.CompletedTask;
|
||||
|
||||
public Task OnUnloadAsync()
|
||||
{
|
||||
AuthorizedClients.Clear();
|
||||
return Utilities.CompletedTask;
|
||||
}
|
||||
public Task OnUnloadAsync() => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
@ -21,9 +21,11 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
||||
"fuck"
|
||||
};
|
||||
|
||||
EnableProfanityDeterment = Utilities.PromptBool("Enable profanity deterring");
|
||||
ProfanityWarningMessage = "Please do not use profanity on this server";
|
||||
ProfanityKickMessage = "Excessive use of profanity";
|
||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
|
||||
EnableProfanityDeterment = Utilities.PromptBool(loc["PLUGINS_PROFANITY_SETUP_ENABLE"]);
|
||||
ProfanityWarningMessage = loc["PLUGINS_PROFANITY_WARNMSG"];
|
||||
ProfanityKickMessage = loc["PLUGINS_PROFANITY_KICKMSG"];
|
||||
KickAfterInfringementCount = 2;
|
||||
|
||||
return this;
|
||||
|
@ -20,7 +20,6 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
||||
BaseConfigurationHandler<Configuration> Settings;
|
||||
ConcurrentDictionary<int, Tracking> ProfanityCounts;
|
||||
IManager Manager;
|
||||
Task CompletedTask = Task.FromResult(false);
|
||||
|
||||
public async Task OnEventAsync(GameEvent E, Server S)
|
||||
{
|
||||
@ -34,6 +33,16 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
||||
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;
|
||||
|
||||
if (containsObjectionalWord)
|
||||
{
|
||||
await E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, new Player()
|
||||
{
|
||||
ClientId = 1
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.Disconnect)
|
||||
@ -87,8 +96,8 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
||||
Manager = manager;
|
||||
}
|
||||
|
||||
public Task OnTickAsync(Server S) => CompletedTask;
|
||||
public Task OnTickAsync(Server S) => Task.CompletedTask;
|
||||
|
||||
public Task OnUnloadAsync() => CompletedTask;
|
||||
public Task OnUnloadAsync() => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,10 @@
|
||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||
</Target>
|
||||
|
@ -1,7 +1,6 @@
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Objects;
|
||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -12,13 +11,24 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
{
|
||||
class Detection
|
||||
{
|
||||
public enum DetectionType
|
||||
{
|
||||
Bone,
|
||||
Chest,
|
||||
Offset,
|
||||
Strain
|
||||
};
|
||||
|
||||
int Kills;
|
||||
int AboveThresholdCount;
|
||||
double AverageKillTime;
|
||||
int HitCount;
|
||||
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
|
||||
ChangeTracking Tracker;
|
||||
double AngleDifferenceAverage;
|
||||
EFClientStatistics ClientStats;
|
||||
DateTime LastKill;
|
||||
DateTime LastHit;
|
||||
long LastOffset;
|
||||
ILogger Log;
|
||||
Strain Strain;
|
||||
|
||||
public Detection(ILogger log, EFClientStatistics clientStats)
|
||||
{
|
||||
@ -26,8 +36,9 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
HitLocationCount = new Dictionary<IW4Info.HitLocation, int>();
|
||||
foreach (var loc in Enum.GetValues(typeof(IW4Info.HitLocation)))
|
||||
HitLocationCount.Add((IW4Info.HitLocation)loc, 0);
|
||||
LastKill = DateTime.UtcNow;
|
||||
ClientStats = clientStats;
|
||||
Strain = new Strain();
|
||||
Tracker = new ChangeTracking();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -35,7 +46,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
/// </summary>
|
||||
/// <param name="kill">kill performed by the player</param>
|
||||
/// <returns>true if detection reached thresholds, false otherwise</returns>
|
||||
public DetectionPenaltyResult ProcessKill(EFClientKill kill)
|
||||
public DetectionPenaltyResult ProcessKill(EFClientKill kill, bool isDamage)
|
||||
{
|
||||
if ((kill.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
|
||||
kill.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
|
||||
@ -44,46 +55,124 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
return new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Any,
|
||||
RatioAmount = 0
|
||||
};
|
||||
|
||||
DetectionPenaltyResult result = null;
|
||||
|
||||
if (LastHit == DateTime.MinValue)
|
||||
LastHit = DateTime.UtcNow;
|
||||
|
||||
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)
|
||||
if (!isDamage)
|
||||
{
|
||||
Kills++;
|
||||
}
|
||||
|
||||
HitCount++;
|
||||
|
||||
#region VIEWANGLES
|
||||
if (kill.AnglesList.Count >= 2)
|
||||
{
|
||||
double realAgainstPredict = Vector3.ViewAngleDistance(kill.AnglesList[0], kill.AnglesList[1], kill.ViewAngles);
|
||||
|
||||
// LIFETIME
|
||||
var hitLoc = ClientStats.HitLocations
|
||||
.First(hl => hl.Location == kill.HitLoc);
|
||||
|
||||
float previousAverage = hitLoc.HitOffsetAverage;
|
||||
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + angle) / hitLoc.HitCount;
|
||||
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + realAgainstPredict) / hitLoc.HitCount;
|
||||
hitLoc.HitOffsetAverage = (float)newAverage;
|
||||
|
||||
if (double.IsNaN(hitLoc.HitOffsetAverage))
|
||||
if (hitLoc.HitOffsetAverage > Thresholds.MaxOffset &&
|
||||
hitLoc.HitCount > 100)
|
||||
{
|
||||
Log.WriteWarning("[Detection::ProcessKill] HitOffsetAvgerage NaN");
|
||||
Log.WriteDebug($"{previousAverage}-{hitLoc.HitCount}-{hitLoc}-{newAverage}");
|
||||
hitLoc.HitOffsetAverage = 0f;
|
||||
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 = {kill.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 > 30)
|
||||
{
|
||||
Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***");
|
||||
Log.WriteDebug($"Session Average = {sessAverage}");
|
||||
Log.WriteDebug($"HitCount = {HitCount}");
|
||||
Log.WriteDebug($"ID = {kill.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, kill.Damage, kill.Distance / 0.0254, kill.ViewAngles, Math.Max(50, kill.TimeOffset - LastOffset));
|
||||
//double currentWeightedStrain = (currentStrain * ClientStats.SPM) / 170.0;
|
||||
LastOffset = kill.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)
|
||||
{
|
||||
result = new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||
Value = currentStrain,
|
||||
HitCount = HitCount,
|
||||
Type = DetectionType.Strain
|
||||
};
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
Log.WriteDebug($"Current Strain: {currentStrain}");
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
#region SESSION_RATIOS
|
||||
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
|
||||
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 maxHeadshotLerpValueForBan = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(3.0), Thresholds.HeadshotRatioThresholdHighSample(3.0), lerpAmount) + marginOfError;
|
||||
// determine what the max bone percentage can be for current number of kills
|
||||
@ -91,10 +180,10 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
double maxBoneRatioLerpValueForBan = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(3.25), Thresholds.BoneRatioThresholdHighSample(3.25), lerpAmount) + marginOfError;
|
||||
|
||||
// calculate headshot ratio
|
||||
double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet]) / (double)Kills);
|
||||
double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head] + HitLocationCount[IW4Info.HitLocation.helmet] + HitLocationCount[IW4Info.HitLocation.neck]) / (double)HitCount);
|
||||
|
||||
// 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;
|
||||
|
||||
#region HEADSHOT_RATIO
|
||||
@ -104,46 +193,44 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
// ban on headshot
|
||||
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
|
||||
{
|
||||
AboveThresholdCount++;
|
||||
Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**");
|
||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
||||
Log.WriteDebug($"**Kills: {Kills}");
|
||||
Log.WriteDebug($"**HitCount: {HitCount}");
|
||||
Log.WriteDebug($"**Ratio {currentHeadshotRatio}");
|
||||
Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}");
|
||||
var sb = new StringBuilder();
|
||||
foreach (var kvp in HitLocationCount)
|
||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
||||
Log.WriteDebug(sb.ToString());
|
||||
Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
||||
|
||||
return new DetectionPenaltyResult()
|
||||
result = new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||
RatioAmount = currentHeadshotRatio,
|
||||
Bone = IW4Info.HitLocation.head,
|
||||
KillCount = Kills
|
||||
Value = currentHeadshotRatio,
|
||||
Location = IW4Info.HitLocation.head,
|
||||
HitCount = HitCount,
|
||||
Type = DetectionType.Bone
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
AboveThresholdCount++;
|
||||
Log.WriteDebug("**Maximum Headshot Ratio Reached For Flag**");
|
||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
||||
Log.WriteDebug($"**Kills: {Kills}");
|
||||
Log.WriteDebug($"**HitCount: {HitCount}");
|
||||
Log.WriteDebug($"**Ratio {currentHeadshotRatio}");
|
||||
Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}");
|
||||
var sb = new StringBuilder();
|
||||
foreach (var kvp in HitLocationCount)
|
||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
||||
Log.WriteDebug(sb.ToString());
|
||||
Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
||||
|
||||
return new DetectionPenaltyResult()
|
||||
result = new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
||||
RatioAmount = currentHeadshotRatio,
|
||||
Bone = IW4Info.HitLocation.head,
|
||||
KillCount = Kills
|
||||
Value = currentHeadshotRatio,
|
||||
Location = IW4Info.HitLocation.head,
|
||||
HitCount = HitCount,
|
||||
Type = DetectionType.Bone
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -158,7 +245,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
{
|
||||
Log.WriteDebug("**Maximum Bone Ratio Reached For Ban**");
|
||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
||||
Log.WriteDebug($"**Kills: {Kills}");
|
||||
Log.WriteDebug($"**HitCount: {HitCount}");
|
||||
Log.WriteDebug($"**Ratio {currentMaxBoneRatio}");
|
||||
Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForBan}");
|
||||
var sb = new StringBuilder();
|
||||
@ -166,19 +253,20 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
||||
Log.WriteDebug(sb.ToString());
|
||||
|
||||
return new DetectionPenaltyResult()
|
||||
result = new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||
RatioAmount = currentMaxBoneRatio,
|
||||
Bone = bone,
|
||||
KillCount = Kills
|
||||
Value = currentMaxBoneRatio,
|
||||
Location = bone,
|
||||
HitCount = HitCount,
|
||||
Type = DetectionType.Bone
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.WriteDebug("**Maximum Bone Ratio Reached For Flag**");
|
||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
||||
Log.WriteDebug($"**Kills: {Kills}");
|
||||
Log.WriteDebug($"**HitCount: {HitCount}");
|
||||
Log.WriteDebug($"**Ratio {currentMaxBoneRatio}");
|
||||
Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForFlag}");
|
||||
var sb = new StringBuilder();
|
||||
@ -186,12 +274,13 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
||||
Log.WriteDebug(sb.ToString());
|
||||
|
||||
return new DetectionPenaltyResult()
|
||||
result = new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
||||
RatioAmount = currentMaxBoneRatio,
|
||||
Bone = bone,
|
||||
KillCount = Kills
|
||||
Value = currentMaxBoneRatio,
|
||||
Location = bone,
|
||||
HitCount = HitCount,
|
||||
Type = DetectionType.Bone
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -199,12 +288,12 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
}
|
||||
|
||||
#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 lerpAmount = Math.Min(1.0, (chestKills - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills));
|
||||
double marginOfError = Thresholds.GetMarginOfError(chestHits);
|
||||
double lerpAmount = Math.Min(1.0, (chestHits - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills));
|
||||
// determine max acceptable ratio of chest to abdomen kills
|
||||
double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(3), Thresholds.ChestAbdomenRatioThresholdHighSample(3), lerpAmount) + marginOfError;
|
||||
double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(4), Thresholds.ChestAbdomenRatioThresholdHighSample(4), lerpAmount) + marginOfError;
|
||||
@ -214,32 +303,32 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
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($"ClientId: {kill.AttackerId}");
|
||||
Log.WriteDebug($"**Chest Kills: {chestKills}");
|
||||
Log.WriteDebug($"**Chest Hits: {chestHits}");
|
||||
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
||||
Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}");
|
||||
var sb = new StringBuilder();
|
||||
foreach (var kvp in HitLocationCount)
|
||||
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
|
||||
Log.WriteDebug(sb.ToString());
|
||||
// Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
||||
|
||||
return new DetectionPenaltyResult()
|
||||
result = new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||
RatioAmount = currentChestAbdomenRatio,
|
||||
Bone = 0,
|
||||
KillCount = chestKills
|
||||
Value = currentChestAbdomenRatio,
|
||||
Location = IW4Info.HitLocation.torso_upper,
|
||||
Type = DetectionType.Chest,
|
||||
HitCount = chestHits
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Flag**");
|
||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
||||
Log.WriteDebug($"**Chest Kills: {chestKills}");
|
||||
Log.WriteDebug($"**Chest Hits: {chestHits}");
|
||||
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
||||
Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}");
|
||||
var sb = new StringBuilder();
|
||||
@ -248,38 +337,50 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
Log.WriteDebug(sb.ToString());
|
||||
// Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
||||
|
||||
return new DetectionPenaltyResult()
|
||||
result = new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
||||
RatioAmount = currentChestAbdomenRatio,
|
||||
Bone = 0,
|
||||
KillCount = chestKills
|
||||
Value = currentChestAbdomenRatio,
|
||||
Location = IW4Info.HitLocation.torso_upper,
|
||||
Type = DetectionType.Chest,
|
||||
HitCount = chestHits
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
return new DetectionPenaltyResult()
|
||||
|
||||
Tracker.OnChange(new DetectionTracking(ClientStats, kill, Strain));
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
foreach (string change in Tracker.GetChanges())
|
||||
{
|
||||
Log.WriteDebug(change);
|
||||
Log.WriteDebug("--------------SNAPSHOT END-----------");
|
||||
}
|
||||
}
|
||||
|
||||
return result ?? new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Any,
|
||||
RatioAmount = 0
|
||||
};
|
||||
}
|
||||
|
||||
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 lerpAmount = Math.Min(1.0, (totalChestKills - 60) / 250.0);
|
||||
double marginOfError = Thresholds.GetMarginOfError(totalChestHits);
|
||||
double lerpAmount = Math.Min(1.0, (totalChestHits - 60) / 250.0);
|
||||
// determine max acceptable ratio of chest to abdomen kills
|
||||
double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(3.0), Thresholds.ChestAbdomenRatioThresholdHighSample(2), lerpAmount) + marginOfError;
|
||||
double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(4.0), Thresholds.ChestAbdomenRatioThresholdHighSample(4.0), 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(3.0), lerpAmount) + marginOfError;
|
||||
|
||||
double currentChestAbdomenRatio = totalChestKills /
|
||||
double currentChestAbdomenRatio = totalChestHits /
|
||||
stats.HitLocations.Single(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount;
|
||||
|
||||
if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag)
|
||||
@ -289,42 +390,42 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
{
|
||||
Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Ban**");
|
||||
Log.WriteDebug($"ClientId: {stats.ClientId}");
|
||||
Log.WriteDebug($"**Total Chest Kills: {totalChestKills}");
|
||||
Log.WriteDebug($"**Total Chest Hits: {totalChestHits}");
|
||||
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
||||
Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}");
|
||||
var sb = new StringBuilder();
|
||||
foreach (var location in stats.HitLocations)
|
||||
sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
|
||||
Log.WriteDebug(sb.ToString());
|
||||
// Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
||||
|
||||
return new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Ban,
|
||||
RatioAmount = currentChestAbdomenRatio,
|
||||
Bone = IW4Info.HitLocation.torso_upper,
|
||||
KillCount = totalChestKills
|
||||
Value = currentChestAbdomenRatio,
|
||||
Location = IW4Info.HitLocation.torso_upper,
|
||||
HitCount = totalChestHits,
|
||||
Type = DetectionType.Chest
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Flag**");
|
||||
Log.WriteDebug($"ClientId: {stats.ClientId}");
|
||||
Log.WriteDebug($"**Total Chest Kills: {totalChestKills}");
|
||||
Log.WriteDebug($"**Total Chest Hits: {totalChestHits}");
|
||||
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
|
||||
Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}");
|
||||
var sb = new StringBuilder();
|
||||
foreach (var location in stats.HitLocations)
|
||||
sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
|
||||
Log.WriteDebug(sb.ToString());
|
||||
// Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
|
||||
|
||||
return new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = Penalty.PenaltyType.Flag,
|
||||
RatioAmount = currentChestAbdomenRatio,
|
||||
Bone = IW4Info.HitLocation.torso_upper,
|
||||
KillCount = totalChestKills
|
||||
Value = currentChestAbdomenRatio,
|
||||
Location = IW4Info.HitLocation.torso_upper,
|
||||
HitCount = totalChestHits,
|
||||
Type = DetectionType.Chest
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -332,7 +433,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
|
||||
return new DetectionPenaltyResult()
|
||||
{
|
||||
Bone = IW4Info.HitLocation.none,
|
||||
ClientPenalty = Penalty.PenaltyType.Any
|
||||
};
|
||||
}
|
||||
|
@ -9,9 +9,10 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
{
|
||||
class DetectionPenaltyResult
|
||||
{
|
||||
public Detection.DetectionType Type { get; set; }
|
||||
public Penalty.PenaltyType ClientPenalty { get; set; }
|
||||
public double RatioAmount { get; set; }
|
||||
public IW4Info.HitLocation Bone { get; set; }
|
||||
public int KillCount { get; set; }
|
||||
public double Value { get; set; }
|
||||
public IW4Info.HitLocation Location { get; set; }
|
||||
public int HitCount { get; set; }
|
||||
}
|
||||
}
|
||||
|
57
Plugins/Stats/Cheat/DetectionTracking.cs
Normal file
57
Plugins/Stats/Cheat/DetectionTracking.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using IW4MAdmin.Plugins.Stats.Cheat;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
{
|
||||
class DetectionTracking : ITrackable
|
||||
{
|
||||
EFClientStatistics Stats;
|
||||
EFClientKill Hit;
|
||||
Strain Strain;
|
||||
|
||||
public DetectionTracking(EFClientStatistics stats, EFClientKill hit, Strain strain)
|
||||
{
|
||||
Stats = stats;
|
||||
Hit = hit;
|
||||
Strain = strain;
|
||||
}
|
||||
|
||||
public string GetTrackableValue()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"SPM = {Stats.SPM}");
|
||||
sb.AppendLine($"KDR = {Stats.KDR}");
|
||||
sb.AppendLine($"Kills = {Stats.Kills}");
|
||||
sb.AppendLine($"Session Score = {Stats.SessionScore}");
|
||||
sb.AppendLine($"Elo = {Stats.EloRating}");
|
||||
sb.AppendLine($"Max Sess Strain = {Stats.MaxSessionStrain}");
|
||||
sb.AppendLine($"MaxStrain = {Stats.MaxStrain}");
|
||||
sb.AppendLine($"Avg Offset = {Stats.AverageHitOffset}");
|
||||
sb.AppendLine($"TimePlayed, {Stats.TimePlayed}");
|
||||
sb.AppendLine($"HitDamage = {Hit.Damage}");
|
||||
sb.AppendLine($"HitOrigin = {Hit.KillOrigin}");
|
||||
sb.AppendLine($"DeathOrigin = {Hit.DeathOrigin}");
|
||||
sb.AppendLine($"ViewAngles = {Hit.ViewAngles}");
|
||||
sb.AppendLine($"WeaponId = {Hit.Weapon.ToString()}");
|
||||
sb.AppendLine($"Timeoffset = {Hit.TimeOffset}");
|
||||
sb.AppendLine($"HitLocation = {Hit.HitLoc.ToString()}");
|
||||
sb.AppendLine($"Distance = {Hit.Distance / 0.0254}");
|
||||
sb.AppendLine($"HitType = {Hit.DeathType.ToString()}");
|
||||
int i = 0;
|
||||
foreach (var predictedAngle in Hit.AnglesList)
|
||||
{
|
||||
sb.AppendLine($"Predicted Angle [{i}] {predictedAngle}");
|
||||
i++;
|
||||
}
|
||||
sb.AppendLine(Strain.GetTrackableValue());
|
||||
sb.AppendLine($"VictimId = {Hit.VictimId}");
|
||||
sb.AppendLine($"AttackerId = {Hit.AttackerId}");
|
||||
return sb.ToString();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
63
Plugins/Stats/Cheat/Strain.cs
Normal file
63
Plugins/Stats/Cheat/Strain.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
{
|
||||
class Strain : ITrackable
|
||||
{
|
||||
private const double StrainDecayBase = 0.9;
|
||||
private double CurrentStrain;
|
||||
private Vector3 LastAngle;
|
||||
private double LastDeltaTime;
|
||||
private double LastDistance;
|
||||
|
||||
public int TimesReachedMaxStrain { 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;
|
||||
|
||||
#if DEBUG
|
||||
Console.WriteLine($"Decay Factor = {decayFactor} ");
|
||||
#endif
|
||||
|
||||
double[] distance = Helpers.Extensions.AngleStuff(newAngle, LastAngle);
|
||||
LastDistance = distance[0] + distance[1];
|
||||
|
||||
// this happens on first kill
|
||||
if ((distance[0] == 0 && distance[1] == 0) ||
|
||||
deltaTime == 0 ||
|
||||
double.IsNaN(CurrentStrain))
|
||||
{
|
||||
return CurrentStrain;
|
||||
}
|
||||
|
||||
double newStrain = Math.Pow(distance[0] + distance[1], 0.99) / deltaTime;
|
||||
newStrain *= killDistance / 1000.0;
|
||||
|
||||
CurrentStrain += newStrain;
|
||||
|
||||
if (CurrentStrain > Thresholds.MaxStrainBan)
|
||||
TimesReachedMaxStrain++;
|
||||
|
||||
LastAngle = newAngle;
|
||||
return CurrentStrain;
|
||||
}
|
||||
|
||||
public string GetTrackableValue()
|
||||
{
|
||||
return $"Strain = {CurrentStrain}\r\n, Angle = {LastAngle}\r\n, Delta Time = {LastDeltaTime}\r\n, Angle Between = {LastDistance}";
|
||||
}
|
||||
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
{
|
||||
@ -31,6 +27,10 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
public const int HighSampleMinKills = 100;
|
||||
public const double KillTimeThreshold = 0.2;
|
||||
|
||||
public const double MaxStrainBan = 0.4;
|
||||
public const double MaxOffset = 1.2;
|
||||
public const double MaxStrainFlag = 0.36;
|
||||
|
||||
public static double GetMarginOfError(int numKills) => 1.6455 / Math.Sqrt(numKills);
|
||||
|
||||
public static double Lerp(double v1, double v2, double amount)
|
||||
|
75
Plugins/Stats/Commands/MostPlayed.cs
Normal file
75
Plugins/Stats/Commands/MostPlayed.cs
Normal file
@ -0,0 +1,75 @@
|
||||
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;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
class MostPlayed : Command
|
||||
{
|
||||
public static async Task<List<string>> GetMostPlayed(Server s)
|
||||
{
|
||||
int serverId = s.GetHashCode();
|
||||
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 != Player.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", Player.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)
|
||||
await E.Origin.Tell(stat);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var stat in topStats)
|
||||
await E.Owner.Broadcast(stat);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
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", Player.Permission.User, false) { }
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
@ -25,18 +25,21 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
stats.Kills = 0;
|
||||
stats.SPM = 0.0;
|
||||
stats.Skill = 0.0;
|
||||
stats.TimePlayed = 0;
|
||||
// todo: make this more dynamic
|
||||
stats.EloRating = 200.0;
|
||||
|
||||
// reset the cached version
|
||||
Plugin.Manager.ResetStats(E.Origin.ClientId, E.Owner.GetHashCode());
|
||||
|
||||
// fixme: this doesn't work properly when another context exists
|
||||
await svc.SaveChangesAsync();
|
||||
await E.Origin.Tell("Your stats for this server have been reset");
|
||||
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_RESET_SUCCESS"]);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
await E.Origin.Tell("You must be connected to a server to reset your stats");
|
||||
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_RESET_FAIL"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,53 +8,78 @@ using SharedLibraryCore.Objects;
|
||||
using SharedLibraryCore.Services;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using SharedLibraryCore.Database;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
class TopStats : Command
|
||||
{
|
||||
public TopStats() : base("topstats", "view the top 5 players on this server", "ts", Player.Permission.User, false) { }
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
public static async Task<List<string>> GetTopStats(Server s)
|
||||
{
|
||||
var statsSvc = new GenericRepository<EFClientStatistics>();
|
||||
int serverId = E.Owner.GetHashCode();
|
||||
int serverId = s.GetHashCode();
|
||||
List<string> topStatsText = new List<string>()
|
||||
{
|
||||
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
|
||||
};
|
||||
|
||||
using (var db = new DatabaseContext())
|
||||
{
|
||||
db.ChangeTracker.AutoDetectChangesEnabled = false;
|
||||
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
|
||||
var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1);
|
||||
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();
|
||||
|
||||
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 >= 3600
|
||||
where client.Level != Player.Permission.Banned
|
||||
where client.LastConnection >= thirtyDaysAgo
|
||||
orderby stats.Performance descending
|
||||
select new
|
||||
{
|
||||
stats.KDR,
|
||||
stats.Performance,
|
||||
alias.Name
|
||||
})
|
||||
.Take(5);
|
||||
|
||||
if (!E.Message.IsBroadcastCommand())
|
||||
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>()
|
||||
{
|
||||
await E.Origin.Tell("^5--Top Players--");
|
||||
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_NOQUALIFY"]
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var stat in topStats)
|
||||
await E.Origin.Tell($"^3{stat.Name}^7 - ^5{stat.KDR} ^7KDR | ^5{stat.Skill} ^7SKILL");
|
||||
}
|
||||
else
|
||||
{
|
||||
await E.Owner.Broadcast("^5--Top Players--");
|
||||
return topStatsText;
|
||||
}
|
||||
|
||||
foreach (var stat in topStats)
|
||||
await E.Owner.Broadcast($"^3{stat.Name}^7 - ^5{stat.KDR} ^7KDR | ^5{stat.Skill} ^7SKILL");
|
||||
}
|
||||
public TopStats() : base("topstats", Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_DESC"], "ts", Player.Permission.User, false) { }
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
var topStats = await GetTopStats(E.Owner);
|
||||
if (!E.Message.IsBroadcastCommand())
|
||||
{
|
||||
foreach (var stat in topStats)
|
||||
await E.Origin.Tell(stat);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var stat in topStats)
|
||||
await E.Owner.Broadcast(stat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
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", Player.Permission.User, false, new CommandArgument[]
|
||||
{
|
||||
new CommandArgument()
|
||||
{
|
||||
@ -24,24 +24,26 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
if (E.Target?.ClientNumber < 0)
|
||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
|
||||
/*if (E.Target?.ClientNumber < 0)
|
||||
{
|
||||
await E.Origin.Tell("The specified player must be ingame");
|
||||
await E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME"]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (E.Origin.ClientNumber < 0 && E.Target == null)
|
||||
{
|
||||
await E.Origin.Tell("You must be ingame to view your stats");
|
||||
await E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME_SELF"]);
|
||||
return;
|
||||
}
|
||||
}*/
|
||||
|
||||
String statLine;
|
||||
EFClientStatistics pStats;
|
||||
|
||||
if (E.Data.Length > 0 && E.Target == null)
|
||||
{
|
||||
await E.Origin.Tell("Cannot find the player you specified");
|
||||
await E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -51,26 +53,26 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
if (E.Target != null)
|
||||
{
|
||||
pStats = clientStats.Find(c => c.ServerId == serverId && c.ClientId == E.Target.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);
|
||||
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()}";
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
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);
|
||||
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()}";
|
||||
}
|
||||
|
||||
if (E.Message.IsBroadcastCommand())
|
||||
{
|
||||
string name = E.Target == null ? E.Origin.Name : E.Target.Name;
|
||||
await E.Owner.Broadcast($"Stats for ^5{name}^7");
|
||||
await E.Owner.Broadcast($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{name}^7");
|
||||
await E.Owner.Broadcast(statLine);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (E.Target != null)
|
||||
await E.Origin.Tell($"Stats for ^5{E.Target.Name}^7");
|
||||
await E.Origin.Tell($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{E.Target.Name}^7");
|
||||
await E.Origin.Tell(statLine);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,6 @@
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Config
|
||||
{
|
||||
@ -16,12 +12,8 @@ namespace IW4MAdmin.Plugins.Stats.Config
|
||||
public string Name() => "Stats";
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
var config = new StatsConfiguration();
|
||||
|
||||
Console.Write("Enable server-side anti-cheat? [y/n]: ");
|
||||
config.EnableAntiCheat = (Console.ReadLine().ToLower().FirstOrDefault() as char?) == 'y';
|
||||
|
||||
config.KillstreakMessages = new List<StreakMessageConfiguration>()
|
||||
EnableAntiCheat = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]);
|
||||
KillstreakMessages = new List<StreakMessageConfiguration>()
|
||||
{
|
||||
new StreakMessageConfiguration(){
|
||||
Count = -1,
|
||||
@ -42,7 +34,7 @@ namespace IW4MAdmin.Plugins.Stats.Config
|
||||
}
|
||||
};
|
||||
|
||||
config.DeathstreakMessages = new List<StreakMessageConfiguration>()
|
||||
DeathstreakMessages = new List<StreakMessageConfiguration>()
|
||||
{
|
||||
new StreakMessageConfiguration()
|
||||
{
|
||||
@ -55,7 +47,7 @@ namespace IW4MAdmin.Plugins.Stats.Config
|
||||
},
|
||||
};
|
||||
|
||||
return config;
|
||||
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 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 System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
@ -15,6 +11,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
public ConcurrentDictionary<int, Detection> PlayerDetections { get; set; }
|
||||
public EFServerStatistics ServerStatistics { get; private set; }
|
||||
public EFServer Server { get; private set; }
|
||||
public bool IsTeamBased { get; set; }
|
||||
|
||||
public ServerStats(EFServer sv, EFServerStatistics st)
|
||||
{
|
||||
@ -23,5 +20,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ServerStatistics = st;
|
||||
Server = sv;
|
||||
}
|
||||
|
||||
public int TeamCount(IW4Info.Team teamName)
|
||||
{
|
||||
if (PlayerStats.Count(p => p.Value.Team == IW4Info.Team.Spectator) / (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Objects;
|
||||
using SharedLibraryCore.Commands;
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
@ -68,12 +69,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
statsSvc.ServerStatsSvc.SaveChanges();
|
||||
|
||||
var serverStats = statsSvc.ServerStatsSvc.Find(c => c.ServerId == serverId).FirstOrDefault();
|
||||
Servers.TryAdd(serverId, new ServerStats(server, serverStats));
|
||||
Servers.TryAdd(serverId, new ServerStats(server, serverStats)
|
||||
{
|
||||
IsTeamBased = sv.Gametype != "dm"
|
||||
});
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.WriteError($"Could not add server to ServerStats - {e.Message}");
|
||||
Log.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_ERROR_ADD"]} - {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,7 +88,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
/// <returns>EFClientStatistic of specified player</returns>
|
||||
public async Task<EFClientStatistics> AddPlayer(Player pl)
|
||||
{
|
||||
Log.WriteInfo($"Adding {pl} to stats");
|
||||
int serverId = pl.CurrentServer.GetHashCode();
|
||||
|
||||
if (!Servers.ContainsKey(serverId))
|
||||
@ -95,10 +98,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
var playerStats = Servers[serverId].PlayerStats;
|
||||
var statsSvc = ContextThreads[serverId];
|
||||
var detectionStats = Servers[serverId].PlayerDetections;
|
||||
|
||||
if (playerStats.ContainsKey(pl.ClientId))
|
||||
{
|
||||
Log.WriteWarning($"Duplicate ClientId in stats {pl.ClientId}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// get the client's stats from the database if it exists, otherwise create and attach a new one
|
||||
// if this fails we want to throw an exception
|
||||
var clientStats = statsSvc.ClientStatSvc.Find(c => c.ClientId == pl.ClientId && c.ServerId == serverId).FirstOrDefault();
|
||||
var clientStatsSvc = statsSvc.ClientStatSvc;
|
||||
var clientStats = clientStatsSvc.Find(c => c.ClientId == pl.ClientId && c.ServerId == serverId).FirstOrDefault();
|
||||
|
||||
if (clientStats == null)
|
||||
{
|
||||
@ -111,6 +122,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ServerId = serverId,
|
||||
Skill = 0.0,
|
||||
SPM = 0.0,
|
||||
EloRating = 200.0,
|
||||
HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>().Select(hl => new EFHitLocationCount()
|
||||
{
|
||||
Active = true,
|
||||
@ -120,12 +132,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.ToList()
|
||||
};
|
||||
|
||||
clientStats = statsSvc.ClientStatSvc.Insert(clientStats);
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
// insert if they've not been added
|
||||
clientStats = clientStatsSvc.Insert(clientStats);
|
||||
await clientStatsSvc.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// migration for previous existing stats
|
||||
else if (clientStats.HitLocations.Count == 0)
|
||||
if (clientStats.HitLocations.Count == 0)
|
||||
{
|
||||
clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>().Select(hl => new EFHitLocationCount()
|
||||
{
|
||||
@ -134,7 +147,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
Location = hl
|
||||
})
|
||||
.ToList();
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
//await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// for stats before rating
|
||||
if (clientStats.EloRating == 0.0)
|
||||
{
|
||||
clientStats.EloRating = clientStats.Skill;
|
||||
}
|
||||
|
||||
if (clientStats.RollingWeightedKDR == 0)
|
||||
{
|
||||
clientStats.RollingWeightedKDR = clientStats.KDR;
|
||||
}
|
||||
|
||||
// set these on connecting
|
||||
@ -142,23 +166,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
clientStats.LastStatCalculation = DateTime.UtcNow;
|
||||
clientStats.SessionScore = pl.Score;
|
||||
|
||||
if (playerStats.ContainsKey(pl.ClientId))
|
||||
{
|
||||
Log.WriteWarning($"Duplicate ClientId in stats {pl.ClientId} vs {playerStats[pl.ClientId].ClientId}");
|
||||
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue);
|
||||
}
|
||||
playerStats.TryAdd(pl.ClientId, clientStats);
|
||||
Log.WriteInfo($"Adding {pl} to stats");
|
||||
|
||||
var detectionStats = Servers[serverId].PlayerDetections;
|
||||
if (!playerStats.TryAdd(pl.ClientId, clientStats))
|
||||
Log.WriteDebug($"Could not add client to stats {pl}");
|
||||
|
||||
if (detectionStats.ContainsKey(pl.ClientId))
|
||||
detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue);
|
||||
|
||||
detectionStats.TryAdd(pl.ClientId, new Cheat.Detection(Log, clientStats));
|
||||
|
||||
// todo: look at this more
|
||||
statsSvc.ClientStatSvc.Update(clientStats);
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
if (!detectionStats.TryAdd(pl.ClientId, new Cheat.Detection(Log, clientStats)))
|
||||
Log.WriteDebug("Could not add client to detection");
|
||||
|
||||
return clientStats;
|
||||
}
|
||||
@ -181,53 +195,89 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
if (!playerStats.ContainsKey(pl.ClientId))
|
||||
{
|
||||
Log.WriteWarning($"Client disconnecting not in stats {pl}");
|
||||
// remove the client from the stats dictionary as they're leaving
|
||||
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue1);
|
||||
detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue2);
|
||||
return;
|
||||
}
|
||||
|
||||
// get individual client's stats
|
||||
var clientStats = playerStats[pl.ClientId];
|
||||
// sync their score
|
||||
clientStats.SessionScore = pl.Score;
|
||||
|
||||
// remove the client from the stats dictionary as they're leaving
|
||||
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue);
|
||||
detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue2);
|
||||
playerStats.TryRemove(pl.ClientId, out EFClientStatistics removedValue3);
|
||||
detectionStats.TryRemove(pl.ClientId, out Cheat.Detection removedValue4);
|
||||
|
||||
// sync their stats before they leave
|
||||
var clientStatsSvc = statsSvc.ClientStatSvc;
|
||||
clientStats = UpdateStats(clientStats);
|
||||
clientStatsSvc.Update(clientStats);
|
||||
await clientStatsSvc.SaveChangesAsync();
|
||||
|
||||
// todo: should this be saved every disconnect?
|
||||
statsSvc.ClientStatSvc.Update(clientStats);
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
// increment the total play time
|
||||
serverStats.TotalPlayTime += (int)(DateTime.UtcNow - pl.LastConnection).TotalSeconds;
|
||||
await statsSvc.ServerStatsSvc.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, int serverId)
|
||||
{
|
||||
string regex = @"^(D);(.+);([0-9]+);(allies|axis);(.+);([0-9]+);(allies|axis);(.+);(.+);([0-9]+);(.+);(.+)$";
|
||||
var match = Regex.Match(eventLine, regex, RegexOptions.IgnoreCase);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
// this gives us what time the player is on
|
||||
var attackerStats = Servers[serverId].PlayerStats[attackerClientId];
|
||||
var victimStats = Servers[serverId].PlayerStats[victimClientId];
|
||||
IW4Info.Team victimTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[4].ToString());
|
||||
IW4Info.Team attackerTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[7].ToString());
|
||||
attackerStats.Team = attackerTeam;
|
||||
victimStats.Team = victimTeam;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process stats for kill event
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task AddScriptKill(Player attacker, Player victim, int serverId, string map, string hitLoc, string type,
|
||||
string damage, string weapon, string killOrigin, string deathOrigin, string viewAngles, string offset, string isKillstreakKill, string Ads)
|
||||
public async Task AddScriptHit(bool isDamage, DateTime time, Player attacker, Player victim, int serverId, string map, string hitLoc, string type,
|
||||
string damage, string weapon, string killOrigin, string deathOrigin, string viewAngles, string offset, string isKillstreakKill, string Ads, string snapAngles)
|
||||
{
|
||||
var statsSvc = ContextThreads[serverId];
|
||||
Vector3 vDeathOrigin = null;
|
||||
Vector3 vKillOrigin = null;
|
||||
Vector3 vViewAngles = null;
|
||||
|
||||
try
|
||||
{
|
||||
vDeathOrigin = Vector3.Parse(deathOrigin);
|
||||
vKillOrigin = Vector3.Parse(killOrigin);
|
||||
vViewAngles = Vector3.Parse(viewAngles).FixIW4Angles();
|
||||
}
|
||||
|
||||
catch (FormatException)
|
||||
{
|
||||
Log.WriteWarning("Could not parse kill or death origin vector");
|
||||
Log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin}");
|
||||
Log.WriteWarning("Could not parse kill or death origin or viewangle vectors");
|
||||
Log.WriteDebug($"Kill - {killOrigin} Death - {deathOrigin} ViewAngle - {viewAngles}");
|
||||
await AddStandardKill(attacker, victim);
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshotAngles = new List<Vector3>();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (string angle in snapAngles.Split(':', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
snapshotAngles.Add(Vector3.Parse(angle).FixIW4Angles());
|
||||
}
|
||||
}
|
||||
|
||||
catch (FormatException)
|
||||
{
|
||||
Log.WriteWarning("Could not parse snapshot angles");
|
||||
return;
|
||||
}
|
||||
|
||||
var kill = new EFClientKill()
|
||||
{
|
||||
Active = true,
|
||||
@ -241,11 +291,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
Damage = Int32.Parse(damage),
|
||||
HitLoc = ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
|
||||
Weapon = ParseEnum<IW4Info.WeaponName>.Get(weapon, typeof(IW4Info.WeaponName)),
|
||||
ViewAngles = Vector3.Parse(viewAngles).FixIW4Angles(),
|
||||
ViewAngles = vViewAngles,
|
||||
TimeOffset = Int64.Parse(offset),
|
||||
When = DateTime.UtcNow,
|
||||
When = time,
|
||||
IsKillstreakKill = isKillstreakKill[0] != '0',
|
||||
AdsPercent = float.Parse(Ads)
|
||||
AdsPercent = float.Parse(Ads),
|
||||
AnglesList = snapshotAngles
|
||||
};
|
||||
|
||||
if (kill.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE &&
|
||||
@ -255,7 +306,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
return;
|
||||
}
|
||||
|
||||
await AddStandardKill(attacker, victim);
|
||||
if (!isDamage)
|
||||
{
|
||||
await AddStandardKill(attacker, victim);
|
||||
}
|
||||
|
||||
if (kill.IsKillstreakKill)
|
||||
{
|
||||
@ -264,6 +318,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
var clientDetection = Servers[serverId].PlayerDetections[attacker.ClientId];
|
||||
var clientStats = Servers[serverId].PlayerStats[attacker.ClientId];
|
||||
var clientStatsSvc = statsSvc.ClientStatSvc;
|
||||
clientStatsSvc.Update(clientStats);
|
||||
|
||||
// increment their hit count
|
||||
if (kill.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
|
||||
@ -272,8 +328,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
clientStats.HitLocations.Single(hl => hl.Location == kill.HitLoc).HitCount += 1;
|
||||
|
||||
statsSvc.ClientStatSvc.Update(clientStats);
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
//statsSvc.ClientStatSvc.Update(clientStats);
|
||||
// await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
}
|
||||
|
||||
//statsSvc.KillStatsSvc.Insert(kill);
|
||||
@ -283,28 +339,48 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
async Task executePenalty(Cheat.DetectionPenaltyResult penalty)
|
||||
{
|
||||
// prevent multiple bans from occuring
|
||||
if (attacker.Level == Player.Permission.Banned)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (penalty.ClientPenalty)
|
||||
{
|
||||
case Penalty.PenaltyType.Ban:
|
||||
await attacker.Ban("You appear to be cheating", new Player() { ClientId = 1 });
|
||||
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new Player()
|
||||
{
|
||||
ClientId = 1
|
||||
});
|
||||
break;
|
||||
case Penalty.PenaltyType.Flag:
|
||||
if (attacker.Level != Player.Permission.User)
|
||||
break;
|
||||
var flagCmd = new CFlag();
|
||||
await flagCmd.ExecuteAsync(new GameEvent(GameEvent.EventType.Flag, $"{(int)penalty.Bone}-{Math.Round(penalty.RatioAmount, 2).ToString()}@{penalty.KillCount}", new Player()
|
||||
var e = new GameEvent()
|
||||
{
|
||||
ClientId = 1,
|
||||
Level = Player.Permission.Console,
|
||||
ClientNumber = -1,
|
||||
CurrentServer = attacker.CurrentServer
|
||||
}, attacker, attacker.CurrentServer));
|
||||
Data = penalty.Type == Cheat.Detection.DetectionType.Bone ?
|
||||
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
|
||||
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
|
||||
Origin = new Player()
|
||||
{
|
||||
ClientId = 1,
|
||||
Level = Player.Permission.Console,
|
||||
ClientNumber = -1,
|
||||
CurrentServer = attacker.CurrentServer
|
||||
},
|
||||
Target = attacker,
|
||||
Owner = attacker.CurrentServer,
|
||||
Type = GameEvent.EventType.Flag
|
||||
};
|
||||
await new CFlag().ExecuteAsync(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await executePenalty(clientDetection.ProcessKill(kill));
|
||||
await executePenalty(clientDetection.ProcessKill(kill, isDamage));
|
||||
await executePenalty(clientDetection.ProcessTotalRatio(clientStats));
|
||||
|
||||
await clientStatsSvc.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@ -319,7 +395,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
Log.WriteError($"[Stats::AddStandardKill] kill attacker ClientId is invalid {attacker.ClientId}-{attacker}");
|
||||
// happens when the client has disconnected before the last status update
|
||||
Log.WriteWarning($"[Stats::AddStandardKill] kill attacker ClientId is invalid {attacker.ClientId}-{attacker}");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -331,18 +408,33 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
Log.WriteError($"[Stats::AddStandardKill] kill victim ClientId is invalid {victim.ClientId}-{victim}");
|
||||
Log.WriteWarning($"[Stats::AddStandardKill] kill victim ClientId is invalid {victim.ClientId}-{victim}");
|
||||
return;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
Log.WriteDebug("Calculating standard kill");
|
||||
#endif
|
||||
|
||||
// update the total stats
|
||||
Servers[serverId].ServerStatistics.TotalKills += 1;
|
||||
|
||||
// this happens when the round has changed
|
||||
if (attackerStats.SessionScore == 0)
|
||||
attackerStats.LastScore = 0;
|
||||
|
||||
if (victimStats.SessionScore == 0)
|
||||
victimStats.LastScore = 0;
|
||||
|
||||
attackerStats.SessionScore = attacker.Score;
|
||||
victimStats.SessionScore = victim.Score;
|
||||
|
||||
// calculate for the clients
|
||||
CalculateKill(attackerStats, victimStats);
|
||||
// this should fix the negative SPM
|
||||
// updates their last score after being calculated
|
||||
attackerStats.LastScore = attacker.Score;
|
||||
victimStats.LastScore = victim.Score;
|
||||
|
||||
// show encouragement/discouragement
|
||||
string streakMessage = (attackerStats.ClientId != victimStats.ClientId) ?
|
||||
@ -368,10 +460,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
// todo: do we want to save this immediately?
|
||||
var statsSvc = ContextThreads[serverId];
|
||||
statsSvc.ClientStatSvc.Update(attackerStats);
|
||||
statsSvc.ClientStatSvc.Update(victimStats);
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
var clientStatsSvc = ContextThreads[serverId].ClientStatSvc;
|
||||
clientStatsSvc.Update(attackerStats);
|
||||
clientStatsSvc.Update(victimStats);
|
||||
await clientStatsSvc.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -400,6 +492,46 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
// process the attacker's stats after the kills
|
||||
attackerStats = UpdateStats(attackerStats);
|
||||
|
||||
// calulate elo
|
||||
if (Servers[attackerStats.ServerId].PlayerStats.Count > 1)
|
||||
{
|
||||
/* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats
|
||||
.Where(cs => cs.Value.ClientId != attackerStats.ClientId)
|
||||
.Where(cs =>
|
||||
Servers[attackerStats.ServerId].IsTeamBased ?
|
||||
cs.Value.Team != attackerStats.Team :
|
||||
cs.Value.Team != IW4Info.Team.Spectator)
|
||||
.Where(cs => cs.Value.Team != IW4Info.Team.Spectator);
|
||||
|
||||
double attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ?
|
||||
validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) :
|
||||
attackerStats.EloRating;
|
||||
|
||||
var validVictimLobbyRatings = Servers[victimStats.ServerId].PlayerStats
|
||||
.Where(cs => cs.Value.ClientId != victimStats.ClientId)
|
||||
.Where(cs =>
|
||||
Servers[attackerStats.ServerId].IsTeamBased ?
|
||||
cs.Value.Team != victimStats.Team :
|
||||
cs.Value.Team != IW4Info.Team.Spectator)
|
||||
.Where(cs => cs.Value.Team != IW4Info.Team.Spectator);
|
||||
|
||||
double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ?
|
||||
validVictimLobbyRatings.Average(cs => cs.Value.EloRating) :
|
||||
victimStats.EloRating;*/
|
||||
|
||||
double attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) - Math.Log(Math.Max(1, attackerStats.EloRating));
|
||||
double winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E));
|
||||
|
||||
// double victimEloDifference = Math.Log(Math.Max(1, attackerStats.EloRating)) - Math.Log(Math.Max(1, victimStats.EloRating));
|
||||
// double lossPercentage = 1.0 / (1 + Math.Pow(10, victimEloDifference/ Math.E));
|
||||
|
||||
attackerStats.EloRating += 6.0 * (1 - winPercentage);
|
||||
victimStats.EloRating -= 6.0 * (1 - winPercentage);
|
||||
|
||||
attackerStats.EloRating = Math.Max(0, Math.Round(attackerStats.EloRating, 2));
|
||||
victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2));
|
||||
}
|
||||
|
||||
// update after calculation
|
||||
attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
|
||||
victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
|
||||
@ -416,24 +548,39 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
// prevent NaN or inactive time lowering SPM
|
||||
if ((DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0 < 0.01 ||
|
||||
(DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0 > 3 ||
|
||||
clientStats.SessionScore < 1)
|
||||
(DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0 > 3 ||
|
||||
clientStats.SessionScore == 0)
|
||||
{
|
||||
// prevents idle time counting
|
||||
clientStats.LastStatCalculation = DateTime.UtcNow;
|
||||
return clientStats;
|
||||
}
|
||||
|
||||
double timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0;
|
||||
double timeSinceLastActive = (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0;
|
||||
|
||||
// calculate the players Score Per Minute for the current session
|
||||
int scoreDifference = clientStats.LastScore == 0 ? 0 : clientStats.SessionScore - clientStats.LastScore;
|
||||
int scoreDifference = 0;
|
||||
// this means they've been tking or suicide and is the only time they can have a negative SPM
|
||||
if (clientStats.RoundScore < 0)
|
||||
{
|
||||
scoreDifference = clientStats.RoundScore + clientStats.LastScore;
|
||||
}
|
||||
|
||||
else if (clientStats.RoundScore > 0 && clientStats.LastScore < clientStats.RoundScore)
|
||||
{
|
||||
scoreDifference = clientStats.RoundScore - clientStats.LastScore;
|
||||
}
|
||||
|
||||
double killSPM = scoreDifference / timeSinceLastCalc;
|
||||
double spmMultiplier = 2.934 * Math.Pow(Servers[clientStats.ServerId].TeamCount(clientStats.Team == IW4Info.Team.Allies ? IW4Info.Team.Axis : IW4Info.Team.Allies), -0.454);
|
||||
killSPM *= Math.Max(1, spmMultiplier);
|
||||
|
||||
// calculate how much the KDR should weigh
|
||||
// 1.637 is a Eddie-Generated number that weights the KDR nicely
|
||||
double kdr = clientStats.Deaths == 0 ? clientStats.Kills : clientStats.KDR;
|
||||
double KDRWeight = Math.Round(Math.Pow(kdr, 1.637 / Math.E), 3);
|
||||
|
||||
// if no SPM, weight is 1 else the weight ishe current session's spm / lifetime average score per minute
|
||||
//double SPMWeightAgainstAverage = (clientStats.SPM < 1) ? 1 : killSPM / clientStats.SPM;
|
||||
double currentKDR = clientStats.SessionDeaths == 0 ? clientStats.SessionKills : clientStats.SessionKills / clientStats.SessionDeaths;
|
||||
double alpha = Math.Sqrt(2) / Math.Min(600, clientStats.Kills + clientStats.Deaths);
|
||||
clientStats.RollingWeightedKDR = (alpha * currentKDR) + (1.0 - alpha) * clientStats.KDR;
|
||||
double KDRWeight = Math.Round(Math.Pow(clientStats.RollingWeightedKDR, 1.637 / Math.E), 3);
|
||||
|
||||
// calculate the weight of the new play time against last 10 hours of gameplay
|
||||
int totalPlayTime = (clientStats.TimePlayed == 0) ?
|
||||
@ -444,6 +591,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
// calculate the new weight against average times the weight against play time
|
||||
clientStats.SPM = (killSPM * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight));
|
||||
|
||||
if (clientStats.SPM < 0)
|
||||
{
|
||||
Log.WriteWarning("[StatManager:UpdateStats] clientStats SPM < 0");
|
||||
Log.WriteDebug($"{scoreDifference}-{clientStats.RoundScore} - {clientStats.LastScore} - {clientStats.SessionScore}");
|
||||
clientStats.SPM = 0;
|
||||
}
|
||||
|
||||
clientStats.SPM = Math.Round(clientStats.SPM, 3);
|
||||
clientStats.Skill = Math.Round((clientStats.SPM * KDRWeight), 3);
|
||||
|
||||
@ -457,7 +612,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
clientStats.LastStatCalculation = DateTime.UtcNow;
|
||||
clientStats.LastScore = clientStats.SessionScore;
|
||||
//clientStats.LastScore = clientStats.SessionScore;
|
||||
|
||||
return clientStats;
|
||||
}
|
||||
@ -482,7 +637,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
var ieClientStats = statsSvc.ClientStatSvc.Find(cs => cs.ServerId == serverId);
|
||||
|
||||
// set these incase they've we've imported settings
|
||||
// set these incase we've imported settings
|
||||
serverStats.TotalKills = ieClientStats.Sum(cs => cs.Kills);
|
||||
serverStats.TotalPlayTime = Manager.GetClientService().GetTotalPlayTime().Result;
|
||||
|
||||
@ -494,10 +649,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
var serverStats = Servers[serverId];
|
||||
foreach (var stat in serverStats.PlayerStats.Values)
|
||||
{
|
||||
stat.KillStreak = 0;
|
||||
stat.DeathStreak = 0;
|
||||
}
|
||||
stat.StartNewSession();
|
||||
}
|
||||
|
||||
public void ResetStats(int clientId, int serverId)
|
||||
@ -507,6 +659,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
stats.Deaths = 0;
|
||||
stats.SPM = 0;
|
||||
stats.Skill = 0;
|
||||
stats.TimePlayed = 0;
|
||||
stats.EloRating = 200;
|
||||
}
|
||||
|
||||
public async Task AddMessageAsync(int clientId, int serverId, string message)
|
||||
@ -532,17 +686,20 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
int serverId = sv.GetHashCode();
|
||||
var statsSvc = ContextThreads[serverId];
|
||||
|
||||
Log.WriteDebug("Syncing server stats");
|
||||
Log.WriteDebug("Syncing stats contexts");
|
||||
await statsSvc.ServerStatsSvc.SaveChangesAsync();
|
||||
|
||||
Log.WriteDebug("Syncing client stats");
|
||||
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
|
||||
Log.WriteDebug("Syncing kill stats");
|
||||
//await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||
await statsSvc.KillStatsSvc.SaveChangesAsync();
|
||||
|
||||
Log.WriteDebug("Syncing servers");
|
||||
await statsSvc.ServerSvc.SaveChangesAsync();
|
||||
|
||||
statsSvc = null;
|
||||
// this should prevent the gunk for having a long lasting context.
|
||||
ContextThreads[serverId] = new ThreadSafeStatsService();
|
||||
}
|
||||
|
||||
public void SetTeamBased(int serverId, bool isTeamBased)
|
||||
{
|
||||
Servers[serverId].IsTeamBased = isTeamBased;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,19 +10,31 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
public class ThreadSafeStatsService
|
||||
{
|
||||
public GenericRepository<EFClientStatistics> ClientStatSvc { get; private set; }
|
||||
public GenericRepository<EFClientStatistics> ClientStatSvc
|
||||
{
|
||||
get
|
||||
{
|
||||
return new GenericRepository<EFClientStatistics>();
|
||||
}
|
||||
}
|
||||
public GenericRepository<EFServer> ServerSvc { get; private set; }
|
||||
public GenericRepository<EFClientKill> KillStatsSvc { get; private set; }
|
||||
public GenericRepository<EFServerStatistics> ServerStatsSvc { get; private set; }
|
||||
public GenericRepository<EFClientMessage> MessageSvc { get; private set; }
|
||||
public GenericRepository<EFClientMessage> MessageSvc
|
||||
{
|
||||
get
|
||||
{
|
||||
return new GenericRepository<EFClientMessage>();
|
||||
}
|
||||
}
|
||||
|
||||
public ThreadSafeStatsService()
|
||||
{
|
||||
ClientStatSvc = new GenericRepository<EFClientStatistics>();
|
||||
//ClientStatSvc = new GenericRepository<EFClientStatistics>();
|
||||
ServerSvc = new GenericRepository<EFServer>();
|
||||
KillStatsSvc = new GenericRepository<EFClientKill>();
|
||||
ServerStatsSvc = new GenericRepository<EFServerStatistics>();
|
||||
MessageSvc = new GenericRepository<EFClientMessage>();
|
||||
//MessageSvc = new GenericRepository<EFClientMessage>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,13 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
{
|
||||
public class IW4Info
|
||||
{
|
||||
public enum Team
|
||||
{
|
||||
Spectator,
|
||||
Axis,
|
||||
Allies
|
||||
}
|
||||
|
||||
public enum MeansOfDeath
|
||||
{
|
||||
NONE,
|
||||
|
@ -4,6 +4,7 @@ using SharedLibraryCore.Database.Models;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Models
|
||||
{
|
||||
@ -38,5 +39,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
||||
public bool IsKillstreakKill { get; set; }
|
||||
[NotMapped]
|
||||
public float AdsPercent { get; set; }
|
||||
[NotMapped]
|
||||
public List<Vector3> AnglesList { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,14 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
||||
public int Kills { get; set; }
|
||||
[Required]
|
||||
public int Deaths { get; set; }
|
||||
|
||||
public double EloRating { get; set; }
|
||||
public virtual ICollection<EFHitLocationCount> HitLocations { get; set; }
|
||||
|
||||
public double RollingWeightedKDR { get; set; }
|
||||
[NotMapped]
|
||||
public double Performance
|
||||
{
|
||||
get => Math.Round((EloRating + Skill) / 2.0, 2);
|
||||
}
|
||||
[NotMapped]
|
||||
public double KDR
|
||||
{
|
||||
@ -36,6 +41,8 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
||||
public double Skill { get; set; }
|
||||
[Required]
|
||||
public int TimePlayed { get; set; }
|
||||
[Required]
|
||||
public double MaxStrain { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public float AverageHitOffset
|
||||
@ -57,6 +64,37 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
||||
[NotMapped]
|
||||
public DateTime LastActive { get; set; }
|
||||
[NotMapped]
|
||||
public int SessionScore { get; set; }
|
||||
public double MaxSessionStrain { get; set; }
|
||||
public void StartNewSession()
|
||||
{
|
||||
KillStreak = 0;
|
||||
DeathStreak = 0;
|
||||
LastScore = 0;
|
||||
SessionScores.Add(0);
|
||||
}
|
||||
[NotMapped]
|
||||
public int SessionScore
|
||||
{
|
||||
set
|
||||
{
|
||||
SessionScores[SessionScores.Count - 1] = value;
|
||||
}
|
||||
get
|
||||
{
|
||||
return SessionScores.Sum();
|
||||
}
|
||||
}
|
||||
[NotMapped]
|
||||
public int RoundScore
|
||||
{
|
||||
get
|
||||
{
|
||||
return SessionScores[SessionScores.Count - 1];
|
||||
}
|
||||
}
|
||||
[NotMapped]
|
||||
private List<int> SessionScores = new List<int>() { 0 };
|
||||
[NotMapped]
|
||||
public IW4Info.Team Team { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,9 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
||||
public int HitCount { get; set; }
|
||||
[Required]
|
||||
public float HitOffsetAverage { get; set; }
|
||||
[Required]
|
||||
public float MaxAngleDistance { get; set; }
|
||||
[Required]
|
||||
public int ClientId { get; set; }
|
||||
[ForeignKey("ClientId"), Column(Order = 0 )]
|
||||
public EFClient Client { get; set; }
|
||||
|
@ -44,10 +44,12 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
await Manager.RemovePlayer(E.Origin);
|
||||
break;
|
||||
case GameEvent.EventType.Say:
|
||||
if (E.Data != string.Empty && E.Data.Trim().Length > 0 && E.Message.Trim()[0] != '!' && E.Origin.ClientId > 1)
|
||||
if (!string.IsNullOrEmpty(E.Data) &&
|
||||
E.Origin.ClientId > 1)
|
||||
await Manager.AddMessageAsync(E.Origin.ClientId, E.Owner.GetHashCode(), E.Data);
|
||||
break;
|
||||
case GameEvent.EventType.MapChange:
|
||||
Manager.SetTeamBased(E.Owner.GetHashCode(), E.Owner.Gametype != "dm");
|
||||
Manager.ResetKillstreaks(S.GetHashCode());
|
||||
await Manager.Sync(S);
|
||||
break;
|
||||
@ -69,18 +71,28 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
break;
|
||||
case GameEvent.EventType.Flag:
|
||||
break;
|
||||
case GameEvent.EventType.Script:
|
||||
case GameEvent.EventType.ScriptKill:
|
||||
string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
|
||||
if (killInfo.Length >= 13)
|
||||
await Manager.AddScriptHit(false, E.Time, E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8],
|
||||
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13]);
|
||||
break;
|
||||
case GameEvent.EventType.Kill:
|
||||
string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
|
||||
if (killInfo.Length >= 9 && killInfo[0].Contains("ScriptKill") && E.Owner.CustomCallback)
|
||||
await Manager.AddScriptKill(E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8],
|
||||
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12]);
|
||||
else if (!E.Owner.CustomCallback)
|
||||
if (!E.Owner.CustomCallback)
|
||||
await Manager.AddStandardKill(E.Origin, E.Target);
|
||||
break;
|
||||
case GameEvent.EventType.Death:
|
||||
break;
|
||||
case GameEvent.EventType.Damage:
|
||||
// if (!E.Owner.CustomCallback)
|
||||
Manager.AddDamageEvent(E.Data, E.Origin.ClientId, E.Target.ClientId, E.Owner.GetHashCode());
|
||||
break;
|
||||
case GameEvent.EventType.ScriptDamage:
|
||||
killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
|
||||
if (killInfo.Length >= 13)
|
||||
await Manager.AddScriptHit(true, E.Time, E.Origin, E.Target, S.GetHashCode(), S.CurrentMap.Name, killInfo[7], killInfo[8],
|
||||
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,34 +115,36 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
int kills = clientStats.Sum(c => c.Kills);
|
||||
int deaths = clientStats.Sum(c => c.Deaths);
|
||||
double kdr = Math.Round(kills / (double)deaths, 2);
|
||||
double skill = Math.Round(clientStats.Sum(c => c.Skill) / clientStats.Count, 2);
|
||||
double spm = Math.Round(clientStats.Sum(c => c.SPM), 1);
|
||||
var validPerformanceValues = clientStats.Where(c => c.Performance > 0);
|
||||
int performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed);
|
||||
double performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
|
||||
double spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Where(c => c.SPM > 0).Count(), 1);
|
||||
|
||||
return new List<ProfileMeta>()
|
||||
{
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Kills",
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"],
|
||||
Value = kills
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Deaths",
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"],
|
||||
Value = deaths
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "KDR",
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"],
|
||||
Value = kdr
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Skill",
|
||||
Value = skill
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
|
||||
Value = performance
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Score Per Minute",
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_META_SPM"],
|
||||
Value = spm
|
||||
}
|
||||
};
|
||||
@ -146,6 +160,8 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
double abdomenRatio = 0;
|
||||
double chestAbdomenRatio = 0;
|
||||
double hitOffsetAverage = 0;
|
||||
double maxStrain = clientStats.Count(c => c.MaxStrain > 0) == 0 ? 0 : clientStats.Max(cs => cs.MaxStrain);
|
||||
//double maxAngle = clientStats.Max(cs => cs.HitLocations.Max(hl => hl.MaxAngleDistance));
|
||||
|
||||
if (clientStats.Where(cs => cs.HitLocations.Count > 0).FirstOrDefault() != null)
|
||||
{
|
||||
@ -165,16 +181,17 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
(double)clientStats.Where(c => c.HitLocations.Count > 0)
|
||||
.Sum(c => c.HitLocations.Where(hl => hl.Location != IW4Info.HitLocation.none).Sum(f => f.HitCount)), 2);
|
||||
|
||||
hitOffsetAverage = clientStats.Sum(c => c.AverageHitOffset) / Math.Max(1, clientStats.Where(c => c.AverageHitOffset > 0).Count());
|
||||
var validOffsets = clientStats.Where(c => c.HitLocations.Count(hl => hl.HitCount > 0) > 0).SelectMany(hl => hl.HitLocations);
|
||||
hitOffsetAverage = validOffsets.Sum(o => o.HitCount * o.HitOffsetAverage) / (double)validOffsets.Sum(o => o.HitCount);
|
||||
}
|
||||
|
||||
return new List<ProfileMeta>()
|
||||
{
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Chest Ratio",
|
||||
Value = chestRatio,
|
||||
Sensitive = true
|
||||
Key = "Chest Ratio",
|
||||
Value = chestRatio,
|
||||
Sensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
@ -197,9 +214,21 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Hit Offset Average",
|
||||
Value = $"{Math.Round(((float)hitOffsetAverage).ToDegrees(), 4)}°",
|
||||
Value = $"{Math.Round(((float)hitOffsetAverage), 4)}°",
|
||||
Sensitive = true
|
||||
}
|
||||
},
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = "Max Strain",
|
||||
Value = Math.Round(maxStrain, 3),
|
||||
Sensitive = true
|
||||
},
|
||||
/*new ProfileMeta()
|
||||
{
|
||||
Key = "Max Angle Distance",
|
||||
Value = Math.Round(maxAngle, 1),
|
||||
Sensitive = true
|
||||
}*/
|
||||
};
|
||||
}
|
||||
|
||||
@ -215,7 +244,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
}).ToList();
|
||||
messageMeta.Add(new ProfileMeta()
|
||||
{
|
||||
Key = "Messages",
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGES"],
|
||||
Value = messages.Count
|
||||
});
|
||||
|
||||
@ -231,29 +260,41 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
|
||||
MetaService.AddMeta(getMessages);
|
||||
|
||||
string totalKills()
|
||||
string totalKills(Server server)
|
||||
{
|
||||
var serverStats = new GenericRepository<EFServerStatistics>();
|
||||
return serverStats.Find(s => s.Active)
|
||||
.Sum(c => c.TotalKills).ToString("#,##0");
|
||||
}
|
||||
|
||||
string totalPlayTime()
|
||||
string totalPlayTime(Server server)
|
||||
{
|
||||
var serverStats = new GenericRepository<EFServerStatistics>();
|
||||
return Math.Ceiling((serverStats.GetQuery(s => s.Active)
|
||||
.Sum(c => c.TotalPlayTime) / 3600.0)).ToString("#,##0");
|
||||
}
|
||||
|
||||
string topStats(Server s)
|
||||
{
|
||||
return String.Join(Environment.NewLine, Commands.TopStats.GetTopStats(s).Result);
|
||||
}
|
||||
|
||||
string mostPlayed(Server s)
|
||||
{
|
||||
return String.Join(Environment.NewLine, Commands.MostPlayed.GetMostPlayed(s).Result);
|
||||
}
|
||||
|
||||
manager.GetMessageTokens().Add(new MessageToken("TOTALKILLS", totalKills));
|
||||
manager.GetMessageTokens().Add(new MessageToken("TOTALPLAYTIME", totalPlayTime));
|
||||
manager.GetMessageTokens().Add(new MessageToken("TOPSTATS", topStats));
|
||||
manager.GetMessageTokens().Add(new MessageToken("MOSTPLAYED", mostPlayed));
|
||||
|
||||
ServerManager = manager;
|
||||
|
||||
Manager = new StatManager(manager);
|
||||
}
|
||||
|
||||
public Task OnTickAsync(Server S) => Utilities.CompletedTask;
|
||||
public Task OnTickAsync(Server S) => Task.CompletedTask;
|
||||
|
||||
public async Task OnUnloadAsync()
|
||||
{
|
||||
|
@ -14,10 +14,18 @@
|
||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Cheat\Strain.cs~RF16f7b3.TMP" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||
</Target>
|
||||
|
@ -51,8 +51,9 @@ namespace IW4MAdmin.Plugins
|
||||
|
||||
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;
|
||||
|
||||
public async Task OnTickAsync(Server S)
|
||||
public Task OnTickAsync(Server S)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
/*
|
||||
if ((DateTime.Now - Interval).TotalSeconds > 1)
|
||||
{
|
||||
|
@ -19,4 +19,8 @@
|
||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -72,9 +72,9 @@ namespace IW4MAdmin.Plugins.Welcome
|
||||
}
|
||||
}
|
||||
|
||||
public Task OnUnloadAsync() => Utilities.CompletedTask;
|
||||
public Task OnUnloadAsync() => Task.CompletedTask;
|
||||
|
||||
public Task OnTickAsync(Server S) => Utilities.CompletedTask;
|
||||
public Task OnTickAsync(Server S) => Task.CompletedTask;
|
||||
|
||||
public async Task OnEventAsync(GameEvent E, Server S)
|
||||
{
|
||||
|
@ -24,6 +24,10 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"
copy "$(ProjectDir)MaxMind\GeoIP.dat" "$(SolutionDir)BUILD\Plugins\GeoIP.dat"" />
|
||||
</Target>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Welcome
|
||||
{
|
||||
@ -10,9 +11,9 @@ namespace IW4MAdmin.Plugins.Welcome
|
||||
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
UserAnnouncementMessage = "^5{{ClientName}} ^7hails from ^5{{ClientLocation}}";
|
||||
UserWelcomeMessage = "Welcome ^5{{ClientName}}^7, this is your ^5{{TimesConnected}} ^7time connecting!";
|
||||
PrivilegedAnnouncementMessage = "{{ClientLevel}} {{ClientName}} has joined the server";
|
||||
UserAnnouncementMessage = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_WELCOME_USERANNOUNCE"];
|
||||
UserWelcomeMessage = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_WELCOME_USERWELCOME"];
|
||||
PrivilegedAnnouncementMessage = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_WELCOME_PRIVANNOUNCE"];
|
||||
return this;
|
||||
}
|
||||
|
||||
|
89
README.md
89
README.md
@ -2,18 +2,17 @@
|
||||
|
||||
# IW4MAdmin
|
||||
### Quick Start Guide
|
||||
### Version 2.0
|
||||
### Version 2.1
|
||||
_______
|
||||
### About
|
||||
**IW4MAdmin** is an administration tool for [IW4x](https://iw4xcachep26muba.onion.link/), [T6M](https://plutonium.pw/), and most Call of Duty<74> dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze.
|
||||
**IW4MAdmin** is an administration tool for [IW4x](https://iw4xcachep26muba.onion.link/), [Pluto T6](https://forum.plutonium.pw/category/33/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/5/plutonium-iw5), and most Call of Duty<74> dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze.
|
||||
|
||||
### Setup
|
||||
**IW4MAdmin** requires minimal configuration to run. There is only one prerequisite.
|
||||
* [.NET Core 2.0.5 Runtime](https://www.microsoft.com/net/download/dotnet-core/runtime-2.0.5) *or newer*
|
||||
* [.NET Core 2.0.7 Runtime](https://www.microsoft.com/net/download/dotnet-core/runtime-2.0.7) *or newer*
|
||||
|
||||
1. Extract `IW4MAdmin-<version>.zip`
|
||||
2. Open command prompt or terminal in the extracted folder
|
||||
3. Run `>dotnet IW4MAdmin.dll`
|
||||
2. Run `StartIW4MAdmin.cmd`
|
||||
___
|
||||
|
||||
### Configuration
|
||||
@ -37,9 +36,8 @@ When **IW4MAdmin** is launched for the _first time_, you will be prompted to set
|
||||
* Allow clients to use a [VPN](https://en.wikipedia.org/wiki/Virtual_private_network)
|
||||
* _This feature requires an active api key on [iphub.info](https://iphub.info/)_
|
||||
|
||||
`Enable discord link`
|
||||
* Shows a link to your server's discord on the webfront
|
||||
* _This feature requires an invite link to your discord server_
|
||||
`Enable social link`
|
||||
* Shows a link to your community's social media/website on the webfront
|
||||
|
||||
`Use Custom Encoding Parser`
|
||||
* Allows alternative encodings to be used for parsing game information and events
|
||||
@ -55,6 +53,13 @@ If you wish to further customize your experience of **IW4MAdmin**, the following
|
||||
* Specifies the address and port the webfront will listen on.
|
||||
* The value can be an [IP Address](https://en.wikipedia.org/wiki/IP_address):port or [Domain Name](https://en.wikipedia.org/wiki/Domain_name):port
|
||||
|
||||
`CustomLocale`
|
||||
* Specifies a [locale name](https://msdn.microsoft.com/en-us/library/39cwe7zf.aspx) to use instead of system default
|
||||
* Locale must be from the `Equivalent Locale Name` column
|
||||
|
||||
`ConnectionString`
|
||||
* Specifies the [connection string](https://www.connectionstrings.com/mysql/) to a MySQL server to be used instead of SQLite
|
||||
|
||||
`Servers`
|
||||
* Specifies the list of servers **IW4MAdmin** will monitor
|
||||
* `IPAddress`
|
||||
@ -73,6 +78,12 @@ If you wish to further customize your experience of **IW4MAdmin**, the following
|
||||
|
||||
`AutoMessages`
|
||||
* Specifies the list of messages that are broadcasted to **all** servers
|
||||
* Specially formatted tokens can be used to broadcast dynamic information
|
||||
* `{{TOTALPLAYERS}}` — displays how many players have connected
|
||||
* `{{TOPSTATS}}` — displays the top 5 players on the server based on performance
|
||||
* `{{MOSTPLAYED}}` — displays the top 5 players based on number of kills
|
||||
* `{{TOTALPLAYTIME}}` — displays the cumulative play time (in man-hours) on all monitored servers
|
||||
* `{{VERSION}}` — displays the version of **IW4MAdmin**
|
||||
|
||||
`GlobalRules`
|
||||
* Specifies the list of rules that apply to **all** servers`
|
||||
@ -88,44 +99,48 @@ ___
|
||||
### Commands
|
||||
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|
||||
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|
|
||||
|prune|pa|demote any admins that have not connected recently (defaults to 30 days)|False|!pa \<optional inactive days\>|Owner|
|
||||
|prune|pa|demote any privileged clients that have not connected recently (defaults to 30 days)|False|!pa \<optional inactive days\>|Owner|
|
||||
|quit|q|quit IW4MAdmin|False|!q |Owner|
|
||||
|rcon|rcon|send rcon command to server|False|!rcon \<command\>|Owner|
|
||||
|ban|b|permanently ban a player from the server|True|!b \<player\> \<reason\>|SeniorAdmin|
|
||||
|unban|ub|unban player by database id|True|!ub \<databaseID\> \<reason\>|SeniorAdmin|
|
||||
|find|f|find player in database|False|!f \<player\>|Administrator|
|
||||
|rcon|rcon|send rcon command to server|False|!rcon \<commands\>|Owner|
|
||||
|ban|b|permanently ban a client from the server|True|!b \<player\> \<reason\>|SeniorAdmin|
|
||||
|unban|ub|unban client by client id|True|!ub \<client id\> \<reason\>|SeniorAdmin|
|
||||
|find|f|find client in database|False|!f \<player\>|Administrator|
|
||||
|killserver|kill|kill the game server|False|!kill |Administrator|
|
||||
|map|m|change to specified map|False|!m \<map\>|Administrator|
|
||||
|maprotate|mr|cycle to the next map in rotation|False|!mr |Administrator|
|
||||
|plugins|p|view all loaded plugins|False|!p |Administrator|
|
||||
|alias|known|get past aliases and ips of a player|True|!known \<player\>|Moderator|
|
||||
|baninfo|bi|get information about a ban for a player|True|!bi \<player\>|Moderator|
|
||||
|tempban|tb|temporarily ban a client for specified time (defaults to 1 hour)|True|!tb \<player\> \<duration (m\|h\|d\|w\|y)\> \<reason\>|Administrator|
|
||||
|alias|known|get past aliases and ips of a client|True|!known \<player\>|Moderator|
|
||||
|baninfo|bi|get information about a ban for a client|True|!bi \<player\>|Moderator|
|
||||
|fastrestart|fr|fast restart current map|False|!fr |Moderator|
|
||||
|flag|fp|flag a suspicious player and announce to admins on join|True|!fp \<player\> \<reason\>|Moderator|
|
||||
|flag|fp|flag a suspicious client and announce to admins on join|True|!fp \<player\> \<reason\>|Moderator|
|
||||
|kick|k|kick a client by name|True|!k \<player\> \<reason\>|Moderator|
|
||||
|list|l|list active clients|False|!l |Moderator|
|
||||
|mask|hide|hide your presence as an administrator|False|!hide |Moderator|
|
||||
|mask|hide|hide your presence as a privileged client|False|!hide |Moderator|
|
||||
|reports|reps|get or clear recent reports|False|!reps \<optional clear\>|Moderator|
|
||||
|say|s|broadcast message to all players|False|!s \<message\>|Moderator|
|
||||
|setlevel|sl|set player to specified administration level|True|!sl \<player\> \<level\>|Moderator|
|
||||
|say|s|broadcast message to all clients|False|!s \<message\>|Moderator|
|
||||
|setlevel|sl|set client to specified privilege level|True|!sl \<player\> \<level\>|Moderator|
|
||||
|setpassword|sp|set your authentication password|False|!sp \<password\>|Moderator|
|
||||
|tempban|tb|temporarily ban a player for specified time (defaults to 1 hour)|True|!tb \<player\> \<duration (m\|h\|d\|w\|y)\> \<reason\>|Moderator|
|
||||
|unflag|uf|Remove flag for client|True|!uf \<player\>|Moderator|
|
||||
|uptime|up|get current application running time|False|!up |Moderator|
|
||||
|usage|us|get current application memory usage|False|!us |Moderator|
|
||||
|kick|k|kick a player by name|True|!k \<player\> \<reason\>|Trusted|
|
||||
|login|l|login using password|False|!l \<password\>|Trusted|
|
||||
|warn|w|warn player for infringing rules|True|!w \<player\> \<reason\>|Trusted|
|
||||
|warnclear|wc|remove all warning for a player|True|!wc \<player\>|Trusted|
|
||||
|admins|a|list currently connected admins|False|!a |User|
|
||||
|usage|us|get application memory usage|False|!us |Moderator|
|
||||
|balance|bal|balance teams|False|!bal |Trusted|
|
||||
|login|li|login using password|False|!li \<password\>|Trusted|
|
||||
|warn|w|warn client for infringing rules|True|!w \<player\> \<reason\>|Trusted|
|
||||
|warnclear|wc|remove all warnings for a client|True|!wc \<player\>|Trusted|
|
||||
|admins|a|list currently connected privileged clients|False|!a |User|
|
||||
|getexternalip|ip|view your external IP address|False|!ip |User|
|
||||
|help|h|list all available commands|False|!h \<optional command\>|User|
|
||||
|ping|pi|get client's ping|False|!pi \<optional client\>|User|
|
||||
|privatemessage|pm|send message to other player|True|!pm \<player\> \<message\>|User|
|
||||
|report|rep|report a player for suspicious behavior|True|!rep \<player\> \<reason\>|User|
|
||||
|help|h|list all available commands|False|!h \<optional commands\>|User|
|
||||
|mostplayed|mp|view the top 5 dedicated players on the server|False|!mp |User|
|
||||
|owner|iamgod|claim ownership of the server|False|!iamgod |User|
|
||||
|ping|pi|get client's latency|False|!pi \<optional player\>|User|
|
||||
|privatemessage|pm|send message to other client|True|!pm \<player\> \<message\>|User|
|
||||
|report|rep|report a client for suspicious behavior|True|!rep \<player\> \<reason\>|User|
|
||||
|resetstats|rs|reset your stats to factory-new|False|!rs |User|
|
||||
|rules|r|list server rules|False|!r |User|
|
||||
|stats|xlrstats|view your stats|False|!xlrstats \<optional player\>|User|
|
||||
|topstats|ts|view the top 5 players on this server|False|!ts |User|
|
||||
|whoami|who|give information about yourself.|False|!who |User|
|
||||
|topstats|ts|view the top 5 players in this server|False|!ts |User|
|
||||
|whoami|who|give information about yourself|False|!who |User|
|
||||
|
||||
_These commands include all shipped plugin commands._
|
||||
|
||||
@ -192,6 +207,7 @@ ___
|
||||
|resetstats|rs|reset your stats to factory-new|False|!rs |User|
|
||||
|stats|xlrstats|view your stats|False|!xlrstats \<optional player\>|User|
|
||||
|topstats|ts|view the top 5 players on this server|False|!ts |User|
|
||||
|mostplayed|mp|view the top 5 dedicated players on the server|False|!mp |User|
|
||||
|
||||
- To qualify for top stats, a client must have played for at least `1 hour` and connected within the past `30 days`.
|
||||
|
||||
@ -208,6 +224,7 @@ ___
|
||||
#### Profanity Determent
|
||||
- This plugin warns and kicks players for using profanity
|
||||
- Profane words and warning message can be specified in `ProfanityDetermentSettings.json`
|
||||
- If a client's name contains a word listed in the settings, they will immediately be kicked
|
||||
___
|
||||
### Webfront
|
||||
`Home`
|
||||
@ -221,6 +238,7 @@ ___
|
||||
|
||||
`Login`
|
||||
* Allows privileged users to login using their `Client ID` and password set via `setpassword`
|
||||
* `ClientID` is a number that can be found by using `!find <client name>` or find the client on the webfront and copy the ID following `ProfileAsync/`
|
||||
|
||||
`Profile`
|
||||
* Shows a client's information and history
|
||||
@ -231,5 +249,10 @@ ___
|
||||
---
|
||||
|
||||
### Misc
|
||||
#### Anti-cheat
|
||||
This is an [IW4x](https://iw4xcachep26muba.onion.link/) only feature (wider game support planned), that uses analytics to detect aimbots and aim-assist tools.
|
||||
To utilize anti-cheat, enable it during setup **and** copy `_customcallbacks.gsc` from `userraw` into your `IW4x Server\userraw\scripts` folder.
|
||||
The anti-cheat feature is a work in progress and as such will be constantly tweaked and may not be 100% accurate, however the goal is to deter as many cheaters as possible from IW4x.
|
||||
#### Database Storage
|
||||
All **IW4MAdmin** information is stored in `Database.db`. Should you need to reset your database, this file can simply be deleted. Additionally, this file should be preserved during updates to retain client information.
|
||||
By default, all **IW4MAdmin** information is stored in `Database.db`. Should you need to reset your database, this file can simply be deleted. Additionally, this file should be preserved during updates to retain client information.
|
||||
Setting the `ConnectionString` property in `IW4MAdminSettings.json` will cause **IW4MAdmin** to attempt to use a MySQL connection for database storage.
|
@ -29,7 +29,7 @@ namespace SharedLibraryCore
|
||||
|
||||
public String Name { get; private set; }
|
||||
public String Description { get; private set; }
|
||||
public String Syntax => $"{Utilities.CurrentLocalization.LocalizationSet["COMMAND_HELP_SYNTAX"]} !{Alias} {String.Join(" ", Arguments.Select(a => $"<{(a.Required ? "" : Utilities.CurrentLocalization.LocalizationSet["COMMAND_HELP_OPTIONAL"] + " ")}{a.Name}>"))}";
|
||||
public String Syntax => $"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_HELP_SYNTAX"]} !{Alias} {String.Join(" ", Arguments.Select(a => $"<{(a.Required ? "" : Utilities.CurrentLocalization.LocalizationIndex["COMMAND_HELP_OPTIONAL"] + " ")}{a.Name}>"))}";
|
||||
public String Alias { get; private set; }
|
||||
public int RequiredArgumentCount => Arguments.Count(c => c.Required);
|
||||
public bool RequiresTarget { get; private set; }
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -11,13 +11,16 @@ namespace SharedLibraryCore.Configuration
|
||||
public bool EnableMultipleOwners { get; set; }
|
||||
public bool EnableSteppedHierarchy { get; set; }
|
||||
public bool EnableClientVPNs { get; set; }
|
||||
public bool EnableDiscordLink { get; set; }
|
||||
public bool EnableSocialLink { get; set; }
|
||||
public bool EnableCustomSayName { get; set; }
|
||||
public string CustomSayName { get; set; }
|
||||
public string DiscordInviteCode { get; set; }
|
||||
public string SocialLinkAddress { get; set; }
|
||||
public string SocialLinkTitle { get; set; }
|
||||
public string IPHubAPIKey { get; set; }
|
||||
public string WebfrontBindUrl { get; set; }
|
||||
public string CustomParserEncoding { get; set; }
|
||||
public string CustomLocale { get; set; }
|
||||
public string ConnectionString { get; set; }
|
||||
public string Id { get; set; }
|
||||
public List<ServerConfiguration> Servers { get; set; }
|
||||
public int AutoMessagePeriod { get; set; }
|
||||
@ -27,7 +30,7 @@ namespace SharedLibraryCore.Configuration
|
||||
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
var loc = Utilities.CurrentLocalization.LocalizationSet;
|
||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
Id = Guid.NewGuid().ToString();
|
||||
|
||||
EnableWebFront = Utilities.PromptBool(loc["SETUP_ENABLE_WEBFRONT"]);
|
||||
@ -38,7 +41,6 @@ namespace SharedLibraryCore.Configuration
|
||||
bool useCustomParserEncoding = Utilities.PromptBool(loc["SETUP_USE_CUSTOMENCODING"]);
|
||||
CustomParserEncoding = useCustomParserEncoding ? Utilities.PromptString(loc["SETUP_ENCODING_STRING"]) : "windows-1252";
|
||||
|
||||
|
||||
WebfrontBindUrl = "http://127.0.0.1:1624";
|
||||
|
||||
if (EnableCustomSayName)
|
||||
@ -49,10 +51,13 @@ namespace SharedLibraryCore.Configuration
|
||||
if (!EnableClientVPNs)
|
||||
IPHubAPIKey = Utilities.PromptString(loc["SETUP_IPHUB_KEY"]);
|
||||
|
||||
EnableDiscordLink = Utilities.PromptBool(loc["SETUP_DISPLAY_DISCORD"]);
|
||||
EnableSocialLink = Utilities.PromptBool(loc["SETUP_DISPLAY_SOCIAL"]);
|
||||
|
||||
if (EnableDiscordLink)
|
||||
DiscordInviteCode = Utilities.PromptString(loc["SETUP_DISCORD_INVITE"]);
|
||||
if (EnableSocialLink)
|
||||
{
|
||||
SocialLinkTitle = Utilities.PromptString(loc["SETUP_SOCIAL_TITLE"]);
|
||||
SocialLinkAddress = Utilities.PromptString(loc["SETUP_SOCIAL_LINK"]);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SharedLibraryCore.Configuration
|
||||
@ -6,15 +7,45 @@ namespace SharedLibraryCore.Configuration
|
||||
public class ServerConfiguration : IBaseConfiguration
|
||||
{
|
||||
public string IPAddress { get; set; }
|
||||
public short Port { get; set; }
|
||||
public ushort Port { get; set; }
|
||||
public string Password { get; set; }
|
||||
public List<string> Rules { get; set; }
|
||||
public List<string> AutoMessages { get; set; }
|
||||
public bool UseT6MParser { get; set; }
|
||||
public bool UseIW5MParser { get; set; }
|
||||
public string ManualLogPath { get; set; }
|
||||
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
UseT6MParser = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationSet["SETUP_SERVER_USET6M"]);
|
||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
|
||||
while (string.IsNullOrEmpty(IPAddress))
|
||||
{
|
||||
string input = Utilities.PromptString(loc["SETUP_SERVER_IP"]);
|
||||
|
||||
if (System.Net.IPAddress.TryParse(input, out System.Net.IPAddress ip))
|
||||
IPAddress = input;
|
||||
}
|
||||
|
||||
while(Port < 1)
|
||||
{
|
||||
string input = Utilities.PromptString(loc["SETUP_SERVER_PORT"]);
|
||||
if (UInt16.TryParse(input, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.CurrentCulture, out ushort port))
|
||||
Port = port;
|
||||
}
|
||||
|
||||
Password = Utilities.PromptString(loc["SETUP_SERVER_RCON"]);
|
||||
|
||||
AutoMessages = new List<string>();
|
||||
Rules = new List<string>();
|
||||
|
||||
|
||||
UseT6MParser = Utilities.PromptBool(loc["SETUP_SERVER_USET6M"]);
|
||||
if (!UseT6MParser)
|
||||
UseIW5MParser = Utilities.PromptBool(loc["SETUP_SERVER_USEIW5M"]);
|
||||
if (UseIW5MParser)
|
||||
ManualLogPath = Utilities.PromptString(loc["SETUP_SERVER_MANUALLOG"]);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -17,20 +17,35 @@ namespace SharedLibraryCore.Database
|
||||
public DbSet<EFAliasLink> AliasLinks { get; set; }
|
||||
public DbSet<EFPenalty> Penalties { get; set; }
|
||||
|
||||
private static string _ConnectionString;
|
||||
|
||||
public DatabaseContext(DbContextOptions<DatabaseContext> opt) : base(opt) { }
|
||||
|
||||
public DatabaseContext(string connStr)
|
||||
{
|
||||
_ConnectionString = connStr;
|
||||
}
|
||||
|
||||
public DatabaseContext()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
string currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase);
|
||||
var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = $"{currentPath}{Path.DirectorySeparatorChar}Database.db".Substring(6) };
|
||||
var connectionString = connectionStringBuilder.ToString();
|
||||
var connection = new SqliteConnection(connectionString);
|
||||
if (string.IsNullOrEmpty(_ConnectionString))
|
||||
{
|
||||
string currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase);
|
||||
var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = $"{currentPath}{Path.DirectorySeparatorChar}Database.db".Substring(6) };
|
||||
var connectionString = connectionStringBuilder.ToString();
|
||||
var connection = new SqliteConnection(connectionString);
|
||||
|
||||
optionsBuilder.UseSqlite(connection);
|
||||
optionsBuilder.UseSqlite(connection);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
optionsBuilder.UseMySql(_ConnectionString);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -98,9 +113,9 @@ namespace SharedLibraryCore.Database
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var configurations = library.ExportedTypes.Where(c => c.GetInterfaces().FirstOrDefault(i => typeof(IModelConfiguration).IsAssignableFrom(i)) != null)
|
||||
.Select( c => (IModelConfiguration)Activator.CreateInstance(c));
|
||||
.Select(c => (IModelConfiguration)Activator.CreateInstance(c));
|
||||
|
||||
foreach (var configurable in configurations)
|
||||
configurable.Configure(modelBuilder);
|
||||
|
@ -50,6 +50,8 @@ namespace SharedLibraryCore.Database.Models
|
||||
|
||||
[NotMapped]
|
||||
public string IPAddressString => new System.Net.IPAddress(BitConverter.GetBytes(IPAddress)).ToString();
|
||||
[NotMapped]
|
||||
public virtual IDictionary<int, long> LinkedAccounts { get; set; }
|
||||
|
||||
public virtual ICollection<EFPenalty> ReceivedPenalties { get; set; }
|
||||
public virtual ICollection<EFPenalty> AdministeredPenalties { get; set; }
|
||||
|
@ -24,5 +24,6 @@ namespace SharedLibraryCore.Dtos
|
||||
public List<ProfileMeta> Meta { get; set; }
|
||||
public bool Online { get; set; }
|
||||
public string TimeOnline { get; set; }
|
||||
public IDictionary<int, long> LinkedAccounts { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ namespace SharedLibraryCore.Dtos
|
||||
public string GameType { get; set; }
|
||||
public int ClientCount { get; set; }
|
||||
public int MaxClients { get; set; }
|
||||
public ChatInfo[] ChatHistory { get; set; }
|
||||
public List<ChatInfo> ChatHistory { get; set; }
|
||||
public List<PlayerInfo> Players { get; set; }
|
||||
public Helpers.PlayerHistory[] PlayerHistory { get; set; }
|
||||
public int ID { get; set; }
|
||||
|
@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using System.Threading;
|
||||
using SharedLibraryCore.Objects;
|
||||
|
||||
namespace SharedLibraryCore
|
||||
@ -15,6 +12,8 @@ namespace SharedLibraryCore
|
||||
Start,
|
||||
Stop,
|
||||
Connect,
|
||||
// this is for IW5 compatibility
|
||||
Join,
|
||||
Disconnect,
|
||||
Say,
|
||||
MapChange,
|
||||
@ -34,7 +33,8 @@ namespace SharedLibraryCore
|
||||
Command,
|
||||
|
||||
// FROM GAME
|
||||
Script,
|
||||
ScriptDamage,
|
||||
ScriptKill,
|
||||
Kill,
|
||||
Damage,
|
||||
Death,
|
||||
@ -47,10 +47,21 @@ namespace SharedLibraryCore
|
||||
Origin = O;
|
||||
Target = T;
|
||||
Owner = S;
|
||||
OnProcessed = new ManualResetEventSlim();
|
||||
Time = DateTime.UtcNow;
|
||||
CurrentEventId++;
|
||||
Id = CurrentEventId;
|
||||
}
|
||||
|
||||
public GameEvent() { }
|
||||
public GameEvent()
|
||||
{
|
||||
OnProcessed = new ManualResetEventSlim();
|
||||
Time = DateTime.UtcNow;
|
||||
CurrentEventId++;
|
||||
Id = CurrentEventId;
|
||||
}
|
||||
|
||||
private static long CurrentEventId;
|
||||
|
||||
public EventType Type;
|
||||
public string Data; // Data is usually the message sent by player
|
||||
@ -60,5 +71,8 @@ namespace SharedLibraryCore
|
||||
public Server Owner;
|
||||
public Boolean Remote = false;
|
||||
public object Extra { get; set; }
|
||||
public ManualResetEventSlim OnProcessed { get; set; }
|
||||
public DateTime Time { get; private set; }
|
||||
public long Id { get; private set; }
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharedLibraryCore
|
||||
{
|
||||
@ -27,7 +28,12 @@ namespace SharedLibraryCore
|
||||
public override long Length()
|
||||
{
|
||||
Retrieve();
|
||||
return FileCache[0].Length;
|
||||
return FileCache.Sum(l => l.Length);
|
||||
}
|
||||
|
||||
public override Task<string[]> Tail(int lineCount)
|
||||
{
|
||||
return Task.FromResult(FileCache);
|
||||
}
|
||||
|
||||
}
|
||||
|
31
SharedLibraryCore/Helpers/ChangeTracking.cs
Normal file
31
SharedLibraryCore/Helpers/ChangeTracking.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Helpers
|
||||
{
|
||||
public class ChangeTracking
|
||||
{
|
||||
List<string> Values;
|
||||
|
||||
public ChangeTracking()
|
||||
{
|
||||
Values = new List<string>();
|
||||
}
|
||||
|
||||
public void OnChange(ITrackable value)
|
||||
{
|
||||
if (Values.Count > 30)
|
||||
Values.RemoveAt(0);
|
||||
Values.Add($"{DateTime.Now.ToString("HH:mm:ss.fff")} {value.GetTrackableValue()}");
|
||||
}
|
||||
|
||||
public void ClearChanges()
|
||||
{
|
||||
Values.Clear();
|
||||
}
|
||||
|
||||
public string[] GetChanges() => Values.ToArray();
|
||||
}
|
||||
}
|
@ -5,16 +5,16 @@ namespace SharedLibraryCore.Helpers
|
||||
public class MessageToken
|
||||
{
|
||||
public string Name { get; private set; }
|
||||
Func<string> Value;
|
||||
public MessageToken(string Name, Func<string> Value)
|
||||
Func<Server, string> Value;
|
||||
public MessageToken(string Name, Func<Server, string> Value)
|
||||
{
|
||||
this.Name = Name;
|
||||
this.Value = Value;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
||||
public string Process(Server server)
|
||||
{
|
||||
return Value().ToString();
|
||||
return this.Value(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,45 @@ namespace SharedLibraryCore.Helpers
|
||||
return Math.Sqrt(Math.Pow(b.X - a.X, 2) + Math.Pow(b.Y - a.Y, 2) + Math.Pow(b.Z - a.Z, 2));
|
||||
}
|
||||
|
||||
public static double AbsoluteDistance(Vector3 a, Vector3 b)
|
||||
{
|
||||
double deltaX = Math.Abs(b.X -a.X);
|
||||
double deltaY = Math.Abs(b.Y - a.Y);
|
||||
double deltaZ = Math.Abs(b.Z - a.Z);
|
||||
|
||||
// this 'fixes' the roll-over angles
|
||||
double dx = deltaX < 360.0 / 2 ? deltaX : 360.0 - deltaX;
|
||||
double dy = deltaY < 360.0 / 2 ? deltaY : 360.0 - deltaY;
|
||||
double dz = deltaZ < 360.0 / 2 ? deltaZ : 360.0 - deltaZ;
|
||||
|
||||
return Math.Sqrt((dx * dx) + (dy * dy) /*+ (dz * dz)*/);
|
||||
}
|
||||
|
||||
public static double ViewAngleDistance(Vector3 a, Vector3 b, Vector3 c)
|
||||
{
|
||||
double dabX = Math.Abs(a.X - b.X);
|
||||
dabX = dabX < 360.0 / 2 ? dabX : 360.0 - dabX;
|
||||
double dabY = Math.Abs(a.Y - b.Y);
|
||||
dabY = dabY < 360.0 / 2 ? dabY : 360.0 - dabY;
|
||||
|
||||
double dacX = Math.Abs(a.X - c.X);
|
||||
dacX = dacX < 360.0 / 2 ? dacX : 360.0 - dacX;
|
||||
double dacY = Math.Abs(a.Y - c.Y);
|
||||
dacY = dacY < 360.0 / 2 ? dacY : 360.0 - dacY;
|
||||
|
||||
double dbcX = Math.Abs(b.X - c.X);
|
||||
dbcX = dbcX < 360.0 / 2 ? dbcX : 360.0 - dbcX;
|
||||
double dbcY = Math.Abs(b.Y - c.Y);
|
||||
dbcY = dbcY < 360.0 / 2 ? dbcY : 360.0 - dbcY;
|
||||
|
||||
double deltaX = (dabX - dacX - dbcX) / 2.0;
|
||||
deltaX = deltaX < 360.0 / 2 ? deltaX : 360.0 - deltaX;
|
||||
double deltaY = (dabY - dacY - dbcY) / 2.0;
|
||||
deltaY = deltaY < 360.0 / 2 ? deltaY : 360.0 - deltaY;
|
||||
|
||||
return Math.Round(Math.Sqrt((deltaX * deltaX) + (deltaY * deltaY)), 4);
|
||||
}
|
||||
|
||||
public static Vector3 Subtract(Vector3 a, Vector3 b) => new Vector3(b.X - a.X, b.Y - a.Y, b.Z - a.Z);
|
||||
|
||||
public double DotProduct(Vector3 a) => (a.X * this.X) + (a.Y * this.Y) + (a.Z * this.Z);
|
||||
@ -51,6 +90,5 @@ namespace SharedLibraryCore.Helpers
|
||||
public double Magnitude() => Math.Sqrt((X * X) + (Y * Y) + (Z * Z));
|
||||
|
||||
public double AngleBetween(Vector3 a) => Math.Acos(this.DotProduct(a) / (a.Magnitude() * this.Magnitude()));
|
||||
|
||||
}
|
||||
}
|
||||
|
28
SharedLibraryCore/Interfaces/IEventHandler.cs
Normal file
28
SharedLibraryCore/Interfaces/IEventHandler.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// This class handle games events (from log, manual events, etc)
|
||||
/// </summary>
|
||||
public interface IEventHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Add a game event event to the queue to be processed
|
||||
/// </summary>
|
||||
/// <param name="gameEvent">Game event</param>
|
||||
void AddEvent(GameEvent gameEvent);
|
||||
/// <summary>
|
||||
/// Get the next event to be processed
|
||||
/// </summary>
|
||||
/// <returns>Game event that needs to be processed</returns>
|
||||
GameEvent GetNextEvent();
|
||||
/// <summary>
|
||||
/// If an event has output. Like executing a command wait until it's available
|
||||
/// </summary>
|
||||
/// <returns>List of output strings</returns>
|
||||
string[] GetEventOutput();
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// <param name="server">server the event occurred on</param>
|
||||
/// <param name="logLine">single log line string</param>
|
||||
/// <returns></returns>
|
||||
/// todo: make this integrate without needing the server
|
||||
GameEvent GetEvent(Server server, string logLine);
|
||||
/// <summary>
|
||||
/// Get game specific folder prefix for log files
|
||||
|
@ -10,7 +10,7 @@ namespace SharedLibraryCore.Interfaces
|
||||
public interface IManager
|
||||
{
|
||||
Task Init();
|
||||
void Start();
|
||||
Task Start();
|
||||
void Stop();
|
||||
ILogger GetLogger();
|
||||
IList<Server> GetServers();
|
||||
@ -23,6 +23,15 @@ namespace SharedLibraryCore.Interfaces
|
||||
PenaltyService GetPenaltyService();
|
||||
IDictionary<int, Player> GetPrivilegedClients();
|
||||
IEventApi GetEventApi();
|
||||
/// <summary>
|
||||
/// Get the event handlers
|
||||
/// </summary>
|
||||
/// <returns>EventHandler for the manager</returns>
|
||||
IEventHandler GetEventHandler();
|
||||
/// <summary>
|
||||
/// Signal to the manager that event(s) needs to be processed
|
||||
/// </summary>
|
||||
void SetHasEvent();
|
||||
bool ShutdownRequested();
|
||||
}
|
||||
}
|
||||
|
11
SharedLibraryCore/Interfaces/ITrackable.cs
Normal file
11
SharedLibraryCore/Interfaces/ITrackable.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces
|
||||
{
|
||||
public interface ITrackable
|
||||
{
|
||||
string GetTrackableValue();
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
@ -7,6 +8,30 @@ namespace SharedLibraryCore.Localization
|
||||
public class Layout
|
||||
{
|
||||
public string LocalizationName { get; set; }
|
||||
public Dictionary<string, string> LocalizationSet { get; set; }
|
||||
public Index LocalizationIndex { get; set; }
|
||||
|
||||
public Layout(Dictionary<string, string> set)
|
||||
{
|
||||
LocalizationIndex = new Index()
|
||||
{
|
||||
Set = set
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class Index
|
||||
{
|
||||
public Dictionary<string, string> Set { get; set; }
|
||||
|
||||
public string this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Set.TryGetValue(key, out string value))
|
||||
throw new Exception($"Invalid locale key {key}");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
434
SharedLibraryCore/Migrations/20180502195450_Update.Designer.cs
generated
Normal file
434
SharedLibraryCore/Migrations/20180502195450_Update.Designer.cs
generated
Normal file
@ -0,0 +1,434 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using SharedLibraryCore.Database;
|
||||
using SharedLibraryCore.Objects;
|
||||
using System;
|
||||
|
||||
namespace SharedLibraryCore.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20180502195450_Update")]
|
||||
partial class Update
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
|
||||
{
|
||||
b.Property<long>("KillId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("AttackerId");
|
||||
|
||||
b.Property<int>("Damage");
|
||||
|
||||
b.Property<int?>("DeathOriginVector3Id");
|
||||
|
||||
b.Property<int>("DeathType");
|
||||
|
||||
b.Property<int>("HitLoc");
|
||||
|
||||
b.Property<int?>("KillOriginVector3Id");
|
||||
|
||||
b.Property<int>("Map");
|
||||
|
||||
b.Property<int>("ServerId");
|
||||
|
||||
b.Property<int>("VictimId");
|
||||
|
||||
b.Property<int?>("ViewAnglesVector3Id");
|
||||
|
||||
b.Property<int>("Weapon");
|
||||
|
||||
b.Property<DateTime>("When");
|
||||
|
||||
b.HasKey("KillId");
|
||||
|
||||
b.HasIndex("AttackerId");
|
||||
|
||||
b.HasIndex("DeathOriginVector3Id");
|
||||
|
||||
b.HasIndex("KillOriginVector3Id");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.HasIndex("VictimId");
|
||||
|
||||
b.HasIndex("ViewAnglesVector3Id");
|
||||
|
||||
b.ToTable("EFClientKills");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
|
||||
{
|
||||
b.Property<long>("MessageId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("ClientId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<int>("ServerId");
|
||||
|
||||
b.Property<DateTime>("TimeSent");
|
||||
|
||||
b.HasKey("MessageId");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.ToTable("EFClientMessages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
|
||||
{
|
||||
b.Property<int>("ClientId");
|
||||
|
||||
b.Property<int>("ServerId");
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("Deaths");
|
||||
|
||||
b.Property<int>("Kills");
|
||||
|
||||
b.Property<double>("MaxStrain");
|
||||
|
||||
b.Property<double>("SPM");
|
||||
|
||||
b.Property<double>("Skill");
|
||||
|
||||
b.Property<int>("TimePlayed");
|
||||
|
||||
b.HasKey("ClientId", "ServerId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.ToTable("EFClientStatistics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
|
||||
{
|
||||
b.Property<int>("HitLocationCountId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnName("EFClientStatistics_ClientId");
|
||||
|
||||
b.Property<int>("HitCount");
|
||||
|
||||
b.Property<float>("HitOffsetAverage");
|
||||
|
||||
b.Property<int>("Location");
|
||||
|
||||
b.Property<float>("MaxAngleDistance");
|
||||
|
||||
b.Property<int>("ServerId")
|
||||
.HasColumnName("EFClientStatistics_ServerId");
|
||||
|
||||
b.HasKey("HitLocationCountId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.HasIndex("ClientId", "ServerId");
|
||||
|
||||
b.ToTable("EFHitLocationCounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b =>
|
||||
{
|
||||
b.Property<int>("ServerId");
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("Port");
|
||||
|
||||
b.HasKey("ServerId");
|
||||
|
||||
b.ToTable("EFServers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
|
||||
{
|
||||
b.Property<int>("StatisticId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("ServerId");
|
||||
|
||||
b.Property<long>("TotalKills");
|
||||
|
||||
b.Property<long>("TotalPlayTime");
|
||||
|
||||
b.HasKey("StatisticId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.ToTable("EFServerStatistics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
|
||||
{
|
||||
b.Property<int>("AliasId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<DateTime>("DateAdded");
|
||||
|
||||
b.Property<int>("IPAddress");
|
||||
|
||||
b.Property<int>("LinkId");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("AliasId");
|
||||
|
||||
b.HasIndex("LinkId");
|
||||
|
||||
b.ToTable("EFAlias");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b =>
|
||||
{
|
||||
b.Property<int>("AliasLinkId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.HasKey("AliasLinkId");
|
||||
|
||||
b.ToTable("EFAliasLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
|
||||
{
|
||||
b.Property<int>("ClientId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("AliasLinkId");
|
||||
|
||||
b.Property<int>("Connections");
|
||||
|
||||
b.Property<int>("CurrentAliasId");
|
||||
|
||||
b.Property<DateTime>("FirstConnection");
|
||||
|
||||
b.Property<DateTime>("LastConnection");
|
||||
|
||||
b.Property<int>("Level");
|
||||
|
||||
b.Property<bool>("Masked");
|
||||
|
||||
b.Property<long>("NetworkId");
|
||||
|
||||
b.Property<string>("Password");
|
||||
|
||||
b.Property<string>("PasswordSalt");
|
||||
|
||||
b.Property<int>("TotalConnectionTime");
|
||||
|
||||
b.HasKey("ClientId");
|
||||
|
||||
b.HasIndex("AliasLinkId");
|
||||
|
||||
b.HasIndex("CurrentAliasId");
|
||||
|
||||
b.HasIndex("NetworkId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("EFClients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
|
||||
{
|
||||
b.Property<int>("PenaltyId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<DateTime>("Expires");
|
||||
|
||||
b.Property<int>("LinkId");
|
||||
|
||||
b.Property<int>("OffenderId");
|
||||
|
||||
b.Property<string>("Offense")
|
||||
.IsRequired();
|
||||
|
||||
b.Property<int>("PunisherId");
|
||||
|
||||
b.Property<int>("Type");
|
||||
|
||||
b.Property<DateTime>("When");
|
||||
|
||||
b.HasKey("PenaltyId");
|
||||
|
||||
b.HasIndex("LinkId");
|
||||
|
||||
b.HasIndex("OffenderId");
|
||||
|
||||
b.HasIndex("PunisherId");
|
||||
|
||||
b.ToTable("EFPenalties");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b =>
|
||||
{
|
||||
b.Property<int>("Vector3Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<float>("X");
|
||||
|
||||
b.Property<float>("Y");
|
||||
|
||||
b.Property<float>("Z");
|
||||
|
||||
b.HasKey("Vector3Id");
|
||||
|
||||
b.ToTable("Vector3");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker")
|
||||
.WithMany()
|
||||
.HasForeignKey("AttackerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeathOriginVector3Id");
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin")
|
||||
.WithMany()
|
||||
.HasForeignKey("KillOriginVector3Id");
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim")
|
||||
.WithMany()
|
||||
.HasForeignKey("VictimId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles")
|
||||
.WithMany()
|
||||
.HasForeignKey("ViewAnglesVector3Id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics")
|
||||
.WithMany("HitLocations")
|
||||
.HasForeignKey("ClientId", "ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
|
||||
{
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("LinkId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("AliasLinkId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias")
|
||||
.WithMany()
|
||||
.HasForeignKey("CurrentAliasId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
|
||||
.WithMany("ReceivedPenalties")
|
||||
.HasForeignKey("LinkId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender")
|
||||
.WithMany("ReceivedPenalties")
|
||||
.HasForeignKey("OffenderId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher")
|
||||
.WithMany("AdministeredPenalties")
|
||||
.HasForeignKey("PunisherId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
35
SharedLibraryCore/Migrations/20180502195450_Update.cs
Normal file
35
SharedLibraryCore/Migrations/20180502195450_Update.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SharedLibraryCore.Migrations
|
||||
{
|
||||
public partial class Update : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<float>(
|
||||
name: "MaxAngleDistance",
|
||||
table: "EFHitLocationCounts",
|
||||
nullable: false,
|
||||
defaultValue: 0f);
|
||||
|
||||
migrationBuilder.AddColumn<double>(
|
||||
name: "MaxStrain",
|
||||
table: "EFClientStatistics",
|
||||
nullable: false,
|
||||
defaultValue: 0.0);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxAngleDistance",
|
||||
table: "EFHitLocationCounts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxStrain",
|
||||
table: "EFClientStatistics");
|
||||
}
|
||||
}
|
||||
}
|
436
SharedLibraryCore/Migrations/20180516023249_AddEloField.Designer.cs
generated
Normal file
436
SharedLibraryCore/Migrations/20180516023249_AddEloField.Designer.cs
generated
Normal file
@ -0,0 +1,436 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using SharedLibraryCore.Database;
|
||||
using SharedLibraryCore.Objects;
|
||||
using System;
|
||||
|
||||
namespace SharedLibraryCore.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20180516023249_AddEloField")]
|
||||
partial class AddEloField
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
|
||||
{
|
||||
b.Property<long>("KillId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("AttackerId");
|
||||
|
||||
b.Property<int>("Damage");
|
||||
|
||||
b.Property<int?>("DeathOriginVector3Id");
|
||||
|
||||
b.Property<int>("DeathType");
|
||||
|
||||
b.Property<int>("HitLoc");
|
||||
|
||||
b.Property<int?>("KillOriginVector3Id");
|
||||
|
||||
b.Property<int>("Map");
|
||||
|
||||
b.Property<int>("ServerId");
|
||||
|
||||
b.Property<int>("VictimId");
|
||||
|
||||
b.Property<int?>("ViewAnglesVector3Id");
|
||||
|
||||
b.Property<int>("Weapon");
|
||||
|
||||
b.Property<DateTime>("When");
|
||||
|
||||
b.HasKey("KillId");
|
||||
|
||||
b.HasIndex("AttackerId");
|
||||
|
||||
b.HasIndex("DeathOriginVector3Id");
|
||||
|
||||
b.HasIndex("KillOriginVector3Id");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.HasIndex("VictimId");
|
||||
|
||||
b.HasIndex("ViewAnglesVector3Id");
|
||||
|
||||
b.ToTable("EFClientKills");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
|
||||
{
|
||||
b.Property<long>("MessageId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("ClientId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<int>("ServerId");
|
||||
|
||||
b.Property<DateTime>("TimeSent");
|
||||
|
||||
b.HasKey("MessageId");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.ToTable("EFClientMessages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
|
||||
{
|
||||
b.Property<int>("ClientId");
|
||||
|
||||
b.Property<int>("ServerId");
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("Deaths");
|
||||
|
||||
b.Property<double>("EloRating");
|
||||
|
||||
b.Property<int>("Kills");
|
||||
|
||||
b.Property<double>("MaxStrain");
|
||||
|
||||
b.Property<double>("SPM");
|
||||
|
||||
b.Property<double>("Skill");
|
||||
|
||||
b.Property<int>("TimePlayed");
|
||||
|
||||
b.HasKey("ClientId", "ServerId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.ToTable("EFClientStatistics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
|
||||
{
|
||||
b.Property<int>("HitLocationCountId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnName("EFClientStatistics_ClientId");
|
||||
|
||||
b.Property<int>("HitCount");
|
||||
|
||||
b.Property<float>("HitOffsetAverage");
|
||||
|
||||
b.Property<int>("Location");
|
||||
|
||||
b.Property<float>("MaxAngleDistance");
|
||||
|
||||
b.Property<int>("ServerId")
|
||||
.HasColumnName("EFClientStatistics_ServerId");
|
||||
|
||||
b.HasKey("HitLocationCountId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.HasIndex("ClientId", "ServerId");
|
||||
|
||||
b.ToTable("EFHitLocationCounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b =>
|
||||
{
|
||||
b.Property<int>("ServerId");
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("Port");
|
||||
|
||||
b.HasKey("ServerId");
|
||||
|
||||
b.ToTable("EFServers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
|
||||
{
|
||||
b.Property<int>("StatisticId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("ServerId");
|
||||
|
||||
b.Property<long>("TotalKills");
|
||||
|
||||
b.Property<long>("TotalPlayTime");
|
||||
|
||||
b.HasKey("StatisticId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.ToTable("EFServerStatistics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
|
||||
{
|
||||
b.Property<int>("AliasId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<DateTime>("DateAdded");
|
||||
|
||||
b.Property<int>("IPAddress");
|
||||
|
||||
b.Property<int>("LinkId");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("AliasId");
|
||||
|
||||
b.HasIndex("LinkId");
|
||||
|
||||
b.ToTable("EFAlias");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b =>
|
||||
{
|
||||
b.Property<int>("AliasLinkId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.HasKey("AliasLinkId");
|
||||
|
||||
b.ToTable("EFAliasLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
|
||||
{
|
||||
b.Property<int>("ClientId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<int>("AliasLinkId");
|
||||
|
||||
b.Property<int>("Connections");
|
||||
|
||||
b.Property<int>("CurrentAliasId");
|
||||
|
||||
b.Property<DateTime>("FirstConnection");
|
||||
|
||||
b.Property<DateTime>("LastConnection");
|
||||
|
||||
b.Property<int>("Level");
|
||||
|
||||
b.Property<bool>("Masked");
|
||||
|
||||
b.Property<long>("NetworkId");
|
||||
|
||||
b.Property<string>("Password");
|
||||
|
||||
b.Property<string>("PasswordSalt");
|
||||
|
||||
b.Property<int>("TotalConnectionTime");
|
||||
|
||||
b.HasKey("ClientId");
|
||||
|
||||
b.HasIndex("AliasLinkId");
|
||||
|
||||
b.HasIndex("CurrentAliasId");
|
||||
|
||||
b.HasIndex("NetworkId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("EFClients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
|
||||
{
|
||||
b.Property<int>("PenaltyId")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Active");
|
||||
|
||||
b.Property<DateTime>("Expires");
|
||||
|
||||
b.Property<int>("LinkId");
|
||||
|
||||
b.Property<int>("OffenderId");
|
||||
|
||||
b.Property<string>("Offense")
|
||||
.IsRequired();
|
||||
|
||||
b.Property<int>("PunisherId");
|
||||
|
||||
b.Property<int>("Type");
|
||||
|
||||
b.Property<DateTime>("When");
|
||||
|
||||
b.HasKey("PenaltyId");
|
||||
|
||||
b.HasIndex("LinkId");
|
||||
|
||||
b.HasIndex("OffenderId");
|
||||
|
||||
b.HasIndex("PunisherId");
|
||||
|
||||
b.ToTable("EFPenalties");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b =>
|
||||
{
|
||||
b.Property<int>("Vector3Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<float>("X");
|
||||
|
||||
b.Property<float>("Y");
|
||||
|
||||
b.Property<float>("Z");
|
||||
|
||||
b.HasKey("Vector3Id");
|
||||
|
||||
b.ToTable("Vector3");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker")
|
||||
.WithMany()
|
||||
.HasForeignKey("AttackerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeathOriginVector3Id");
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin")
|
||||
.WithMany()
|
||||
.HasForeignKey("KillOriginVector3Id");
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim")
|
||||
.WithMany()
|
||||
.HasForeignKey("VictimId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles")
|
||||
.WithMany()
|
||||
.HasForeignKey("ViewAnglesVector3Id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics")
|
||||
.WithMany("HitLocations")
|
||||
.HasForeignKey("ClientId", "ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
|
||||
{
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("LinkId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("AliasLinkId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias")
|
||||
.WithMany()
|
||||
.HasForeignKey("CurrentAliasId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
|
||||
.WithMany("ReceivedPenalties")
|
||||
.HasForeignKey("LinkId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender")
|
||||
.WithMany("ReceivedPenalties")
|
||||
.HasForeignKey("OffenderId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher")
|
||||
.WithMany("AdministeredPenalties")
|
||||
.HasForeignKey("PunisherId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user