Compare commits

...

26 Commits
2.0 ... 2.1

Author SHA1 Message Date
7ec02499a6 [application] update readme & branch for 2.1 2018-05-24 21:39:52 -05:00
897ec0d0c1 [master] make version info update live instead of requiring a restart in 2018-05-24 21:39:03 -05:00
d9a601328c stats tweaked to scale SPM based on team size
invalid client id results in 404 rather than exception page
performance based on traditional elo rating
fixed @ (broadcast commands)
added reports to penalty list and profile
2018-05-24 14:48:57 -05:00
36d493f05b update file localizations
update custom callbacks
add server count to master
add most played to token list
2018-05-21 16:09:27 -05:00
be68335f70 update change tracking and elo
master shows monitoring server count
master can provide individual localizations
2018-05-20 21:35:56 -05:00
4d585e6ab2 set default elo rating
maybe fix deadlock again :c
changed "skill" to Performance (Skill + Elo / 2)
2018-05-17 18:31:58 -05:00
4006c09045 add most played command
hopefully fixed thread lock?
started work on elo rating
2018-05-15 23:57:37 -05:00
699c19cd4b adding Cod4 support (for steam GUID is truncated to 16 characters)
exit properly whoops
add all linked accounts to drop down
consolidate linked admin accounts to the most recently seen one
limited some waits to 5s to hopefully prevent a rare thread lock
2018-05-14 12:55:10 -05:00
6e5501b32d fix T6 reading
add WaW support
fix stats threading
2018-05-10 23:52:20 -05:00
e964013700 lots of fixes :) 2018-05-10 00:34:29 -05:00
9ff7f39e8d SPM fix for negative/teamdamage
added localization as downloaded from the Master API
interupted network communication no longer treated as unknown exception
topstats prints the right message if no one qualifies
angle adjustments
move unflag to seperate command
2018-05-07 23:58:46 -05:00
a54ea3913d add translation for webfront
discord link has been genericized to social link
2018-05-05 17:52:04 -05:00
e8dff01c41 re-added the kill server command (can only be used if run as admin)
less warns when using a disposed socket
topstats added to tokens as {{TOPSTATS}}
fixed topstats reporting for only a single server
added fix to iw4 regex for negative score
tokens now support multiple lines (using Environment.NewLine to separate)
localization includes culture again
2018-05-05 15:36:26 -05:00
3092a529e9 add penalties for angle info
queue Tell/Say to prevent plugins from throwing exception when server is offlline
fixed CPU usage issue over time
sort penalties by type on webfront
2018-05-03 23:22:10 -05:00
f442f251f6 more stat SPM fixes
prevent null say event from executing when exiting
adjusted rcon and socket timeout
fixed bug with login/setpassword not working after claiming ownership
2018-05-03 00:25:49 -05:00
3a463be7f8 Profanity deterrent kick players with offensive names
status parsing with Regex in IW4 is much cleaner
fixed tempban not always kicking
made plugin event tasks parallel
2018-04-29 15:44:04 -05:00
35e7f57156 fixed up IW5 parser with new event system
changed login alias to li (duplicate)
fixed crashing bug in generic repo
fixed anonymous name in access to web console
2018-04-28 20:11:13 -05:00
8071fb37bc SPM and skill is rounded in profile now
fixed web console not waiting for reponse
fixed password not saving over time
web users level update properly now when promoted/demoted
2018-04-28 16:39:45 -05:00
bb90a807b7 moved heartbeat to timer instead of manual task/thread
GameEventHandler uses ConcurrentQueue for events
exception handlers for events and log reading
added IW4ScriptCommands plugin
fixed stats
lots of little fixes
2018-04-28 00:22:18 -05:00
2c2c442ba7 updated portuguese translation
fixed issue with locale when no config present
changed kick color on webfront
aliased owner to iamgod (for b3 familiar users)
hopefully fixed stats issue
added T5M (V2 BO2) support
made dvar grab at beginning minimal to prevent throttling on older CODS
2018-04-26 19:19:42 -05:00
b6c979beba fixed base controller Manager being null
fixed log reading duplicates with new event processing
added portuguese translation
2018-04-26 15:26:03 -05:00
99390f1f35 fixed issue with status response erroring when incorrect length
view angle vector parse fail is now a handled exception
change local host check to byte array to make it faster than comparing string
kick command now requires moderator level or higher
tempban now requires administrator level or higher
hopefully fixed negative SPM bug
pipelined the events and consolidated them to run through GameEventHandler
uniform console colors
2018-04-26 01:13:04 -05:00
ece519251a added MySQL support
fixed login bug
IW3 official support
2018-04-25 01:38:59 -05:00
0e3d280595 more localization
fixed issue with IW4 parser not reading map changes properly
2018-04-24 17:01:27 -05:00
5dfaa4ebd6 update projects to .NET Core 2.0.7
added instance and client count to api page
removed vestigial ConfigGenerator
2018-04-23 16:03:50 -05:00
02ef5a0bf8 adding IW5m parsers
reduce status polling rate
adding preliminary russian localization
small rcon tweak to attempt to send custom encoded messages
removed exception handling in ConvertLong
throttled servers will still attempt to execute events
2018-04-23 00:43:48 -05:00
144 changed files with 6433 additions and 1640 deletions

View File

@ -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,

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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">

View File

@ -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\"

View File

@ -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"

View File

@ -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;
}
}
}

View File

@ -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?"

View 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";
}
}

View File

@ -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()
};
}

View File

@ -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);
}
}
}

View 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";
}
}

View File

@ -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";
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View File

@ -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,
};
}
}
}

View File

@ -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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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": "Эффективность"
}
}
}

View File

@ -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

View File

@ -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()

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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;
}
}

View 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;
}
}
}

View File

@ -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);
}
}

View File

@ -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()));
}
}
}

View File

@ -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}

View File

@ -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\">

View File

@ -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

View File

@ -1,4 +1,4 @@
{
"current-version-stable": 2.0,
"current-version-prerelease": 2.0
"current-version-stable": 2.1,
"current-version-prerelease": 2.1
}

View File

@ -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()):

View File

@ -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,

View File

@ -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

View 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

View File

@ -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']

View File

@ -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>')

View File

@ -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)

View File

@ -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">&mdash; {{client_count}} clients</span>
<span class="h4 text-muted">&mdash; {{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 %}

View File

@ -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>

View File

@ -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']
)

View 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");
}
}
}

View 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 &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
</Target>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
</ItemGroup>
</Project>

View 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;
}
}

View File

@ -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"]);
}
}
}

View File

@ -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 &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
</Target>

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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 &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
</Target>

View File

@ -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
};
}

View File

@ -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; }
}
}

View 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();
}
}
}

View 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);
}
}

View File

@ -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)

View 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);
}
}
}
}

View File

@ -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"]);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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 };
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>();
}
}
}

View File

@ -8,6 +8,13 @@ namespace IW4MAdmin.Plugins.Stats
{
public class IW4Info
{
public enum Team
{
Spectator,
Axis,
Allies
}
public enum MeansOfDeath
{
NONE,

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }

View File

@ -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()
{

View File

@ -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 &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
</Target>

View File

@ -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)
{

View File

@ -19,4 +19,8 @@
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
</ItemGroup>
</Project>

View File

@ -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)
{

View File

@ -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 &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;&#xD;&#xA;copy &quot;$(ProjectDir)MaxMind\GeoIP.dat&quot; &quot;$(SolutionDir)BUILD\Plugins\GeoIP.dat&quot;" />
</Target>

View File

@ -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;
}

View File

@ -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}}` &mdash; displays how many players have connected
* `{{TOPSTATS}}` &mdash; displays the top 5 players on the server based on performance
* `{{MOSTPLAYED}}` &mdash; displays the top 5 players based on number of kills
* `{{TOTALPLAYTIME}}` &mdash; displays the cumulative play time (in man-hours) on all monitored servers
* `{{VERSION}}` &mdash; 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.

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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; }

View File

@ -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; }
}
}

View File

@ -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; }

View File

@ -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; }
}
}

View File

@ -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);
}
}

View 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();
}
}

View File

@ -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);
}
}
}

View File

@ -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()));
}
}

View 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();
}
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SharedLibraryCore.Interfaces
{
public interface ITrackable
{
string GetTrackableValue();
}
}

View File

@ -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;
}
}
}
}

View 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
}
}
}

View 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");
}
}
}

View 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