Compare commits

..

1 Commits
2.3 ... 2.1

Author SHA1 Message Date
7ec02499a6 [application] update readme & branch for 2.1 2018-05-24 21:39:52 -05:00
457 changed files with 8512 additions and 40106 deletions

12
.gitignore vendored
View File

@ -220,17 +220,7 @@ Thumbs.db
DEPLOY
global.min.css
global.min.js
bootstrap-custom.css
bootstrap-custom.min.css
**/Master/static
**/Master/dev_env
/WebfrontCore/Views/Plugins/*
/WebfrontCore/wwwroot/**/dds
/DiscordWebhook/env
/DiscordWebhook/config.dev.json
/GameLogServer/env
launchSettings.json
/VpnDetectionPrivate.js
/Plugins/ScriptPlugins/VpnDetectionPrivate.js
**/Master/env_master
/GameLogServer/log_env

View File

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Application.API
{
class EventApi : IEventApi
{
Queue<EventInfo> Events = new Queue<EventInfo>();
DateTime LastFlagEvent;
static string[] FlaggedMessageContains =
{
" wh ",
"hax",
"cheat",
" hack ",
"aim",
"wall",
"cheto",
"hak",
"bot"
};
int FlaggedMessageCount;
public Queue<EventInfo> GetEvents() => Events;
public void OnServerEvent(object sender, GameEvent E)
{
if (E.Type == GameEvent.EventType.Say && E.Origin.Level < Player.Permission.Trusted)
{
bool flaggedMessage = false;
foreach (string msg in FlaggedMessageContains)
flaggedMessage = flaggedMessage ? flaggedMessage : E.Data.ToLower().Contains(msg);
if (flaggedMessage)
FlaggedMessageCount++;
if (FlaggedMessageCount > 3)
{
if (Events.Count > 20)
Events.Dequeue();
FlaggedMessageCount = 0;
E.Owner.Broadcast(Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_REPORT"]).Wait(5000);
Events.Enqueue(new EventInfo(
EventInfo.EventType.ALERT,
EventInfo.EventVersion.IW4MAdmin,
"Chat indicates there may be a cheater",
"Alert",
E.Owner.Hostname, ""));
}
if ((DateTime.UtcNow - LastFlagEvent).Minutes >= 3)
{
FlaggedMessageCount = 0;
LastFlagEvent = DateTime.Now;
}
}
if (E.Type == GameEvent.EventType.Report)
{
Events.Enqueue(new EventInfo(
EventInfo.EventType.ALERT,
EventInfo.EventVersion.IW4MAdmin,
$"**{E.Origin.Name}** has reported **{E.Target.Name}** for: {E.Data.Trim()}",
E.Target.Name, E.Origin.Name, ""));
}
}
}
}

View File

@ -1,12 +0,0 @@
using System.Threading.Tasks;
using RestEase;
namespace IW4MAdmin.Application.API.GameLogServer
{
[Header("User-Agent", "IW4MAdmin-RestEase")]
public interface IGameLogServer
{
[Get("log/{path}/{key}")]
Task<LogInfo> Log([Path] string path, [Path] string key);
}
}

View File

@ -1,19 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application.API.GameLogServer
{
public class LogInfo
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("length")]
public int Length { get; set; }
[JsonProperty("data")]
public string Data { get; set; }
[JsonProperty("next_key")]
public string NextKey { get; set; }
}
}

View File

@ -8,13 +8,9 @@ namespace IW4MAdmin.Application.API.Master
public class ApiServer
{
[JsonProperty("id")]
public long Id { get; set; }
[JsonProperty("ip")]
public string IPAddress { get; set; }
public int Id { get; set; }
[JsonProperty("port")]
public short Port { get; set; }
[JsonProperty("version")]
public string Version { get; set; }
[JsonProperty("gametype")]
public string Gametype { get; set; }
[JsonProperty("map")]

View File

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using RestEase;
using SharedLibraryCore;
@ -12,7 +11,6 @@ namespace IW4MAdmin.Application.API.Master
public class HeartbeatState
{
public bool Connected { get; set; }
public CancellationToken Token { get; set; }
}
public class Heartbeat
@ -41,14 +39,12 @@ namespace IW4MAdmin.Application.API.Master
{
ClientNum = s.ClientNum,
Game = s.GameName.ToString(),
Version = s.Version,
Gametype = s.Gametype,
Hostname = s.Hostname,
Map = s.CurrentMap.Name,
MaxClientNum = s.MaxClients,
Id = s.EndPoint,
Port = (short)s.Port,
IPAddress = s.IP
Id = s.GetHashCode(),
Port = (short)s.GetPort()
}).ToList()
};

View File

@ -36,7 +36,7 @@ namespace IW4MAdmin.Application.API.Master
public class Endpoint
{
#if !DEBUG
private static readonly IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
private static IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
#else
private static IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
#endif

View File

@ -2,16 +2,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp2.0</TargetFramework>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2.2.8.4</Version>
<Version>2.1.0</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Product>IW4MAdmin</Product>
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated servers</Description>
<Copyright>2019</Copyright>
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated server</Description>
<Copyright>2018</Copyright>
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl>
<RepositoryUrl>https://github.com/RaidMax/IW4M-Admin</RepositoryUrl>
@ -21,21 +20,15 @@
<Configurations>Debug;Release;Prerelease</Configurations>
<Win32Resource />
<RootNamespace>IW4MAdmin.Application</RootNamespace>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RestEase" Version="1.4.9" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" />
<PackageReference Include="RestEase" Version="1.4.5" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.4.0" />
</ItemGroup>
<PropertyGroup>
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredCompilation>true</TieredCompilation>
<AssemblyVersion>2.2.8.4</AssemblyVersion>
<FileVersion>2.2.8.4</FileVersion>
<LangVersion>7.1</LangVersion>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
<ItemGroup>
@ -48,24 +41,53 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\IW4MAdmin.en-US.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>IW4MAdmin.en-US.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\IW4MAdmin.en-US.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>IW4MAdmin.en-US.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Update="DefaultSettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Localization\IW4MAdmin.en-US.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Localization\IW4MAdmin.es-EC.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Localization\IW4MAdmin.pt-BR.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Localization\IW4MAdmin.ru-RU.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="call $(ProjectDir)BuildScripts\PreBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" />
<Exec Command="call $(ProjectDir)BuildScripts\PreBuild.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
</Target>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
<Output TaskParameter="Assemblies" ItemName="CurrentAssembly" />
</GetAssemblyIdentity>
<Exec Command="call $(ProjectDir)BuildScripts\PostBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir) %(CurrentAssembly.Version)" />
<Exec Command="call $(ProjectDir)BuildScripts\PostBuild.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
</Target>
<Target Name="PostPublish" AfterTargets="Publish">
<Exec Command="call $(ProjectDir)BuildScripts\PostPublish.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(ConfigurationName)" />
<Exec Command="call $(ProjectDir)BuildScripts\PostPublish.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
</Target>
</Project>

View File

@ -1,763 +0,0 @@
using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RconParsers;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Events;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Services;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static SharedLibraryCore.GameEvent;
namespace IW4MAdmin.Application
{
public class ApplicationManager : IManager
{
private ConcurrentBag<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
public ILogger Logger => GetLogger(0);
public bool Running { get; private set; }
public bool IsInitialized { get; private set; }
// expose the event handler so we can execute the events
public OnServerEventEventHandler OnServerEvent { get; set; }
public DateTime StartTime { get; private set; }
public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString();
public IList<IRConParser> AdditionalRConParsers { get; }
public IList<IEventParser> AdditionalEventParsers { get; }
public ITokenAuthentication TokenAuthenticator { get; }
public CancellationToken CancellationToken => _tokenSource.Token;
public string ExternalIPAddress { get; private set; }
public bool IsRestartRequested { get; private set; }
static ApplicationManager Instance;
List<Command> Commands;
readonly List<MessageToken> MessageTokens;
ClientService ClientSvc;
readonly AliasService AliasSvc;
readonly PenaltyService PenaltySvc;
public BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
GameEventHandler Handler;
readonly IPageList PageList;
readonly Dictionary<long, ILogger> Loggers = new Dictionary<long, ILogger>();
private readonly MetaService _metaService;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource;
private ApplicationManager()
{
_servers = new ConcurrentBag<Server>();
Commands = new List<Command>();
MessageTokens = new List<MessageToken>();
ClientSvc = new ClientService();
AliasSvc = new AliasService();
PenaltySvc = new PenaltyService();
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
StartTime = DateTime.UtcNow;
PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>();
AdditionalRConParsers = new List<IRConParser>();
OnServerEvent += OnGameEvent;
OnServerEvent += EventApi.OnGameEvent;
TokenAuthenticator = new TokenAuthentication();
_metaService = new MetaService();
_tokenSource = new CancellationTokenSource();
}
private async void OnGameEvent(object sender, GameEventArgs args)
{
#if DEBUG == true
Logger.WriteDebug($"Entering event process for {args.Event.Id}");
#endif
var newEvent = args.Event;
// the event has failed already
if (newEvent.Failed)
{
goto skip;
}
try
{
await newEvent.Owner.ExecuteEvent(newEvent);
// save the event info to the database
var changeHistorySvc = new ChangeHistoryService();
await changeHistorySvc.Add(args.Event);
#if DEBUG
Logger.WriteDebug($"Processed event with id {newEvent.Id}");
#endif
}
// this happens if a plugin requires login
catch (AuthorizationException ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Permission;
newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {ex.Message}");
}
catch (NetworkException ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Exception;
Logger.WriteError(ex.Message);
Logger.WriteDebug(ex.GetExceptionInfo());
}
catch (ServerException ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Exception;
Logger.WriteWarning(ex.Message);
}
catch (Exception ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Exception;
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"].FormatExt(newEvent.Owner));
Logger.WriteDebug(ex.GetExceptionInfo());
}
skip:
// tell anyone waiting for the output that we're done
newEvent.OnProcessed.Set();
}
public IList<Server> GetServers()
{
return Servers;
}
public IList<Command> GetCommands()
{
return Commands;
}
public static ApplicationManager GetInstance()
{
return Instance ?? (Instance = new ApplicationManager());
}
public async Task UpdateServerStates()
{
// store the server hash code and task for it
var runningUpdateTasks = new Dictionary<long, Task>();
while (!_tokenSource.IsCancellationRequested)
{
// select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks
.Where(ut => ut.Value.Status == TaskStatus.RanToCompletion ||
ut.Value.Status == TaskStatus.Canceled ||
ut.Value.Status == TaskStatus.Faulted)
.Select(ut => ut.Key)
.ToList();
// this is to prevent the log reader from starting before the initial
// query of players on the server
if (serverTasksToRemove.Count > 0)
{
IsInitialized = true;
}
// remove the update tasks as they have completd
foreach (long serverId in serverTasksToRemove)
{
runningUpdateTasks.Remove(serverId);
}
// select the servers where the tasks have completed
var serverIds = Servers.Select(s => s.EndPoint).Except(runningUpdateTasks.Select(r => r.Key)).ToList();
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
{
runningUpdateTasks.Add(server.EndPoint, Task.Run(async () =>
{
try
{
await server.ProcessUpdatesAsync(_tokenSource.Token);
if (server.Throttled)
{
await Task.Delay((int)_throttleTimeout.TotalMilliseconds, _tokenSource.Token);
}
}
catch (Exception e)
{
Logger.WriteWarning($"Failed to update status for {server}");
Logger.WriteDebug(e.GetExceptionInfo());
}
finally
{
server.IsInitialized = true;
}
}));
}
#if DEBUG
Logger.WriteDebug($"{runningUpdateTasks.Count} servers queued for stats updates");
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
try
{
await Task.Delay(ConfigHandler.Configuration().RConPollRate, _tokenSource.Token);
}
// if a cancellation is received, we want to return immediately after shutting down
catch
{
foreach (var server in Servers.Where(s => serverIds.Contains(s.EndPoint)))
{
await server.ProcessUpdatesAsync(_tokenSource.Token);
}
break;
}
}
}
public async Task Init()
{
Running = true;
ExternalIPAddress = await Utilities.GetExternalIP();
#region PLUGINS
SharedLibraryCore.Plugins.PluginImporter.Load(this);
foreach (var Plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
{
try
{
await Plugin.OnLoadAsync(this);
}
catch (Exception ex)
{
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_PLUGIN"]} {Plugin.Name}");
Logger.WriteDebug(ex.GetExceptionInfo());
}
}
#endregion
#region CONFIG
var config = ConfigHandler.Configuration();
// copy over default config if it doesn't exist
if (config == null)
{
var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration();
ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
var newConfig = ConfigHandler.Configuration();
newConfig.AutoMessages = defaultConfig.AutoMessages;
newConfig.GlobalRules = defaultConfig.GlobalRules;
newConfig.Maps = defaultConfig.Maps;
newConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
newConfig.QuickMessages = defaultConfig.QuickMessages;
if (newConfig.Servers == null)
{
ConfigHandler.Set(newConfig);
newConfig.Servers = new List<ServerConfiguration>();
do
{
var serverConfig = new ServerConfiguration();
foreach (var parser in AdditionalRConParsers)
{
serverConfig.AddRConParser(parser);
}
foreach (var parser in AdditionalEventParsers)
{
serverConfig.AddEventParser(parser);
}
newConfig.Servers.Add((ServerConfiguration)serverConfig.Generate());
} while (Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["SETUP_SERVER_SAVE"]));
config = newConfig;
await ConfigHandler.Save();
}
}
else
{
if (string.IsNullOrEmpty(config.Id))
{
config.Id = Guid.NewGuid().ToString();
await ConfigHandler.Save();
}
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
{
config.WebfrontBindUrl = "http://0.0.0.0:1624";
await ConfigHandler.Save();
}
foreach (var serverConfig in config.Servers)
{
Migration.ConfigurationMigration.ModifyLogPath020919(serverConfig);
if (serverConfig.RConParserVersion == null || serverConfig.EventParserVersion == null)
{
foreach (var parser in AdditionalRConParsers)
{
serverConfig.AddRConParser(parser);
}
foreach (var parser in AdditionalEventParsers)
{
serverConfig.AddEventParser(parser);
}
serverConfig.ModifyParsers();
}
await ConfigHandler.Save();
}
}
if (config.Servers.Count == 0)
{
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
}
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252");
#endregion
#region DATABASE
using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString,
GetApplicationSettings().Configuration()?.DatabaseProvider))
{
await new ContextSeed(db).Seed();
}
#endregion
#region COMMANDS
if (ClientSvc.GetOwners().Result.Count == 0)
{
Commands.Add(new COwner());
}
Commands.Add(new CQuit());
Commands.Add(new CRestart());
Commands.Add(new CKick());
Commands.Add(new CSay());
Commands.Add(new CTempBan());
Commands.Add(new CBan());
Commands.Add(new CWhoAmI());
Commands.Add(new CList());
Commands.Add(new CHelp());
Commands.Add(new CFastRestart());
Commands.Add(new CMapRotate());
Commands.Add(new CSetLevel());
Commands.Add(new CUsage());
Commands.Add(new CUptime());
Commands.Add(new CWarn());
Commands.Add(new CWarnClear());
Commands.Add(new CUnban());
Commands.Add(new CListAdmins());
Commands.Add(new CLoadMap());
Commands.Add(new CFindPlayer());
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());
Commands.Add(new CListAlias());
Commands.Add(new CExecuteRCON());
Commands.Add(new CPlugins());
Commands.Add(new CIP());
Commands.Add(new CMask());
Commands.Add(new CPruneAdmins());
//Commands.Add(new CKillServer());
Commands.Add(new CSetPassword());
Commands.Add(new CPing());
Commands.Add(new CSetGravatar());
Commands.Add(new CNextMap());
Commands.Add(new RequestTokenCommand());
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
{
Commands.Add(C);
}
#endregion
#region META
async Task<List<ProfileMeta>> getProfileMeta(int clientId, int offset, int count, DateTime? startAt)
{
var metaList = new List<ProfileMeta>();
// we don't want to return anything because it means we're trying to retrieve paged meta data
if (count > 1)
{
return metaList;
}
var lastMapMeta = await _metaService.GetPersistentMeta("LastMapPlayed", new EFClient() { ClientId = clientId });
if (lastMapMeta != null)
{
metaList.Add(new ProfileMeta()
{
Id = lastMapMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_MAP"],
Value = lastMapMeta.Value,
Show = true,
Type = ProfileMeta.MetaType.Information,
});
}
var lastServerMeta = await _metaService.GetPersistentMeta("LastServerPlayed", new EFClient() { ClientId = clientId });
if (lastServerMeta != null)
{
metaList.Add(new ProfileMeta()
{
Id = lastServerMeta.MetaId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_SERVER"],
Value = lastServerMeta.Value,
Show = true,
Type = ProfileMeta.MetaType.Information
});
}
var client = await GetClientService().Get(clientId);
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_TIME_HOURS"]} {Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PLAYER"]}",
Value = Math.Round(client.TotalConnectionTime / 3600.0, 1).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Show = true,
Column = 1,
Order = 0,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_FSEEN"],
Value = Utilities.GetTimePassed(client.FirstConnection, false),
Show = true,
Column = 1,
Order = 1,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_LSEEN"],
Value = Utilities.GetTimePassed(client.LastConnection, false),
Show = true,
Column = 1,
Order = 2,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Id = client.ClientId,
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_CONNECTIONS"],
Value = client.Connections.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Show = true,
Column = 1,
Order = 3,
Type = ProfileMeta.MetaType.Information
});
metaList.Add(new ProfileMeta()
{
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_MASKED"],
Value = client.Masked ? Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_TRUE"] : Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_FALSE"],
Sensitive = true,
Column = 1,
Order = 4,
Type = ProfileMeta.MetaType.Information
});
return metaList;
}
async Task<List<ProfileMeta>> getPenaltyMeta(int clientId, int offset, int count, DateTime? startAt)
{
if (count <= 1)
{
return new List<ProfileMeta>();
}
var penalties = await GetPenaltyService().GetClientPenaltyForMetaAsync(clientId, count, offset, startAt);
return penalties.Select(_penalty => new ProfileMeta()
{
Id = _penalty.Id,
Type = _penalty.PunisherId == clientId ? ProfileMeta.MetaType.Penalized : ProfileMeta.MetaType.ReceivedPenalty,
Value = _penalty,
When = _penalty.TimePunished,
Sensitive = _penalty.Sensitive
})
.ToList();
}
MetaService.AddRuntimeMeta(getProfileMeta);
MetaService.AddRuntimeMeta(getPenaltyMeta);
#endregion
await InitializeServers();
}
private async Task InitializeServers()
{
var config = ConfigHandler.Configuration();
int successServers = 0;
Exception lastException = null;
async Task Init(ServerConfiguration Conf)
{
// setup the event handler after the class is initialized
Handler = new GameEventHandler(this);
try
{
var ServerInstance = new IW4MServer(this, Conf);
await ServerInstance.Initialize();
_servers.Add(ServerInstance);
Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"].FormatExt(ServerInstance.Hostname));
// add the start event for this server
var e = new GameEvent()
{
Type = GameEvent.EventType.Start,
Data = $"{ServerInstance.GameName} started",
Owner = ServerInstance
};
Handler.AddEvent(e);
successServers++;
}
catch (ServerException e)
{
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"].FormatExt($"[{Conf.IPAddress}:{Conf.Port}]"));
if (e.GetType() == typeof(DvarException))
{
Logger.WriteDebug($"{e.Message} {(e.GetType() == typeof(DvarException) ? $"({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})" : "")}");
}
lastException = e;
}
}
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
if (successServers == 0)
{
throw lastException;
}
if (successServers != config.Servers.Count)
{
if (!Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_START_WITH_ERRORS"]))
{
throw lastException;
}
}
}
private async Task SendHeartbeat()
{
bool connected = false;
while (!_tokenSource.IsCancellationRequested)
{
if (!connected)
{
try
{
await Heartbeat.Send(this, true);
connected = true;
}
catch (Exception e)
{
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)
{
if (((RestEase.ApiException)ex).StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
}
}
catch (RestEase.ApiException e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
if (e.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
connected = false;
}
}
catch (Exception e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
}
}
try
{
await Task.Delay(30000, _tokenSource.Token);
}
catch { break; }
}
}
public async Task Start()
{
await Task.WhenAll(new[]
{
SendHeartbeat(),
UpdateServerStates()
});
}
public void Stop()
{
_tokenSource.Cancel();
Running = false;
Instance = null;
}
public void Restart()
{
IsRestartRequested = true;
Stop();
}
public ILogger GetLogger(long serverId)
{
if (Loggers.ContainsKey(serverId))
{
return Loggers[serverId];
}
else
{
Logger newLogger;
if (serverId == 0)
{
newLogger = new Logger("IW4MAdmin-Manager");
}
else
{
newLogger = new Logger($"IW4MAdmin-Server-{serverId}");
}
Loggers.Add(serverId, newLogger);
return newLogger;
}
}
public IList<MessageToken> GetMessageTokens()
{
return MessageTokens;
}
public IList<EFClient> GetActiveClients()
{
return _servers.SelectMany(s => s.Clients).Where(p => p != null).ToList();
}
public ClientService GetClientService()
{
return ClientSvc;
}
public AliasService GetAliasService()
{
return AliasSvc;
}
public PenaltyService GetPenaltyService()
{
return PenaltySvc;
}
public IConfigurationHandler<ApplicationConfiguration> GetApplicationSettings()
{
return ConfigHandler;
}
public IEventHandler GetEventHandler()
{
return Handler;
}
public IList<Assembly> GetPluginAssemblies()
{
return SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies.Union(SharedLibraryCore.Plugins.PluginImporter.Assemblies).ToList();
}
public IPageList GetPageList()
{
return PageList;
}
public IRConParser GenerateDynamicRConParser()
{
return new DynamicRConParser();
}
public IEventParser GenerateDynamicEventParser()
{
return new DynamicEventParser();
}
}
}

View File

@ -2,9 +2,6 @@ set SolutionDir=%1
set ProjectDir=%2
set TargetDir=%3
set OutDir=%4
set Version=%5
echo %Version% > "%SolutionDir%DEPLOY\version.txt"
echo Copying dependency configs
copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%"
@ -21,14 +18,3 @@ echo Copying plugins for publish
del %SolutionDir%BUILD\Plugins\Tests.dll
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\Windows\Plugins\"
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
echo Copying script plugins for publish
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\Windows\Plugins\"
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
echo Copying GSC files for publish
xcopy /Y "%SolutionDir%_customcallbacks.gsc" "%SolutionDir%Publish\Windows\userraw\scripts\"
xcopy /Y "%SolutionDir%_customcallbacks.gsc" "%SolutionDir%Publish\WindowsPrerelease\userraw\scripts\"
xcopy /Y "%SolutionDir%_commands.gsc" "%SolutionDir%Publish\Windows\userraw\scripts\"
xcopy /Y "%SolutionDir%_commands.gsc" "%SolutionDir%Publish\WindowsPrerelease\userraw\scripts\"

View File

@ -1,8 +1,6 @@
set SolutionDir=%1
set ProjectDir=%2
set TargetDir=%3
set CurrentConfiguration=%4
SET COPYCMD=/Y
echo Deleting extra language files
@ -56,66 +54,6 @@ del "%SolutionDir%Publish\Windows\*pdb"
if exist "%SolutionDir%Publish\WindowsPrerelease\web.config" del "%SolutionDir%Publish\WindowsPrerelease\web.config"
del "%SolutionDir%Publish\WindowsPrerelease\*pdb"
echo setting up library folders
if "%CurrentConfiguration%" == "Prerelease" (
echo PR-Config
if not exist "%SolutionDir%Publish\WindowsPrerelease\Configuration" md "%SolutionDir%Publish\WindowsPrerelease\Configuration"
move "%SolutionDir%Publish\WindowsPrerelease\DefaultSettings.json" "%SolutionDir%Publish\WindowsPrerelease\Configuration\"
)
if "%CurrentConfiguration%" == "Release" (
echo R-Config
if not exist "%SolutionDir%Publish\Windows\Configuration" md "%SolutionDir%Publish\Windows\Configuration"
if exist "%SolutionDir%Publish\Windows\DefaultSettings.json" move "%SolutionDir%Publish\Windows\DefaultSettings.json" "%SolutionDir%Publish\Windows\Configuration\DefaultSettings.json"
)
if "%CurrentConfiguration%" == "Prerelease" (
echo PR-LIB
if not exist "%SolutionDir%Publish\WindowsPrerelease\Lib\" md "%SolutionDir%Publish\WindowsPrerelease\Lib\"
move "%SolutionDir%Publish\WindowsPrerelease\*.dll" "%SolutionDir%Publish\WindowsPrerelease\Lib\"
move "%SolutionDir%Publish\WindowsPrerelease\*.json" "%SolutionDir%Publish\WindowsPrerelease\Lib\"
)
if "%CurrentConfiguration%" == "Release" (
echo R-LIB
if not exist "%SolutionDir%Publish\Windows\Lib\" md "%SolutionDir%Publish\Windows\Lib\"
move "%SolutionDir%Publish\Windows\*.dll" "%SolutionDir%Publish\Windows\Lib\"
move "%SolutionDir%Publish\Windows\*.json" "%SolutionDir%Publish\Windows\Lib\"
)
if "%CurrentConfiguration%" == "Prerelease" (
echo PR-RT
move "%SolutionDir%Publish\WindowsPrerelease\runtimes" "%SolutionDir%Publish\WindowsPrerelease\Lib\runtimes"
if exist "%SolutionDir%Publish\WindowsPrerelease\refs" move "%SolutionDir%Publish\WindowsPrerelease\refs" "%SolutionDir%Publish\WindowsPrerelease\Lib\refs"
)
if "%CurrentConfiguration%" == "Release" (
echo R-RT
move "%SolutionDir%Publish\Windows\runtimes" "%SolutionDir%Publish\Windows\Lib\runtimes"
if exist "%SolutionDir%Publish\Windows\refs" move "%SolutionDir%Publish\Windows\refs" "%SolutionDir%Publish\Windows\Lib\refs"
)
if "%CurrentConfiguration%" == "Prerelease" (
echo PR-LOC
if not exist "%SolutionDir%Publish\WindowsPrerelease\Localization" md "%SolutionDir%Publish\WindowsPrerelease\Localization"
)
if "%CurrentConfiguration%" == "Release" (
echo R-LOC
if not exist "%SolutionDir%Publish\Windows\Localization" md "%SolutionDir%Publish\Windows\Localization"
)
echo making start scripts
@(echo @echo off && echo @title IW4MAdmin && echo set DOTNET_CLI_TELEMETRY_OPTOUT=1 && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.cmd"
@(echo @echo off && echo @title IW4MAdmin && echo set DOTNET_CLI_TELEMETRY_OPTOUT=1 && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%SolutionDir%Publish\Windows\StartIW4MAdmin.cmd"
@(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.sh"
dos2unix "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.sh"
@(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%SolutionDir%Publish\Windows\StartIW4MAdmin.sh"
dos2unix "%SolutionDir%Publish\Windows\StartIW4MAdmin.sh"
echo setting permissions...
cacls "%SolutionDir%Publish\WindowsPrerelease" /t /e /p Everyone:F
cacls "%SolutionDir%Publish\Windows" /t /e /p Everyone:F
echo making start script
@echo dotnet IW4MAdmin.dll > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.cmd"
@echo dotnet IW4MAdmin.dll > "%SolutionDir%Publish\Windows\StartIW4MAdmin.cmd"

View File

@ -16,231 +16,7 @@
"Keep grenade launcher use to a minimum",
"Balance teams at ALL times"
],
"DisallowedClientNames": [ "Unknown Soldier", "VickNet", "UnknownSoldier", "CHEATER", "Play77" ],
"QuickMessages": [
{
"Game": "IW4",
"Messages": {
"QUICKMESSAGE_AREA_SECURE": "Area secure!",
"QUICKMESSAGE_ARE_YOU_CRAZY": "Are you crazy?",
"QUICKMESSAGE_ATTACK_LEFT_FLANK": "Attack left flank!",
"QUICKMESSAGE_ATTACK_RIGHT_FLANK": "Attack right flank!",
"QUICKMESSAGE_COME_ON": "Come on.",
"QUICKMESSAGE_ENEMIES_SPOTTED": "Multiple contacts!",
"QUICKMESSAGE_ENEMY_DOWN": "Enemy down!",
"QUICKMESSAGE_ENEMY_GRENADE": "Enemy grenade!",
"QUICKMESSAGE_ENEMY_SPOTTED": "Contact!",
"QUICKMESSAGE_FALL_BACK": "Fall back!",
"QUICKMESSAGE_FOLLOW_ME": "On me!",
"QUICKMESSAGE_GREAT_SHOT": "Nice shot!",
"QUICKMESSAGE_GRENADE": "Grenade!",
"QUICKMESSAGE_HOLD_THIS_POSITION": "Hold this position!",
"QUICKMESSAGE_HOLD_YOUR_FIRE": "Hold your fire!",
"QUICKMESSAGE_IM_IN_POSITION": "In position.",
"QUICKMESSAGE_IM_ON_MY_WAY": "Moving.",
"QUICKMESSAGE_MOVE_IN": "Move in!",
"QUICKMESSAGE_NEED_REINFORCEMENTS": "Need reinforcements!",
"QUICKMESSAGE_NO_SIR": "Negative.",
"QUICKMESSAGE_ON_MY_WAY": "On my way.",
"QUICKMESSAGE_REGROUP": "Regroup!",
"QUICKMESSAGE_SNIPER": "Sniper!",
"QUICKMESSAGE_SORRY": "Sorry.",
"QUICKMESSAGE_SQUAD_ATTACK_LEFT_FLANK": "Squad, attack left flank!",
"QUICKMESSAGE_SQUAD_ATTACK_RIGHT_FLANK": "Squad, attack right flank!",
"QUICKMESSAGE_SQUAD_HOLD_THIS_POSITION": "Squad, hold this position!",
"QUICKMESSAGE_SQUAD_REGROUP": "Squad, regroup!",
"QUICKMESSAGE_SQUAD_STICK_TOGETHER": "Squad, stick together!",
"QUICKMESSAGE_STICK_TOGETHER": "Stick together!",
"QUICKMESSAGE_SUPPRESSING_FIRE": "Base of fire!",
"QUICKMESSAGE_TOOK_LONG_ENOUGH": "Took long enough!",
"QUICKMESSAGE_TOOK_YOU_LONG_ENOUGH": "Took you long enough!",
"QUICKMESSAGE_WATCH_SIX": "Watch your six!",
"QUICKMESSAGE_YES_SIR": "Roger.",
"QUICKMESSAGE_YOURE_CRAZY": "You're crazy!",
"QUICKMESSAGE_YOURE_NUTS": "You're nuts!",
"QUICKMESSAGE_YOU_OUTTA_YOUR_MIND": "You outta your mind?"
}
}
],
"Maps": [
{
"Game": "IW3",
"Maps": [
{
"Alias": "Ambush",
"Name": "mp_convoy"
},
{
"Alias": "Backlot",
"Name": "mp_backlot"
},
{
"Alias": "Bloc",
"Name": "mp_bloc"
},
{
"Alias": "Bog",
"Name": "mp_bog"
},
{
"Alias": "Countdown",
"Name": "mp_countdown"
},
{
"Alias": "Crash",
"Name": "mp_crash"
},
{
"Alias": "Crossfire",
"Name": "mp_crossfire"
},
{
"Alias": "District",
"Name": "mp_citystreets"
},
{
"Alias": "Downpour",
"Name": "mp_farm"
},
{
"Alias": "Overgrown",
"Name": "mp_overgrown"
},
{
"Alias": "Pipeline",
"Name": "mp_pipeline"
},
{
"Alias": "Shipment",
"Name": "mp_shipment"
},
{
"Alias": "Showdown",
"Name": "mp_showdown"
},
{
"Alias": "Strike",
"Name": "mp_strike"
},
{
"Alias": "Vacant",
"Name": "mp_vacant"
},
{
"Alias": "Wet Work",
"Name": "mp_cargoship"
},
{
"Alias": "Winter Crash",
"Name": "mp_crash_snow"
},
{
"Alias": "Broadcast",
"Name": "mp_broadcast"
},
{
"Alias": "Creek",
"Name": "mp_creek"
},
{
"Alias": "Chinatown",
"Name": "mp_carentan"
},
{
"Alias": "Killhouse",
"Name": "mp_killhouse"
}
]
},
{
"Game": "T4",
"Maps": [
{
"Alias": "Airfield",
"Name": "mp_airfield"
},
{
"Alias": "Asylum",
"Name": "mp_asylum"
},
{
"Alias": "Castle",
"Name": "mp_castle"
},
{
"Alias": "Cliffside",
"Name": "mp_shrine"
},
{
"Alias": "Courtyard",
"Name": "mp_courtyard"
},
{
"Alias": "Dome",
"Name": "mp_dome"
},
{
"Alias": "Downfall",
"Name": "mp_downfall"
},
{
"Alias": "Hanger",
"Name": "mp_hangar"
},
{
"Alias": "Makin",
"Name": "mp_makin"
},
{
"Alias": "Outskirts",
"Name": "mp_outskirts"
},
{
"Alias": "Roundhouse",
"Name": "mp_roundhouse"
},
{
"Alias": "Upheaval",
"Name": "mp_suburban"
},
{
"Alias": "Knee Deep",
"Name": "mp_kneedeep"
},
{
"Alias": "Nightfire",
"Name": "mp_nachtfeuer"
},
{
"Alias": "Station",
"Name": "mp_subway"
},
{
"Alias": "Banzai",
"Name": "mp_kwai"
},
{
"Alias": "Corrosion",
"Name": "mp_stalingrad"
},
{
"Alias": "Sub Pens",
"Name": "mp_docks"
},
{
"Alias": "Battery",
"Name": "mp_drum"
},
{
"Alias": "Breach",
"Name": "mp_bgate"
},
{
"Alias": "Revolution",
"Name": "mp_vodka"
}
]
},
{
"Game": "IW4",
"Maps": [
@ -249,6 +25,11 @@
"Name": "mp_rust"
},
{
"Alias": "Highrise",
"Name": "mp_highrise"
},
{
"Alias": "Terminal",
"Name": "mp_terminal"
@ -259,6 +40,16 @@
"Name": "mp_crash"
},
{
"Alias": "Skidrow",
"Name": "mp_nightshift"
},
{
"Alias": "Quarry",
"Name": "mp_quarry"
},
{
"Alias": "Afghan",
"Name": "mp_afghan"
@ -380,7 +171,7 @@
},
{
"Alias": "Test map",
"Alias": "IW4 Credits",
"Name": "iw4_credits"
},
@ -399,11 +190,6 @@
"Name": "mp_cargoship_sh"
},
{
"Alias": "Cargoship",
"Name": "mp_cargoship"
},
{
"Alias": "Shipment",
"Name": "mp_shipment"
@ -430,306 +216,28 @@
},
{
"Alias": "Tropical Favela",
"Alias": "Favela - Tropical",
"Name": "mp_fav_tropical"
},
{
"Alias": "Tropical Estate",
"Alias": "Estate - Tropical",
"Name": "mp_estate_tropical"
},
{
"Alias": "Tropical Crash",
"Alias": "Crash - Tropical",
"Name": "mp_crash_tropical"
},
{
"Alias": "Forgotten City",
"Name": "mp_bloc_sh"
},
{
"Alias": "Crossfire",
"Name": "mp_cross_fire"
},
{
"Alias": "Bloc",
"Name": "mp_bloc"
},
{
"Alias": "Oilrig",
"Name": "oilrig"
},
{
"Name": "Village",
"Alias": "co_hunted"
}
]
},
{
"Game": "T5",
"Maps": [
{
"Alias": "Array",
"Name": "mp_array"
},
{
"Alias": "Berlin Wall",
"Name": "mp_berlinwall2"
},
{
"Alias": "Convoy",
"Name": "mp_gridlock"
},
{
"Alias": "Cracked",
"Name": "mp_cracked"
},
{
"Alias": "Crisis",
"Name": "mp_crisis"
},
{
"Alias": "Discovery",
"Name": "mp_discovery"
},
{
"Alias": "Drive-In",
"Name": "mp_drivein"
},
{
"Alias": "Firing Range",
"Name": "mp_firingrange"
},
{
"Alias": "Grid",
"Name": "mp_duga"
},
{
"Alias": "Hangar 18",
"Name": "mp_area51"
},
{
"Alias": "Hanoi",
"Name": "mp_hanoi"
},
{
"Alias": "Havana",
"Name": "mp_cairo"
},
{
"Alias": "Hazard",
"Name": "mp_golfcourse"
},
{
"Alias": "Hotel",
"Name": "mp_hotel"
},
{
"Alias": "Jungle",
"Name": "mp_havoc"
},
{
"Alias": "Kowloon",
"Name": "mp_kowloon"
},
{
"Alias": "Launch",
"Name": "mp_cosmodrome"
},
{
"Alias": "Nuketown",
"Name": "mp_nuked"
},
{
"Alias": "Radiation",
"Name": "mp_radiation"
},
{
"Alias": "Silo",
"Name": "mp_silo"
},
{
"Alias": "Stadium",
"Name": "mp_stadium"
},
{
"Alias": "Stockpile",
"Name": "mp_outskirts"
},
{
"Alias": "Summit",
"Name": "mp_mountain"
},
{
"Alias": "Villa",
"Name": "mp_villa"
},
{
"Alias": "WMD",
"Name": "mp_russianbase"
},
{
"Alias": "Zoo",
"Name": "mp_zoo"
}
]
},
{
"Game": "IW5",
"Maps": [
{
"Alias": "Seatown",
"Name": "mp_seatown"
},
{
"Alias": "Lockdown",
"Name": "mp_alpha"
},
{
"Alias": "Mission",
"Name": "mp_bravo"
},
{
"Alias": "Carbon",
"Name": "mp_carbon"
},
{
"Alias": "Dome",
"Name": "mp_dome"
},
{
"Alias": "Arkaden",
"Name": "mp_plaza2"
},
{
"Alias": "Downturn",
"Name": "mp_exchange"
},
{
"Alias": "Bootleg",
"Name": "mp_bootleg"
},
{
"Alias": "Hardhat",
"Name": "mp_hardhat"
},
{
"Alias": "Interchange",
"Name": "mp_interchange"
},
{
"Alias": "Fallen",
"Name": "mp_lambeth"
},
{
"Alias": "Outpost",
"Name": "mp_radar"
},
{
"Alias": "Bakaara",
"Name": "mp_mogadishu"
},
{
"Alias": "Resistance",
"Name": "mp_paris"
},
{
"Alias": "Underground",
"Name": "mp_underground"
},
{
"Alias": "Village",
"Name": "mp_village"
},
{
"Alias": "Aground",
"Name": "mp_aground_ss"
},
{
"Alias": "Boardwalk",
"Name": "mp_boardwalk"
},
{
"Alias": "U-turn",
"Name": "mp_burn_ss"
},
{
"Alias": "Foundation",
"Name": "mp_cement"
},
{
"Alias": "Erosion",
"Name": "mp_courtyard_ss"
},
{
"Alias": "Intersection",
"Name": "mp_crosswalk_ss"
},
{
"Alias": "Getaway",
"Name": "mp_hillside_ss"
},
{
"Alias": "Piazza",
"Name": "mp_italy"
},
{
"Alias": "Sanctuary",
"Name": "mp_meteora"
},
{
"Alias": "Gulch",
"Name": "mp_moab"
},
{
"Alias": "Black Box",
"Name": "mp_morningwood"
},
{
"Alias": "Parish",
"Name": "mp_nola"
},
{
"Alias": "Overwatch",
"Name": "mp_overwatch"
},
{
"Alias": "Liberation",
"Name": "mp_park"
},
{
"Alias": "Oasis",
"Name": "mp_qadeem"
},
{
"Alias": "Lookout",
"Name": "mp_restrepo_ss"
},
{
"Alias": "Off Shore",
"Name": "mp_roughneck"
},
{
"Alias": "Decommission",
"Name": "mp_shipbreaker"
},
{
"Alias": "Vortex",
"Name": "mp_six_ss"
},
{
"Alias": "Terminal",
"Name": "mp_terminal_cls"
}
]
},
{
"Game": "T6",
"Game": "T6M",
"Maps": [
{
"Alias": "Aftermath",

View File

@ -1,293 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.EventParsers
{
class BaseEventParser : IEventParser
{
public BaseEventParser()
{
Configuration = new DynamicEventParserConfiguration()
{
GameDirectory = "main",
};
Configuration.Say.Pattern = @"^(say|sayteam);(-?[A-Fa-f0-9_]{1,32});([0-9]+);(.+);(.*)$";
Configuration.Say.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Say.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Say.AddMapping(ParserRegex.GroupType.Message, 5);
Configuration.Quit.Pattern = @"^(Q);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);([0-9]+);(.*)$";
Configuration.Quit.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Quit.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Join.Pattern = @"^(J);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);([0-9]+);(.*)$";
Configuration.Join.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginNetworkId, 2);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginClientNumber, 3);
Configuration.Join.AddMapping(ParserRegex.GroupType.OriginName, 4);
Configuration.Damage.Pattern = @"^(D);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world)?;(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world)?;(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Damage.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetTeam, 4);
Configuration.Damage.AddMapping(ParserRegex.GroupType.TargetName, 5);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginNetworkId, 6);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginClientNumber, 7);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginTeam, 8);
Configuration.Damage.AddMapping(ParserRegex.GroupType.OriginName, 9);
Configuration.Damage.AddMapping(ParserRegex.GroupType.Weapon, 10);
Configuration.Damage.AddMapping(ParserRegex.GroupType.Damage, 11);
Configuration.Damage.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Damage.AddMapping(ParserRegex.GroupType.HitLocation, 13);
Configuration.Kill.Pattern = @"^(K);(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+);(-?[0-9]+);(axis|allies|world)?;(.{1,24});(-?[A-Fa-f0-9_]{1,32}|bot[0-9]+)?;-?([0-9]+);(axis|allies|world)?;(.{1,24})?;((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$";
Configuration.Kill.AddMapping(ParserRegex.GroupType.EventType, 1);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetNetworkId, 2);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetClientNumber, 3);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetTeam, 4);
Configuration.Kill.AddMapping(ParserRegex.GroupType.TargetName, 5);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginNetworkId, 6);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginClientNumber, 7);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginTeam, 8);
Configuration.Kill.AddMapping(ParserRegex.GroupType.OriginName, 9);
Configuration.Kill.AddMapping(ParserRegex.GroupType.Weapon, 10);
Configuration.Kill.AddMapping(ParserRegex.GroupType.Damage, 11);
Configuration.Kill.AddMapping(ParserRegex.GroupType.MeansOfDeath, 12);
Configuration.Kill.AddMapping(ParserRegex.GroupType.HitLocation, 13);
}
public IEventParserConfiguration Configuration { get; set; }
public string Version { get; set; } = "CoD";
public Game GameName { get; set; } = Game.COD;
public string URLProtocolFormat { get; set; } = "CoD://{{ip}}:{{port}}";
public virtual GameEvent GenerateGameEvent(string logLine)
{
logLine = Regex.Replace(logLine, @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim();
string[] lineSplit = logLine.Split(';');
string eventType = lineSplit[0];
if (eventType == "say" || eventType == "sayteam")
{
var matchResult = Regex.Match(logLine, Configuration.Say.Pattern);
if (matchResult.Success)
{
string message = matchResult
.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.Message]]
.ToString()
.Replace("\x15", "")
.Trim();
if (message.Length > 0)
{
long originId = matchResult.Groups[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong();
if (message[0] == '!' || message[0] == '@')
{
return new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = new EFClient() { NetworkId = originId },
Message = message,
RequiredEntity = GameEvent.EventRequiredEntity.Origin
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = new EFClient() { NetworkId = originId },
Message = message,
RequiredEntity = GameEvent.EventRequiredEntity.Origin
};
}
}
}
if (eventType == "K")
{
var match = Regex.Match(logLine, Configuration.Kill.Pattern);
if (match.Success)
{
long originId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].Value.ToString().ConvertGuidToLong(1);
long targetId = match.Groups[Configuration.Kill.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].Value.ToString().ConvertGuidToLong(1);
return new GameEvent()
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
}
if (eventType == "D")
{
var regexMatch = Regex.Match(logLine, Configuration.Damage.Pattern);
if (regexMatch.Success)
{
long originId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(1);
long targetId = regexMatch.Groups[Configuration.Damage.GroupMapping[ParserRegex.GroupType.TargetNetworkId]].ToString().ConvertGuidToLong(1);
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
}
if (eventType == "J")
{
var regexMatch = Regex.Match(logLine, Configuration.Join.Pattern);
if (regexMatch.Success)
{
return new GameEvent()
{
Type = GameEvent.EventType.PreConnect,
Data = logLine,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().StripColors(),
},
NetworkId = regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(),
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Connecting,
},
RequiredEntity = GameEvent.EventRequiredEntity.None
};
}
}
if (eventType == "Q")
{
var regexMatch = Regex.Match(logLine, Configuration.Quit.Pattern);
if (regexMatch.Success)
{
return new GameEvent()
{
Type = GameEvent.EventType.PreDisconnect,
Data = logLine,
Origin = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginName]].ToString().StripColors()
},
NetworkId = regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginNetworkId]].ToString().ConvertGuidToLong(),
ClientNumber = Convert.ToInt32(regexMatch.Groups[Configuration.Quit.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
State = EFClient.ClientState.Disconnecting
},
RequiredEntity = GameEvent.EventRequiredEntity.Origin
};
}
}
if (eventType.Contains("ExitLevel"))
{
return new GameEvent()
{
Type = GameEvent.EventType.MapEnd,
Data = lineSplit[0],
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None
};
}
if (eventType.Contains("InitGame"))
{
string dump = eventType.Replace("InitGame: ", "");
return new GameEvent()
{
Type = GameEvent.EventType.MapChange,
Data = lineSplit[0],
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
Extra = dump.DictionaryFromKeyValue(),
RequiredEntity = GameEvent.EventRequiredEntity.None
};
}
// this is a custom event printed out by _customcallbacks.gsc (used for team balance)
if (eventType == "JoinTeam")
{
return new GameEvent()
{
Type = GameEvent.EventType.JoinTeam,
Data = logLine,
Origin = new EFClient() { NetworkId = lineSplit[1].ConvertGuidToLong() },
RequiredEntity = GameEvent.EventRequiredEntity.Target
};
}
// this is a custom event printed out by _customcallbacks.gsc (used for anticheat)
if (eventType == "ScriptKill")
{
long originId = lineSplit[1].ConvertGuidToLong(1);
long targetId = lineSplit[2].ConvertGuidToLong(1);
return new GameEvent()
{
Type = GameEvent.EventType.ScriptKill,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
// this is a custom event printed out by _customcallbacks.gsc (used for anticheat)
if (eventType == "ScriptDamage")
{
long originId = lineSplit[1].ConvertGuidToLong(1);
long targetId = lineSplit[2].ConvertGuidToLong(1);
return new GameEvent()
{
Type = GameEvent.EventType.ScriptDamage,
Data = logLine,
Origin = new EFClient() { NetworkId = originId },
Target = new EFClient() { NetworkId = targetId },
RequiredEntity = GameEvent.EventRequiredEntity.Origin | GameEvent.EventRequiredEntity.Target
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Origin = Utilities.IW4MAdminClient(),
Target = Utilities.IW4MAdminClient(),
RequiredEntity = GameEvent.EventRequiredEntity.None
};
}
}
}

View File

@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.EventParsers
{
/// <summary>
/// empty generic implementation of the IEventParserConfiguration
/// allows script plugins to generate dynamic event parsers
/// </summary>
sealed internal class DynamicEventParser : BaseEventParser
{
}
}

View File

@ -1,19 +0,0 @@
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.EventParsers
{
/// <summary>
/// generic implementation of the IEventParserConfiguration
/// allows script plugins to generate dynamic configurations
/// </summary>
sealed internal class DynamicEventParserConfiguration : IEventParserConfiguration
{
public string GameDirectory { get; set; }
public ParserRegex Say { get; set; } = new ParserRegex();
public ParserRegex Join { get; set; } = new ParserRegex();
public ParserRegex Quit { get; set; } = new ParserRegex();
public ParserRegex Kill { get; set; } = new ParserRegex();
public ParserRegex Damage { get; set; } = new ParserRegex();
public ParserRegex Action { get; set; } = new ParserRegex();
}
}

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

@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Application.EventParsers
{
class IW4EventParser : IEventParser
{
public virtual GameEvent GetEvent(Server server, string logLine)
{
string[] lineSplit = logLine.Split(';');
string cleanedEventLine = Regex.Replace(lineSplit[0], @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim();
if (cleanedEventLine[0] == 'K')
{
if (!server.CustomCallback)
{
return new GameEvent()
{
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)),
Owner = server
};
}
}
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 = message,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server,
Message = message
};
}
if (cleanedEventLine.Contains("ScriptKill"))
{
return new GameEvent()
{
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()),
Owner = server
};
}
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()
{
Type = GameEvent.EventType.MapEnd,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
if (cleanedEventLine.Contains("InitGame"))
{
string dump = cleanedEventLine.Replace("InitGame: ", "");
return new GameEvent()
{
Type = GameEvent.EventType.MapChange,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server,
Extra = dump.DictionaryFromKeyValue()
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
// other parsers can derive from this parser so we make it virtual
public virtual string GetGameDir() => "userraw";
}
}

View File

@ -0,0 +1,53 @@
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 IW4MAdmin.Application.EventParsers
{
class IW5EventParser : IW4EventParser
{
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

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Application.EventParsers
{
class T6MEventParser : IW4EventParser
{
/*public GameEvent GetEvent(Server server, string logLine)
{
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.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
};
}
if (lineSplit[0][0] == 'D')
{
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
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
};
}
if (lineSplit[0] == "say" || lineSplit[0] == "sayteam")
{
return new GameEvent()
{
Type = GameEvent.EventType.Say,
Data = lineSplit[4],
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server,
Message = lineSplit[4]
};
}
if (lineSplit[0].Contains("ExitLevel"))
{
return new GameEvent()
{
Type = GameEvent.EventType.MapEnd,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}
if (lineSplit[0].Contains("InitGame"))
{
string dump = cleanedEventLine.Replace("InitGame: ", "");
return new GameEvent()
{
Type = GameEvent.EventType.MapChange,
Data = lineSplit[0],
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server,
Extra = dump.DictionaryFromKeyValue()
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Unknown,
Origin = new Player()
{
ClientId = 1
},
Target = new Player()
{
ClientId = 1
},
Owner = server
};
}*/
public override string GetGameDir() => $"t6r{Path.DirectorySeparatorChar}data";
}
}

View File

@ -1,26 +1,105 @@
using SharedLibraryCore;
using SharedLibraryCore.Events;
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
{
readonly ApplicationManager Manager;
private ConcurrentQueue<GameEvent> EventQueue;
private Queue<GameEvent> StatusSensitiveQueue;
private IManager Manager;
public GameEventHandler(IManager mgr)
{
Manager = (ApplicationManager)mgr;
EventQueue = new ConcurrentQueue<GameEvent>();
StatusSensitiveQueue = new Queue<GameEvent>();
Manager = mgr;
}
public void AddEvent(GameEvent gameEvent)
{
#if DEBUG
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
gameEvent.Owner.Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner}");
#endif
Manager.OnServerEvent?.Invoke(gameEvent.Owner, new GameEventArgs(null, false, gameEvent));
// 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

@ -1,80 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO
{
class GameLogEventDetection
{
private long previousFileSize;
private readonly Server _server;
private readonly IGameLogReader _reader;
private readonly string _gameLogFile;
class EventState
{
public ILogger Log { get; set; }
public string ServerId { get; set; }
}
public GameLogEventDetection(Server server, string gameLogPath, Uri gameLogServerUri)
{
_gameLogFile = gameLogPath;
_reader = gameLogServerUri != null ? new GameLogReaderHttp(gameLogServerUri, gameLogPath, server.EventParser) : _reader = new GameLogReader(gameLogPath, server.EventParser);
_server = server;
}
public async Task PollForChanges()
{
while (!_server.Manager.CancellationToken.IsCancellationRequested)
{
#if !DEBUG
if (_server.IsInitialized)
#endif
{
try
{
await UpdateLogEvents();
}
catch (Exception e)
{
_server.Logger.WriteWarning($"Failed to update log event for {_server.EndPoint}");
_server.Logger.WriteDebug(e.GetExceptionInfo());
}
}
await Task.Delay(_reader.UpdateInterval, _server.Manager.CancellationToken);
}
_server.Logger.WriteDebug("Stopped polling for changes");
}
private async Task UpdateLogEvents()
{
long fileSize = _reader.Length;
if (previousFileSize == 0)
{
previousFileSize = fileSize;
}
long fileDiff = fileSize - previousFileSize;
// this makes the http log get pulled
if (fileDiff < 1 && fileSize != -1)
return;
var events = await _reader.ReadEventsFromLog(_server, fileDiff, previousFileSize);
foreach (var ev in events)
{
_server.Manager.GetEventHandler().AddEvent(ev);
}
previousFileSize = fileSize;
}
}
}

View File

@ -3,21 +3,14 @@ using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO
{
class GameLogReader : IGameLogReader
class GameLogReader
{
IEventParser Parser;
readonly string LogFile;
private bool? ignoreBots;
public long Length => new FileInfo(LogFile).Length;
public int UpdateInterval => 300;
string LogFile;
public GameLogReader(string logFile, IEventParser parser)
{
@ -25,42 +18,21 @@ namespace IW4MAdmin.Application.IO
Parser = parser;
}
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
public ICollection<GameEvent> EventsFromLog(Server server, long fileSizeDiff, long startPosition)
{
if (!ignoreBots.HasValue)
{
ignoreBots = server.Manager.GetApplicationSettings().Configuration().IgnoreBots;
}
// allocate the bytes for the new log lines
List<string> logLines = new List<string>();
// open the file as a stream
using (FileStream fs = new FileStream(LogFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var rd = new StreamReader(new FileStream(LogFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), Utilities.EncodingType))
{
byte[] buff = new byte[fileSizeDiff];
fs.Seek(startPosition, SeekOrigin.Begin);
await fs.ReadAsync(buff, 0, (int)fileSizeDiff, server.Manager.CancellationToken);
var stringBuilder = new StringBuilder();
char[] charBuff = Utilities.EncodingType.GetChars(buff);
foreach (char c in charBuff)
// 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()))
{
if (c == '\n')
{
logLines.Add(stringBuilder.ToString());
stringBuilder = new StringBuilder();
}
else if (c != '\r')
{
stringBuilder.Append(c);
}
}
if (stringBuilder.Length > 0)
{
logLines.Add(stringBuilder.ToString());
logLines.Add(newLine);
}
}
@ -73,50 +45,15 @@ namespace IW4MAdmin.Application.IO
{
try
{
var gameEvent = Parser.GenerateGameEvent(eventLine);
// we don't want to add the event if ignoreBots is on and the event comes from a bot
if (!ignoreBots.Value || (ignoreBots.Value && !((gameEvent.Origin?.IsBot ?? false) || (gameEvent.Target?.IsBot ?? false))))
{
gameEvent.Owner = server;
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != 1)
{
gameEvent.Origin = server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId);
}
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target)
{
gameEvent.Target = server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Target?.NetworkId);
}
if (gameEvent.Origin != null)
{
gameEvent.Origin.CurrentServer = server;
}
if (gameEvent.Target != null)
{
gameEvent.Target.CurrentServer = server;
}
events.Add(gameEvent);
}
}
catch (InvalidOperationException)
{
if (!ignoreBots.Value)
{
server.Logger.WriteWarning("Could not find client in client list when parsing event line");
server.Logger.WriteDebug(eventLine);
}
// todo: catch elsewhere
events.Add(Parser.GetEvent(server, eventLine));
}
catch (Exception e)
{
server.Logger.WriteWarning("Could not properly parse event line");
server.Logger.WriteDebug(e.Message);
server.Logger.WriteDebug(eventLine);
Program.ServerManager.GetLogger().WriteWarning("Could not properly parse event line");
Program.ServerManager.GetLogger().WriteDebug(e.Message);
Program.ServerManager.GetLogger().WriteDebug(eventLine);
}
}
}

View File

@ -1,121 +0,0 @@
using IW4MAdmin.Application.API.GameLogServer;
using RestEase;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using static SharedLibraryCore.Utilities;
namespace IW4MAdmin.Application.IO
{
/// <summary>
/// provides capibility of reading log files over HTTP
/// </summary>
class GameLogReaderHttp : IGameLogReader
{
readonly IEventParser Parser;
readonly IGameLogServer Api;
readonly string logPath;
private bool? ignoreBots;
private string lastKey = "next";
public GameLogReaderHttp(Uri gameLogServerUri, string logPath, IEventParser parser)
{
this.logPath = logPath.ToBase64UrlSafeString(); ;
Parser = parser;
Api = RestClient.For<IGameLogServer>(gameLogServerUri);
}
public long Length => -1;
public int UpdateInterval => 500;
public async Task<ICollection<GameEvent>> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
{
if (!ignoreBots.HasValue)
{
ignoreBots = server.Manager.GetApplicationSettings().Configuration().IgnoreBots;
}
var events = new List<GameEvent>();
string b64Path = logPath;
var response = await Api.Log(b64Path, lastKey);
lastKey = response.NextKey;
if (!response.Success && string.IsNullOrEmpty(lastKey))
{
server.Logger.WriteError($"Could not get log server info of {logPath}/{b64Path} ({server.LogPath})");
return events;
}
else if (!string.IsNullOrWhiteSpace(response.Data))
{
#if DEBUG
server.Manager.GetLogger(0).WriteInfo(response.Data);
#endif
// parse each line
foreach (string eventLine in response.Data.Split(Environment.NewLine))
{
if (eventLine.Length > 0)
{
try
{
var gameEvent = Parser.GenerateGameEvent(eventLine);
// we don't want to add the event if ignoreBots is on and the event comes from a bot
if (!ignoreBots.Value || (ignoreBots.Value && !((gameEvent.Origin?.IsBot ?? false) || (gameEvent.Target?.IsBot ?? false))))
{
gameEvent.Owner = server;
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Origin) == GameEvent.EventRequiredEntity.Origin && gameEvent.Origin.NetworkId != 1)
{
gameEvent.Origin = server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Origin?.NetworkId);
}
if ((gameEvent.RequiredEntity & GameEvent.EventRequiredEntity.Target) == GameEvent.EventRequiredEntity.Target)
{
gameEvent.Target = server.GetClientsAsList().First(_client => _client.NetworkId == gameEvent.Target?.NetworkId);
}
if (gameEvent.Origin != null)
{
gameEvent.Origin.CurrentServer = server;
}
if (gameEvent.Target != null)
{
gameEvent.Target.CurrentServer = server;
}
events.Add(gameEvent);
}
#if DEBUG == true
server.Logger.WriteDebug($"Parsed event with id {gameEvent.Id} from http");
#endif
}
catch (InvalidOperationException)
{
if (!ignoreBots.Value)
{
server.Logger.WriteWarning("Could not find client in client list when parsing event line");
server.Logger.WriteDebug(eventLine);
}
}
catch (Exception e)
{
server.Logger.WriteWarning("Could not properly parse remote event line");
server.Logger.WriteDebug(e.Message);
server.Logger.WriteDebug(eventLine);
}
}
}
}
return events;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -12,13 +12,11 @@ namespace IW4MAdmin.Application.Localization
{
public class Configure
{
public static void Initialize(string customLocale = null)
public static void Initialize(string customLocale)
{
string currentLocale = string.IsNullOrEmpty(customLocale) ? CultureInfo.CurrentCulture.Name : customLocale;
string[] localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale}.json");
string[] localizationFiles = Directory.GetFiles("Localization", $"*.{currentLocale}.json");
if (!Program.ServerManager.GetApplicationSettings()?.Configuration()?.UseLocalTranslations ?? false)
{
try
{
var api = Endpoint.Get();
@ -31,18 +29,17 @@ namespace IW4MAdmin.Application.Localization
{
// the online localization failed so will default to local files
}
}
// culture doesn't exist so we just want language
if (localizationFiles.Length == 0)
{
localizationFiles = Directory.GetFiles(Path.Join(Utilities.OperatingDirectory, "Localization"), $"*.{currentLocale.Substring(0, 2)}*.json");
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(Path.Join(Utilities.OperatingDirectory, "Localization"), "*.en-US.json");
localizationFiles = Directory.GetFiles("Localization", "*.en-US.json");
}
// this should never happen unless the localization folder is empty
@ -62,12 +59,12 @@ namespace IW4MAdmin.Application.Localization
{
if (!localizationDict.TryAdd(item.Key, item.Value))
{
Program.ServerManager.GetLogger(0).WriteError($"Could not add locale string {item.Key} to localization");
Program.ServerManager.GetLogger().WriteError($"Could not add locale string {item.Key} to localization");
}
}
}
string localizationFile = $"{Path.Join(Utilities.OperatingDirectory, "Localization")}{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocale}-{currentLocale.ToUpper()}.json";
string localizationFile = $"Localization{Path.DirectorySeparatorChar}IW4MAdmin.{currentLocale}-{currentLocale.ToUpper()}.json";
Utilities.CurrentLocalization = new SharedLibraryCore.Localization.Layout(localizationDict)
{

View File

@ -0,0 +1,269 @@
{
"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": "Эффективность"
}
}
}

90
Application/Logger.cs Normal file
View File

@ -0,0 +1,90 @@
using SharedLibraryCore;
using System;
using System.Collections.Generic;
using System.IO;
namespace IW4MAdmin.Application
{
class Logger : SharedLibraryCore.Interfaces.ILogger
{
enum LogType
{
Verbose,
Info,
Debug,
Warning,
Error,
Assert
}
string FileName;
object ThreadLock;
public Logger(string fn)
{
FileName = fn;
ThreadLock = new object();
if (File.Exists(fn))
File.Delete(fn);
}
void Write(string msg, LogType type)
{
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
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
Console.WriteLine(LogLine);
File.AppendAllText(FileName, LogLine + Environment.NewLine);
#else
if (type == LogType.Error || type == LogType.Verbose)
Console.WriteLine(LogLine);
//if (type != LogType.Debug)
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
#endif
}
}
public void WriteVerbose(string msg)
{
Write(msg, LogType.Verbose);
}
public void WriteDebug(string msg)
{
Write(msg, LogType.Debug);
}
public void WriteError(string msg)
{
Write(msg, LogType.Error);
}
public void WriteInfo(string msg)
{
Write(msg, LogType.Info);
}
public void WriteWarning(string msg)
{
Write(msg, LogType.Warning);
}
public void WriteAssert(bool condition, string msg)
{
if (!condition)
Write(msg, LogType.Assert);
}
}
}

View File

@ -1,144 +1,57 @@
using IW4MAdmin.Application.Migration;
using System;
using System.Threading.Tasks;
using System.IO;
using System.Reflection;
using SharedLibraryCore;
using System;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Database;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using SharedLibraryCore.Localization;
namespace IW4MAdmin.Application
{
public class Program
{
public static double Version { get; private set; } = Utilities.GetVersionAsDouble();
public static ApplicationManager ServerManager;
private static Task ApplicationTask;
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();
/// <summary>
/// entrypoint of the application
/// </summary>
/// <returns></returns>
public static async Task Main()
public static void Main(string[] args)
{
AppDomain.CurrentDomain.SetData("DataDirectory", Utilities.OperatingDirectory);
AppDomain.CurrentDomain.SetData("DataDirectory", OperatingDirectory);
//System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.BelowNormal;
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray;
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
Version = Assembly.GetExecutingAssembly().GetName().Version.Major + Assembly.GetExecutingAssembly().GetName().Version.Minor / 10.0f;
Version = Math.Round(Version, 2);
Console.WriteLine("=====================================================");
Console.WriteLine(" IW4MAdmin");
Console.WriteLine(" IW4M ADMIN");
Console.WriteLine(" by RaidMax ");
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine($" Version {Version.ToString("0.0")}");
Console.WriteLine("=====================================================");
await LaunchAsync();
}
Index loc = null;
/// <summary>
/// event callback executed when the control + c combination is detected
/// gracefully stops the server manager and waits for all tasks to finish
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
{
ServerManager?.Stop();
await ApplicationTask;
}
/// <summary>
/// task that initializes application and starts the application monitoring and runtime tasks
/// </summary>
/// <returns></returns>
private static async Task LaunchAsync()
{
restart:
try
{
CheckDirectories();
ServerManager = ApplicationManager.GetInstance();
var configuration = ServerManager.GetApplicationSettings().Configuration();
Localization.Configure.Initialize(configuration?.EnableCustomLocale ?? false ? (configuration.CustomLocale ?? "en-US") : "en-US");
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCancelKey);
Localization.Configure.Initialize(ServerManager.GetApplicationSettings().Configuration()?.CustomLocale);
loc = Utilities.CurrentLocalization.LocalizationIndex;
// do any needed housekeeping file/folder migrations
ConfigurationMigration.MoveConfigFolder10518(null);
ConfigurationMigration.CheckDirectories();
using (var db = new DatabaseContext(ServerManager.GetApplicationSettings().Configuration()?.ConnectionString))
new ContextSeed(db).Seed().Wait();
ServerManager.Logger.WriteInfo(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_VERSION"].FormatExt(Version));
await CheckVersion();
await ServerManager.Init();
}
catch (Exception e)
{
var loc = Utilities.CurrentLocalization.LocalizationIndex;
string failMessage = loc == null ? "Failed to initalize IW4MAdmin" : loc["MANAGER_INIT_FAIL"];
string exitMessage = loc == null ? "Press any key to exit..." : loc["MANAGER_EXIT"];
Console.WriteLine(failMessage);
while (e.InnerException != null)
{
e = e.InnerException;
}
Console.WriteLine(e.Message);
Console.WriteLine(exitMessage);
Console.ReadKey();
return;
}
try
{
ApplicationTask = RunApplicationTasksAsync();
await ApplicationTask;
}
catch { }
if (ServerManager.IsRestartRequested)
{
goto restart;
}
}
/// <summary>
/// runs the core application tasks
/// </summary>
/// <returns></returns>
private static async Task RunApplicationTasksAsync()
{
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront ?
WebfrontCore.Program.Init(ServerManager, ServerManager.CancellationToken) :
Task.CompletedTask;
// we want to run this one on a manual thread instead of letting the thread pool handle it,
// because we can't exit early from waiting on console input, and it prevents us from restarting
var inputThread = new Thread(async () => await ReadConsoleInput());
inputThread.Start();
var tasks = new[]
{
ServerManager.Start(),
webfrontTask,
};
await Task.WhenAll(tasks);
inputThread.Abort();
ServerManager.Logger.WriteVerbose(Utilities.CurrentLocalization.LocalizationIndex["MANAGER_SHUTDOWN_SUCCESS"]);
}
/// <summary>
/// checks for latest version of the application
/// notifies user if an update is available
/// </summary>
/// <returns></returns>
private static async Task CheckVersion()
{
var api = API.Master.Endpoint.Get();
var loc = Utilities.CurrentLocalization.LocalizationIndex;
var version = new API.Master.VersionInfo()
{
@ -147,7 +60,7 @@ namespace IW4MAdmin.Application
try
{
version = await api.GetVersion();
version = api.GetVersion().Result;
}
catch (Exception e)
@ -173,7 +86,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"].FormatExt($"[v{Version.ToString("0.0")}]"));
Console.WriteLine($"{loc["MANAGER_VERSION_CURRENT"]} [v{Version.ToString("0.0")}]");
Console.ForegroundColor = ConsoleColor.Gray;
}
#else
@ -181,7 +94,7 @@ 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"].FormatExt($"[v{Version.ToString("0.0")}-pr]"));
Console.WriteLine($"{loc["MANAGER_VERSION_CURRENT"]} [v{Version.ToString("0.0")}-pr]");
Console.ForegroundColor = ConsoleColor.Gray;
}
#endif
@ -191,44 +104,83 @@ namespace IW4MAdmin.Application
Console.WriteLine(loc["MANAGER_VERSION_SUCCESS"]);
Console.ForegroundColor = ConsoleColor.Gray;
}
ServerManager.Init().Wait();
var consoleTask = Task.Run(() =>
{
String userInput;
Player Origin = ServerManager.GetClientService().Get(1).Result.AsPlayer();
do
{
userInput = Console.ReadLine();
if (userInput?.ToLower() == "quit")
ServerManager.Stop();
if (ServerManager.Servers.Count == 0)
{
Console.WriteLine(loc["MANAGER_CONSOLE_NOSERV"]);
continue;
}
/// <summary>
/// reads input from the console and executes entered commands on the default server
/// </summary>
/// <returns></returns>
private static async Task ReadConsoleInput()
{
string lastCommand;
var Origin = Utilities.IW4MAdminClient(ServerManager.Servers[0]);
try
{
while (!ServerManager.CancellationToken.IsCancellationRequested)
{
lastCommand = Console.ReadLine();
if (lastCommand?.Length > 0)
{
if (lastCommand?.Length > 0)
if (userInput?.Length > 0)
{
Origin.CurrentServer = ServerManager.Servers[0];
GameEvent E = new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = lastCommand,
Data = userInput,
Origin = Origin,
Owner = ServerManager.Servers[0]
};
ServerManager.GetEventHandler().AddEvent(E);
await E.WaitAsync(Utilities.DefaultCommandTimeout, ServerManager.CancellationToken);
E.OnProcessed.Wait(5000);
}
Console.Write('>');
}
}
}
}
catch (OperationCanceledException)
{ }
} while (ServerManager.Running);
});
}
catch (Exception e)
{
Console.WriteLine(loc["MANAGER_INIT_FAIL"]);
while (e.InnerException != null)
{
e = e.InnerException;
}
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()
{
string curDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
if (!Directory.Exists($"{curDirectory}Plugins"))
Directory.CreateDirectory($"{curDirectory}Plugins");
}
}
}

514
Application/Manager.cs Normal file
View File

@ -0,0 +1,514 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.IO;
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Services;
using IW4MAdmin.Application.API;
using Microsoft.Extensions.Configuration;
using WebfrontCore;
using SharedLibraryCore.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Text;
using IW4MAdmin.Application.API.Master;
namespace IW4MAdmin.Application
{
public class ApplicationManager : IManager
{
private List<Server> _servers;
public List<Server> Servers => _servers.OrderByDescending(s => s.ClientNum).ToList();
public Dictionary<int, Player> PrivilegedClients { get; set; }
public ILogger Logger { get; private set; }
public bool Running { get; private set; }
public EventHandler<GameEvent> ServerEventOccurred { get; private set; }
public DateTime StartTime { get; private set; }
static ApplicationManager Instance;
List<AsyncStatus> TaskStatuses;
List<Command> Commands;
List<MessageToken> MessageTokens;
ClientService ClientSvc;
AliasService AliasSvc;
PenaltyService PenaltySvc;
BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
EventApi Api;
GameEventHandler Handler;
ManualResetEventSlim OnEvent;
private ApplicationManager()
{
Logger = new Logger($@"{Utilities.OperatingDirectory}IW4MAdmin.log");
_servers = new List<Server>();
Commands = new List<Command>();
TaskStatuses = new List<AsyncStatus>();
MessageTokens = new List<MessageToken>();
ClientSvc = new ClientService();
AliasSvc = new AliasService();
PenaltySvc = new PenaltyService();
PrivilegedClients = new Dictionary<int, Player>();
Api = new EventApi();
ServerEventOccurred += Api.OnServerEvent;
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
StartTime = DateTime.UtcNow;
OnEvent = new ManualResetEventSlim();
}
public IList<Server> GetServers()
{
return Servers;
}
public IList<Command> GetCommands()
{
return Commands;
}
public static ApplicationManager GetInstance()
{
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
{
c.Password,
c.PasswordSalt,
c.ClientId,
c.Level,
c.Name
});
foreach (var a in ipList)
{
try
{
PrivilegedClients.Add(a.ClientId, new Player()
{
Name = a.Name,
ClientId = a.ClientId,
Level = a.Level,
PasswordSalt = a.PasswordSalt,
Password = a.Password
});
}
catch (ArgumentException)
{
continue;
}
}
#endregion
#region CONFIG
var config = ConfigHandler.Configuration();
// copy over default config if it doesn't exist
if (config == null)
{
var defaultConfig = new BaseConfigurationHandler<DefaultConfiguration>("DefaultSettings").Configuration();
ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
var newConfig = ConfigHandler.Configuration();
newConfig.AutoMessagePeriod = defaultConfig.AutoMessagePeriod;
newConfig.AutoMessages = defaultConfig.AutoMessages;
newConfig.GlobalRules = defaultConfig.GlobalRules;
newConfig.Maps = defaultConfig.Maps;
if (newConfig.Servers == null)
{
ConfigHandler.Set(newConfig);
newConfig.Servers = new List<ServerConfiguration>();
do
{
newConfig.Servers.Add((ServerConfiguration)new ServerConfiguration().Generate());
} while (Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["SETUP_SERVER_SAVE"]));
config = newConfig;
await ConfigHandler.Save();
}
}
else if (config != null)
{
if (string.IsNullOrEmpty(config.Id))
{
config.Id = Guid.NewGuid().ToString();
await ConfigHandler.Save();
}
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
{
config.WebfrontBindUrl = "http://127.0.0.1:1624";
await ConfigHandler.Save();
}
}
else if (config.Servers.Count == 0)
throw new ServerException("A server configuration in IW4MAdminSettings.json is invalid");
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Utilities.EncodingType = Encoding.GetEncoding(!string.IsNullOrEmpty(config.CustomParserEncoding) ? config.CustomParserEncoding : "windows-1252");
#endregion
#region PLUGINS
SharedLibraryCore.Plugins.PluginImporter.Load(this);
foreach (var Plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
{
try
{
await Plugin.OnLoadAsync(this);
}
catch (Exception e)
{
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_PLUGIN"]} {Plugin.Name}");
Logger.WriteDebug($"Exception: {e.Message}");
Logger.WriteDebug($"Stack Trace: {e.StackTrace}");
}
}
#endregion
#region COMMANDS
if (ClientSvc.GetOwners().Result.Count == 0)
Commands.Add(new COwner());
Commands.Add(new CQuit());
Commands.Add(new CKick());
Commands.Add(new CSay());
Commands.Add(new CTempBan());
Commands.Add(new CBan());
Commands.Add(new CWhoAmI());
Commands.Add(new CList());
Commands.Add(new CHelp());
Commands.Add(new CFastRestart());
Commands.Add(new CMapRotate());
Commands.Add(new CSetLevel());
Commands.Add(new CUsage());
Commands.Add(new CUptime());
Commands.Add(new CWarn());
Commands.Add(new CWarnClear());
Commands.Add(new CUnban());
Commands.Add(new CListAdmins());
Commands.Add(new CLoadMap());
Commands.Add(new CFindPlayer());
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());
Commands.Add(new CListAlias());
Commands.Add(new CExecuteRCON());
Commands.Add(new CPlugins());
Commands.Add(new CIP());
Commands.Add(new CMask());
Commands.Add(new CPruneAdmins());
Commands.Add(new CKillServer());
Commands.Add(new CSetPassword());
Commands.Add(new CPing());
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
Commands.Add(C);
#endregion
#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);
await ServerInstance.Initialize();
lock (_servers)
{
_servers.Add(ServerInstance);
}
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($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_UNFIXABLE"]} [{Conf.IPAddress}:{Conf.Port}]");
if (e.GetType() == typeof(DvarException))
Logger.WriteDebug($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"]} {(e as DvarException).Data["dvar_name"]} ({Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR_HELP"]})");
else if (e.GetType() == typeof(NetworkException))
{
Logger.WriteDebug(e.Message);
}
// throw the exception to the main method to stop before instantly exiting
throw e;
}
}
await Task.WhenAll(config.Servers.Select(c => Init(c)).ToArray());
#endregion
}
private async Task SendHeartbeat(object state)
{
var heartbeatState = (HeartbeatState)state;
while (Running)
{
if (!heartbeatState.Connected)
{
try
{
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)
{
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)
{
heartbeatState.Connected = false;
}
}
catch (Exception e)
{
Logger.WriteWarning($"Could not send heartbeat - {e.Message}");
}
}
await Task.Delay(30000);
}
}
public async Task Start()
{
// 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)
{
try
{
await newEvent.Owner.ExecuteEvent(newEvent);
#if DEBUG
Logger.WriteDebug("Processed Event");
#endif
}
// 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)
await S.Broadcast("^1" + Utilities.CurrentLocalization.LocalizationIndex["BROADCAST_OFFLINE"]);
#endif
_servers.Clear();
}
public void Stop()
{
Running = false;
// trigger the event processing loop to end
SetHasEvent();
}
public ILogger GetLogger()
{
return Logger;
}
public IList<MessageToken> GetMessageTokens()
{
return MessageTokens;
}
public IList<Player> GetActiveClients()
{
var ActiveClients = new List<Player>();
foreach (var server in _servers)
ActiveClients.AddRange(server.Players.Where(p => p != null));
return ActiveClients;
}
public ClientService GetClientService() => ClientSvc;
public AliasService GetAliasService() => AliasSvc;
public PenaltyService GetPenaltyService() => PenaltySvc;
public IConfigurationHandler<ApplicationConfiguration> GetApplicationSettings() => ConfigHandler;
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

@ -1,90 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace IW4MAdmin.Application.Migration
{
/// <summary>
/// helps facilitate the migration of configs from one version and or location
/// to another
/// </summary>
class ConfigurationMigration
{
/// <summary>
/// ensures required directories are created
/// </summary>
public static void CheckDirectories()
{
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Plugins")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Plugins"));
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Database")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Database"));
}
if (!Directory.Exists(Path.Join(Utilities.OperatingDirectory, "Log")))
{
Directory.CreateDirectory(Path.Join(Utilities.OperatingDirectory, "Log"));
}
}
/// <summary>
/// moves existing configs from the root folder into a configs folder
/// </summary>
public static void MoveConfigFolder10518(ILogger log)
{
string currentDirectory = Utilities.OperatingDirectory;
// we don't want to do this for migrations or tests where the
// property isn't initialized or it's wrong
if (currentDirectory != null)
{
string configDirectory = Path.Join(currentDirectory, "Configuration");
if (!Directory.Exists(configDirectory))
{
log?.WriteDebug($"Creating directory for configs {configDirectory}");
Directory.CreateDirectory(configDirectory);
}
var configurationFiles = Directory.EnumerateFiles(currentDirectory, "*.json")
.Select(f => f.Split(Path.DirectorySeparatorChar).Last())
.Where(f => f.Count(c => c == '.') == 1);
foreach (var configFile in configurationFiles)
{
log?.WriteDebug($"Moving config file {configFile}");
string destinationPath = Path.Join("Configuration", configFile);
if (!File.Exists(destinationPath))
{
File.Move(configFile, destinationPath);
}
}
if (!File.Exists(Path.Join("Database", "Database.db")) &&
File.Exists("Database.db"))
{
log?.WriteDebug("Moving database file");
File.Move("Database.db", Path.Join("Database", "Database.db"));
}
}
}
public static void ModifyLogPath020919(SharedLibraryCore.Configuration.ServerConfiguration config)
{
if (config.ManualLogPath.IsRemoteLog())
{
config.GameLogServerUrl = new Uri(config.ManualLogPath);
config.ManualLogPath = null;
}
}
}
}

View File

@ -1,127 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace IW4MAdmin.Application
{
class Logger : ILogger
{
enum LogType
{
Verbose,
Info,
Debug,
Warning,
Error,
Assert
}
readonly string FileName;
readonly SemaphoreSlim OnLogWriting;
static readonly short MAX_LOG_FILES = 10;
public Logger(string fn)
{
FileName = Path.Join(Utilities.OperatingDirectory, "Log", $"{fn}.log");
OnLogWriting = new SemaphoreSlim(1, 1);
RotateLogs();
}
/// <summary>
/// rotates logs when log is initialized
/// </summary>
private void RotateLogs()
{
string maxLog = FileName + MAX_LOG_FILES;
if (File.Exists(maxLog))
{
File.Delete(maxLog);
}
for (int i = MAX_LOG_FILES - 1; i >= 0; i--)
{
string logToMove = i == 0 ? FileName : FileName + i;
string movedLogName = FileName + (i + 1);
if (File.Exists(logToMove))
{
File.Move(logToMove, movedLogName);
}
}
}
void Write(string msg, LogType type)
{
OnLogWriting.Wait();
string stringType = type.ToString();
try
{
stringType = Utilities.CurrentLocalization.LocalizationIndex[$"GLOBAL_{type.ToString().ToUpper()}"];
}
catch (Exception) { }
string LogLine = $"[{DateTime.Now.ToString("MM.dd.yyy HH:mm:ss.fff")}] - {stringType}: {msg}";
try
{
#if DEBUG
// lets keep it simple and dispose of everything quickly as logging wont be that much (relatively)
Console.WriteLine(LogLine);
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
//Debug.WriteLine(msg);
#else
if (type == LogType.Error || type == LogType.Verbose)
{
Console.WriteLine(LogLine);
}
File.AppendAllText(FileName, $"{LogLine}{Environment.NewLine}");
#endif
}
catch (Exception ex)
{
Console.WriteLine("Well.. It looks like your machine can't event write to the log file. That's something else...");
Console.WriteLine(ex.GetExceptionInfo());
}
OnLogWriting.Release(1);
}
public void WriteVerbose(string msg)
{
Write(msg, LogType.Verbose);
}
public void WriteDebug(string msg)
{
Write(msg, LogType.Debug);
}
public void WriteError(string msg)
{
Write(msg, LogType.Error);
}
public void WriteInfo(string msg)
{
Write(msg, LogType.Info);
}
public void WriteWarning(string msg)
{
Write(msg, LogType.Warning);
}
public void WriteAssert(bool condition, string msg)
{
if (!condition)
Write(msg, LogType.Assert);
}
}
}

View File

@ -1,26 +0,0 @@
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application
{
/// <summary>
/// implementatin of IPageList that supports basic
/// pages title and page location for webfront
/// </summary>
class PageList : IPageList
{
/// <summary>
/// Pages dictionary
/// Key = page name
/// Value = page location (url)
/// </summary>
public IDictionary<string, string> Pages { get; set; }
public PageList()
{
Pages = new Dictionary<string, string>();
}
}
}

View File

@ -1,100 +0,0 @@
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
namespace IW4MAdmin.Application.Misc
{
class TokenAuthentication : ITokenAuthentication
{
private readonly ConcurrentDictionary<long, TokenState> _tokens;
private readonly RNGCryptoServiceProvider _random;
private readonly static TimeSpan _timeoutPeriod = new TimeSpan(0, 0, 120);
private const short TOKEN_LENGTH = 4;
public TokenAuthentication()
{
_tokens = new ConcurrentDictionary<long, TokenState>();
_random = new RNGCryptoServiceProvider();
}
public bool AuthorizeToken(long networkId, string token)
{
bool authorizeSuccessful = _tokens.ContainsKey(networkId) && _tokens[networkId].Token == token;
if (authorizeSuccessful)
{
_tokens.TryRemove(networkId, out TokenState _);
}
return authorizeSuccessful;
}
public TokenState GenerateNextToken(long networkId)
{
TokenState state = null;
if (_tokens.ContainsKey(networkId))
{
state = _tokens[networkId];
if ((DateTime.Now - state.RequestTime) > _timeoutPeriod)
{
_tokens.TryRemove(networkId, out TokenState _);
}
else
{
return state;
}
}
state = new TokenState()
{
NetworkId = networkId,
Token = _generateToken(),
TokenDuration = _timeoutPeriod
};
_tokens.TryAdd(networkId, state);
// perform some housekeeping so we don't have built up tokens if they're not ever used
foreach (var (key, value) in _tokens)
{
if ((DateTime.Now - value.RequestTime) > _timeoutPeriod)
{
_tokens.TryRemove(key, out TokenState _);
}
}
return state;
}
public string _generateToken()
{
bool validCharacter(char c)
{
// this ensure that the characters are 0-9, A-Z, a-z
return (c > 47 && c < 58) || (c > 64 && c < 91) || (c > 96 && c < 123);
}
StringBuilder token = new StringBuilder();
while (token.Length < TOKEN_LENGTH)
{
byte[] charSet = new byte[1];
_random.GetBytes(charSet);
if (validCharacter((char)charSet[0]))
{
token.Append((char)charSet[0]);
}
}
_random.Dispose();
return token.ToString();
}
}
}

View File

@ -0,0 +1,42 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Application.Misc
{
public class VPNCheck
{
public static async Task<bool> UsingVPN(string ip, string apiKey)
{
#if DEBUG
return await Task.FromResult(false);
#else
try
{
using (var RequestClient = new System.Net.Http.HttpClient())
{
RequestClient.DefaultRequestHeaders.Add("X-Key", apiKey);
string response = await RequestClient.GetStringAsync($"http://v2.api.iphub.info/ip/{ip}");
var responseJson = JsonConvert.DeserializeObject<JObject>(response);
int blockType = Convert.ToInt32(responseJson["block"]);
/*if (responseJson.ContainsKey("isp"))
{
if (responseJson["isp"].ToString() == "TSF-IP-CORE")
return true;
}*/
return blockType == 1;
}
}
catch (Exception)
{
return false;
}
#endif
}
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Release</Configuration>
<TargetFramework>netcoreapp2.0</TargetFramework>
<PublishDir>C:\Projects\IW4M-Admin\Publish\Windows</PublishDir>
</PropertyGroup>
</Project>

View File

@ -1,185 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using static SharedLibraryCore.Server;
namespace IW4MAdmin.Application.RconParsers
{
#if DEBUG
public class BaseRConParser : IRConParser
#else
class BaseRConParser : IRConParser
#endif
{
public BaseRConParser()
{
Configuration = new DynamicRConParserConfiguration()
{
CommandPrefixes = new CommandPrefix()
{
Tell = "tell {0} {1}",
Say = "say {0}",
Kick = "clientkick {0} \"{1}\"",
Ban = "clientkick {0} \"{1}\"",
TempBan = "tempbanclient {0} \"{1}\"",
RConCommand = "ÿÿÿÿrcon {0} {1}",
RConGetDvar = "ÿÿÿÿrcon {0} {1}",
RConSetDvar = "ÿÿÿÿrcon {0} set {1}",
RConGetStatus = "ÿÿÿÿgetstatus",
RConGetInfo = "ÿÿÿÿgetinfo",
RConResponse = "ÿÿÿÿprint",
},
};
Configuration.Status.Pattern = @"^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$";
Configuration.Status.AddMapping(ParserRegex.GroupType.RConClientNumber, 1);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConScore, 2);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConPing, 3);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConNetworkId, 4);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7);
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n(?:latched: \"(.+)?\"\n)? *(.+)$";
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarLatchedValue, 4);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDomain, 5);
}
public IRConParserConfiguration Configuration { get; set; }
public virtual string Version { get; set; } = "CoD";
public Game GameName { get; set; } = Game.COD;
public bool CanGenerateLogPath { get; set; } = true;
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
{
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
return response.Skip(1).ToArray();
}
public async Task<Dvar<T>> GetDvarAsync<T>(Connection connection, string dvarName)
{
string[] lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName);
string response = string.Join('\n', lineSplit.Skip(1));
var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (!lineSplit[0].Contains(Configuration.CommandPrefixes.RConResponse) ||
response.Contains("Unknown command") ||
!match.Success)
{
throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName));
}
string value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value.StripColors();
string defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value.StripColors();
string latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value.StripColors();
return new Dvar<T>()
{
Name = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarName]].Value.StripColors(),
Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)),
DefaultValue = string.IsNullOrEmpty(defaultValue) ? default : (T)Convert.ChangeType(defaultValue, typeof(T)),
LatchedValue = string.IsNullOrEmpty(latchedValue) ? default : (T)Convert.ChangeType(latchedValue, typeof(T)),
Domain = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDomain]].Value.StripColors()
};
}
public virtual async Task<List<EFClient>> GetStatusAsync(Connection connection)
{
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS);
return ClientsFromStatus(response);
}
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
{
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, $"{dvarName} {dvarValue}")).Length > 0;
}
private List<EFClient> ClientsFromStatus(string[] Status)
{
List<EFClient> StatusPlayers = new List<EFClient>();
if (Status.Length < 4)
{
throw new ServerException("Unexpected status response received");
}
int validMatches = 0;
foreach (string statusLine in Status)
{
string responseLine = statusLine.Trim();
var regex = Regex.Match(responseLine, Configuration.Status.Pattern, RegexOptions.IgnoreCase);
if (regex.Success)
{
validMatches++;
int clientNumber = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]].Value);
int score = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]].Value);
int ping = 999;
// their state can be CNCT, ZMBI etc
if (regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value.Length <= 3)
{
ping = int.Parse(regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Value);
}
long networkId;
try
{
networkId = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]].Value.ConvertGuidToLong();
}
catch (FormatException)
{
continue;
}
string name = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].Value.StripColors().Trim();
int? ip = regex.Groups[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Value.Split(':')[0].ConvertToIP();
var client = new EFClient()
{
CurrentAlias = new EFAlias()
{
Name = name,
IPAddress = ip
},
NetworkId = networkId,
ClientNumber = clientNumber,
Ping = ping,
Score = score,
State = EFClient.ClientState.Connecting
};
#if DEBUG
if (client.NetworkId < 1000 && client.NetworkId > 0)
{
client.IPAddress = 2147483646;
client.Ping = 0;
}
#endif
StatusPlayers.Add(client);
}
}
// this happens if status is requested while map is rotating
if (Status.Length > 5 && validMatches == 0)
{
throw new ServerException("Server is rotating map");
}
return StatusPlayers;
}
}}

View File

@ -1,10 +0,0 @@
namespace IW4MAdmin.Application.RconParsers
{
/// <summary>
/// empty implementation of the IW4RConParser
/// allows script plugins to generate dynamic RCon parsers
/// </summary>
sealed internal class DynamicRConParser : BaseRConParser
{
}
}

View File

@ -1,18 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.RCon;
namespace IW4MAdmin.Application.RconParsers
{
/// <summary>
/// generic implementation of the IRConParserConfiguration
/// allows script plugins to generate dynamic RCon configurations
/// </summary>
sealed internal class DynamicRConParserConfiguration : IRConParserConfiguration
{
public CommandPrefix CommandPrefixes { get; set; }
public ParserRegex Status { get; set; } = new ParserRegex();
public ParserRegex Dvar { get; set; } = new ParserRegex();
public bool WaitForResponse { get; set; } = true;
}
}

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

@ -0,0 +1,138 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using SharedLibraryCore;
using SharedLibraryCore.RCon;
using SharedLibraryCore.Exceptions;
namespace Application.RconParsers
{
class IW4RConParser : IRConParser
{
private static CommandPrefix Prefixes = new CommandPrefix()
{
Tell = "tellraw {0} {1}",
Say = "sayraw {0}",
Kick = "clientkick {0} \"{1}\"",
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)
{
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
return response.Skip(1).ToArray();
}
public async Task<Dvar<T>> GetDvarAsync<T>(Connection connection, string dvarName)
{
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.DVAR, dvarName);
if (LineSplit.Length < 3)
{
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
e.Data["dvar_name"] = dvarName;
throw e;
}
string[] ValueSplit = LineSplit[1].Split(new char[] { '"' }, StringSplitOptions.RemoveEmptyEntries);
if (ValueSplit.Length < 5)
{
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
e.Data["dvar_name"] = dvarName;
throw e;
}
string DvarName = Regex.Replace(ValueSplit[0], @"\^[0-9]", "");
string DvarCurrentValue = Regex.Replace(ValueSplit[2], @"\^[0-9]", "");
string DvarDefaultValue = Regex.Replace(ValueSplit[4], @"\^[0-9]", "");
return new Dvar<T>(DvarName)
{
Value = (T)Convert.ChangeType(DvarCurrentValue, typeof(T))
};
}
public async Task<List<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)
{
return (await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"set {dvarName} {dvarValue}")).Length > 0;
}
public virtual CommandPrefix GetCommandPrefixes() => Prefixes;
private List<Player> ClientsFromStatus(string[] Status)
{
List<Player> StatusPlayers = new List<Player>();
if (Status.Length < 4)
throw new ServerException("Unexpected status response received");
int validMatches = 0;
foreach (String S in Status)
{
String responseLine = S.Trim();
var regex = Regex.Match(responseLine, StatusRegex, RegexOptions.IgnoreCase);
if (regex.Success)
{
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 = name,
NetworkId = networkId,
ClientNumber = clientNumber,
IPAddress = ip,
Ping = ping,
Score = score,
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

@ -0,0 +1,201 @@
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 T6MRConParser : IRConParser
{
class T6MResponse
{
public class SInfo
{
public short Com_maxclients { get; set; }
public string Game { get; set; }
public string Gametype { get; set; }
public string Mapname { get; set; }
public short NumBots { get; set; }
public short NumClients { get; set; }
public short Round { get; set; }
public string Sv_hostname { get; set; }
}
public class PInfo
{
public short Assists { get; set; }
public string Clan { get; set; }
public short Deaths { get; set; }
public short Downs { get; set; }
public short Headshots { get; set; }
public short Id { get; set; }
public bool IsBot { get; set; }
public short Kills { get; set; }
public string Name { get; set; }
public short Ping { get; set; }
public short Revives { get; set; }
public int Score { get; set; }
public long Xuid { get; set; }
public string Ip { get; set; }
}
public SInfo Info { get; set; }
public PInfo[] Players { get; set; }
}
private static CommandPrefix Prefixes = new CommandPrefix()
{
Tell = "tell {0} {1}",
Say = "say {0}",
Kick = "clientKick {0}",
Ban = "clientKick {0}",
TempBan = "clientKick {0}"
};
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)
{
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"get {dvarName}");
if (LineSplit.Length < 2)
{
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
e.Data["dvar_name"] = dvarName;
throw e;
}
string[] ValueSplit = LineSplit[1].Split(new char[] { '"' });
if (ValueSplit.Length == 0)
{
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
e.Data["dvar_name"] = dvarName;
throw e;
}
string DvarName = dvarName;
string DvarCurrentValue = Regex.Replace(ValueSplit[1], @"\^[0-9]", "");
return new Dvar<T>(DvarName)
{
Value = (T)Convert.ChangeType(DvarCurrentValue, typeof(T))
};
}
public async Task<List<Player>> GetStatusAsync(Connection connection)
{
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status");
return ClientsFromStatus(response);
//return ClientsFromResponse(connection);
}
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 async Task<List<Player>> ClientsFromResponse(Connection conn)
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri($"http://{conn.Endpoint.Address}:{conn.Endpoint.Port}/");
try
{
var parameters = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("rcon_password", conn.RConPassword)
});
var serverResponse = await client.PostAsync("/info", parameters);
var serverResponseObject = Newtonsoft.Json.JsonConvert.DeserializeObject<T6MResponse>(await serverResponse.Content.ReadAsStringAsync());
return serverResponseObject.Players.Select(p => new Player()
{
Name = p.Name,
NetworkId = p.Xuid,
ClientNumber = p.Id,
IPAddress = p.Ip.Split(':')[0].ConvertToIP(),
Ping = p.Ping,
Score = p.Score,
IsBot = p.IsBot,
}).ToList();
}
catch (HttpRequestException e)
{
throw new NetworkException(e.Message);
}
}
}
private List<Player> ClientsFromStatus(string[] status)
{
List<Player> StatusPlayers = new List<Player>();
foreach (string statusLine in status)
{
String responseLine = statusLine;
if (Regex.Matches(responseLine, @"^ *\d+", RegexOptions.IgnoreCase).Count > 0) // its a client line!
{
String[] playerInfo = responseLine.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
int clientId = -1;
int Ping = -1;
Int32.TryParse(playerInfo[3], out Ping);
var regex = Regex.Match(responseLine, @"\^7.*\ +0 ");
string name = Encoding.UTF8.GetString(Encoding.Convert(Utilities.EncodingType, Encoding.UTF8, Utilities.EncodingType.GetBytes(regex.Value.Substring(0, regex.Value.Length - 2).StripColors().Trim())));
long networkId = playerInfo[4].ConvertLong();
int.TryParse(playerInfo[0], out clientId);
regex = Regex.Match(responseLine, @"\d+\.\d+\.\d+.\d+\:\d{1,5}");
#if DEBUG
Ping = 1;
#endif
int ipAddress = regex.Value.Split(':')[0].ConvertToIP();
regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+");
int score = 0;
// todo: fix this when T6M score is valid ;)
//int score = Int32.Parse(playerInfo[1]);
var p = new Player()
{
Name = name,
NetworkId = networkId,
ClientNumber = clientId,
IPAddress = ipAddress,
Ping = Ping,
Score = score,
IsBot = networkId == 0
};
if (p.IsBot)
p.NetworkId = -p.ClientNumber;
StatusPlayers.Add(p);
}
}
return StatusPlayers;
}
}
}

1059
Application/Server.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,214 +0,0 @@
import requests
import time
import json
import collections
import os
# the following classes model the discord webhook api parameters
class WebhookAuthor():
def __init__(self, name=None, url=None, icon_url=None):
if name:
self.name = name
if url:
self.url = url
if icon_url:
self.icon_url = icon_url
class WebhookField():
def __init__(self, name=None, value=None, inline=False):
if name:
self.name = name
if value:
self.value = value
if inline:
self.inline = inline
class WebhookEmbed():
def __init__(self):
self.author = ''
self.title = ''
self.url = ''
self.description = ''
self.color = 0
self.fields = []
self.thumbnail = {}
class WebhookParams():
def __init__(self, username=None, avatar_url=None, content=None):
self.username = ''
self.avatar_url = ''
self.content = ''
self.embeds = []
# quick way to convert all the objects to a nice json object
def to_json(self):
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
# gets the relative link to a user's profile
def get_client_profile(profile_id):
return u'{}/Client/ProfileAsync/{}'.format(base_url, profile_id)
def get_client_profile_markdown(client_name, profile_id):
return u'[{}]({})'.format(client_name, get_client_profile(profile_id))
#todo: exception handling for opening the file
if os.getenv("DEBUG"):
config_file_name = 'config.dev.json'
else:
config_file_name = 'config.json'
with open(config_file_name) as json_config_file:
json_config = json.load(json_config_file)
# this should be an URL to an IP or FQN to an IW4MAdmin instance
# ie http://127.0.0.1 or http://IW4MAdmin.com
base_url = json_config['IW4MAdminUrl']
end_point = '/api/event'
request_url = base_url + end_point
# this should be the full discord webhook url
# ie https://discordapp.com/api/webhooks/<id>/<token>
discord_webhook_notification_url = json_config['DiscordWebhookNotificationUrl']
discord_webhook_information_url = json_config['DiscordWebhookInformationUrl']
# this should be the numerical id of the discord group
# 12345678912345678
notify_role_ids = json_config['NotifyRoleIds']
def get_new_events():
events = []
response = requests.get(request_url)
data = response.json()
should_notify = False
for event in data:
# commonly used event info items
event_type = event['eventType']['name']
server_name = event['ownerEntity']['name']
if event['originEntity']:
origin_client_name = event['originEntity']['name']
origin_client_id = int(event['originEntity']['id'])
if event['targetEntity']:
target_client_name = event['targetEntity']['name'] or ''
target_client_id = int(event['targetEntity']['id']) or 0
webhook_item = WebhookParams()
webhook_item_embed = WebhookEmbed()
#todo: the following don't need to be generated every time, as it says the same
webhook_item.username = 'IW4MAdmin'
webhook_item.avatar_url = 'https://raidmax.org/IW4MAdmin/img/iw4adminicon-3.png'
webhook_item_embed.color = 31436
webhook_item_embed.url = base_url
webhook_item_embed.thumbnail = { 'url' : 'https://raidmax.org/IW4MAdmin/img/iw4adminicon-3.png' }
webhook_item.embeds.append(webhook_item_embed)
# the server should be visible on all event types
server_field = WebhookField('Server', server_name)
webhook_item_embed.fields.append(server_field)
role_ids_string = ''
for id in notify_role_ids:
role_ids_string += '\r\n<@&{}>\r\n'.format(id)
if event_type == 'Report':
report_reason = event['extraInfo']
report_reason_field = WebhookField('Reason', report_reason)
reported_by_field = WebhookField('By', get_client_profile_markdown(origin_client_name, origin_client_id))
reported_field = WebhookField('Reported Player',get_client_profile_markdown(target_client_name, target_client_id))
# add each fields to the embed
webhook_item_embed.title = 'Player Reported'
webhook_item_embed.fields.append(reported_field)
webhook_item_embed.fields.append(reported_by_field)
webhook_item_embed.fields.append(report_reason_field)
should_notify = True
elif event_type == 'Ban':
ban_reason = event['extraInfo']
ban_reason_field = WebhookField('Reason', ban_reason)
banned_by_field = WebhookField('By', get_client_profile_markdown(origin_client_name, origin_client_id))
banned_field = WebhookField('Banned Player', get_client_profile_markdown(target_client_name, target_client_id))
# add each fields to the embed
webhook_item_embed.title = 'Player Banned'
webhook_item_embed.fields.append(banned_field)
webhook_item_embed.fields.append(banned_by_field)
webhook_item_embed.fields.append(ban_reason_field)
should_notify = True
elif event_type == 'Connect':
connected_field = WebhookField('Connected Player', get_client_profile_markdown(origin_client_name, origin_client_id))
webhook_item_embed.title = 'Player Connected'
webhook_item_embed.fields.append(connected_field)
elif event_type == 'Disconnect':
disconnected_field = WebhookField('Disconnected Player', get_client_profile_markdown(origin_client_name, origin_client_id))
webhook_item_embed.title = 'Player Disconnected'
webhook_item_embed.fields.append(disconnected_field)
elif event_type == 'Say':
say_client_field = WebhookField('Player', get_client_profile_markdown(origin_client_name, origin_client_id))
message_field = WebhookField('Message', event['extraInfo'])
webhook_item_embed.title = 'Message From Player'
webhook_item_embed.fields.append(say_client_field)
webhook_item_embed.fields.append(message_field)
#if event_type == 'ScriptKill' or event_type == 'Kill':
# kill_str = '{} killed {}'.format(get_client_profile_markdown(origin_client_name, origin_client_id),
# get_client_profile_markdown(target_client_name, target_client_id))
# killed_field = WebhookField('Kill Information', kill_str)
# webhook_item_embed.title = 'Player Killed'
# webhook_item_embed.fields.append(killed_field)
#todo: handle other events
else:
continue
#make sure there's at least one group to notify
if len(notify_role_ids) > 0:
# unfortunately only the content can be used to to notify members in groups
#embed content shows the role but doesn't notify
webhook_item.content = role_ids_string
events.append({'item' : webhook_item, 'notify' : should_notify})
return events
# sends the data to the webhook location
def execute_webhook(data):
for event in data:
event_json = event['item'].to_json()
url = None
if event['notify']:
url = discord_webhook_notification_url
else:
if len(discord_webhook_information_url) > 0:
url = discord_webhook_information_url
if url :
response = requests.post(url,
data=event_json,
headers={'Content-type' : 'application/json'})
# grabs new events and executes the webhook fo each valid event
def run():
failed_count = 1
print('starting polling for events')
while True:
try:
new_events = get_new_events()
execute_webhook(new_events)
except Exception as e:
print('failed to get new events ({})'.format(failed_count))
print(e)
failed_count += 1
time.sleep(5)
if __name__ == "__main__":
run()

View File

@ -1,99 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>15a81d6e-7502-46ce-8530-0647a380b5f4</ProjectGuid>
<ProjectHome>.</ProjectHome>
<StartupFile>DiscordWebhook.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<OutputPath>.</OutputPath>
<Name>DiscordWebhook</Name>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<RootNamespace>DiscordWebhook</RootNamespace>
<InterpreterId>MSBuild|env|$(MSBuildProjectFullPath)</InterpreterId>
<IsWindowsApplication>False</IsWindowsApplication>
<LaunchProvider>Standard Python launcher</LaunchProvider>
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
<Environment>DEBUG=True</Environment>
<PublishUrl>C:\Projects\IW4M-Admin\Publish\WindowsPrerelease\DiscordWebhook</PublishUrl>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Prerelease' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
<OutputPath>bin\Prerelease\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="DiscordWebhook.py" />
</ItemGroup>
<ItemGroup>
<Interpreter Include="env\">
<Id>env</Id>
<Version>3.6</Version>
<Description>env (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
</ItemGroup>
<ItemGroup>
<Content Include="config.json">
<Publish>True</Publish>
</Content>
<Content Include="requirements.txt">
<Publish>True</Publish>
</Content>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
<!-- Uncomment the CoreCompile target to enable the Build command in
Visual Studio and specify your pre- and post-build commands in
the BeforeBuild and AfterBuild targets below. -->
<!--<Target Name="CoreCompile" />-->
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<AutoAssignPort>True</AutoAssignPort>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@ -1,6 +0,0 @@
{
"IW4MAdminUrl": "",
"DiscordWebhookNotificationUrl": "",
"DiscordWebhookInformationUrl": "",
"NotifyRoleIds": []
}

View File

@ -1,7 +0,0 @@
certifi>=2018.4.16
chardet>=3.0.4
idna>=2.7
pip>=18.0
requests>=2.19.1
setuptools>=39.0.1
urllib3>=1.23

View File

@ -1,118 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>42efda12-10d3-4c40-a210-9483520116bc</ProjectGuid>
<ProjectHome>.</ProjectHome>
<ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
<StartupFile>runserver.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<LaunchProvider>Standard Python launcher</LaunchProvider>
<WebBrowserUrl>http://localhost</WebBrowserUrl>
<OutputPath>.</OutputPath>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<Name>GameLogServer</Name>
<RootNamespace>GameLogServer</RootNamespace>
<InterpreterId>MSBuild|log_env|$(MSBuildProjectFullPath)</InterpreterId>
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
<Environment>DEBUG=True</Environment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Prerelease' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
<OutputPath>bin\Prerelease\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="GameLogServer\log_reader.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="GameLogServer\restart_resource.py" />
<Compile Include="GameLogServer\server.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="runserver.py" />
<Compile Include="GameLogServer\__init__.py" />
<Compile Include="GameLogServer\log_resource.py" />
</ItemGroup>
<ItemGroup>
<Folder Include="GameLogServer\" />
</ItemGroup>
<ItemGroup>
<None Include="Stable.pubxml" />
</ItemGroup>
<ItemGroup>
<Interpreter Include="env\">
<Id>env</Id>
<Version>3.6</Version>
<Description>env (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
<Interpreter Include="log_env\">
<Id>log_env</Id>
<Version>3.6</Version>
<Description>log_env (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
</ItemGroup>
<ItemGroup>
<Content Include="requirements.txt" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
<!-- Specify pre- and post-build commands in the BeforeBuild and
AfterBuild targets below. -->
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<AutoAssignPort>True</AutoAssignPort>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@ -1,9 +0,0 @@
"""
The flask application package.
"""
from flask import Flask
from flask_restful import Api
app = Flask(__name__)
api = Api(app)

View File

@ -1,118 +0,0 @@
import re
import os
import time
import random
import string
class LogReader(object):
def __init__(self):
self.log_file_sizes = {}
# (if the time between checks is greater, ignore ) - in seconds
self.max_file_time_change = 30
def read_file(self, path, retrieval_key):
# this removes old entries that are no longer valid
try:
self._clear_old_logs()
except Exception as e:
print('could not clear old logs')
print(e)
if os.name != 'nt':
path = re.sub(r'^[A-Z]\:', '', path)
path = re.sub(r'\\+', '/', path)
# prevent traversing directories
if re.search('r^.+\.\.\\.+$', path):
return self._generate_bad_response()
# must be a valid log path and log file
if not re.search(r'^.+[\\|\/](.+)[\\|\/].+.log$', path):
return self._generate_bad_response()
# get the new file size
new_file_size = self.file_length(path)
# the log size was unable to be read (probably the wrong path)
if new_file_size < 0:
return self._generate_bad_response()
next_retrieval_key = self._generate_key()
# this is the first time the key has been requested, so we need to the next one
if retrieval_key not in self.log_file_sizes or int(time.time() - self.log_file_sizes[retrieval_key]['read']) > self.max_file_time_change:
print('retrieval key "%s" does not exist or is outdated' % retrieval_key)
last_log_info = {
'size' : new_file_size,
'previous_key' : None
}
else:
last_log_info = self.log_file_sizes[retrieval_key]
print('next key is %s' % next_retrieval_key)
expired_key = last_log_info['previous_key']
print('expired key is %s' % expired_key)
# grab the previous value
last_size = last_log_info['size']
file_size_difference = new_file_size - last_size
#print('generating info for next key %s' % next_retrieval_key)
# update the new size
self.log_file_sizes[next_retrieval_key] = {
'size' : new_file_size,
'read': time.time(),
'next_key': next_retrieval_key,
'previous_key': retrieval_key
}
if expired_key in self.log_file_sizes:
print('deleting expired key %s' % expired_key)
del self.log_file_sizes[expired_key]
#print('reading %i bytes starting at %i' % (file_size_difference, last_size))
new_log_content = self.get_file_lines(path, last_size, file_size_difference)
return {
'content': new_log_content,
'next_key': next_retrieval_key
}
def get_file_lines(self, path, start_position, length_to_read):
try:
file_handle = open(path, 'rb')
file_handle.seek(start_position)
file_data = file_handle.read(length_to_read)
file_handle.close()
# using ignore errors omits the pesky 0xb2 bytes we're reading in for some reason
return file_data.decode('utf-8', errors='ignore')
except Exception as e:
print('could not read the log file at {0}, wanted to read {1} bytes'.format(path, length_to_read))
print(e)
return False
def _clear_old_logs(self):
expired_logs = [path for path in self.log_file_sizes if int(time.time() - self.log_file_sizes[path]['read']) > self.max_file_time_change]
for key in expired_logs:
print('removing expired log with key {0}'.format(key))
del self.log_file_sizes[key]
def _generate_bad_response(self):
return {
'content': None,
'next_key': None
}
def _generate_key(self):
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
def file_length(self, path):
try:
return os.stat(path).st_size
except Exception as e:
print('could not get the size of the log file at {0}'.format(path))
print(e)
return -1
reader = LogReader()

View File

@ -1,16 +0,0 @@
from flask_restful import Resource
from GameLogServer.log_reader import reader
from base64 import urlsafe_b64decode
class LogResource(Resource):
def get(self, path, retrieval_key):
path = urlsafe_b64decode(path).decode('utf-8')
log_info = reader.read_file(path, retrieval_key)
content = log_info['content']
return {
'success' : content is not None,
'length': 0 if content is None else len(content),
'data': content,
'next_key': log_info['next_key']
}

View File

@ -1,29 +0,0 @@
#from flask_restful import Resource
#from flask import request
#import requests
#import os
#import subprocess
#import re
#def get_pid_of_server_windows(port):
# process = subprocess.Popen('netstat -aon', shell=True, stdout=subprocess.PIPE)
# output = process.communicate()[0]
# matches = re.search(' *(UDP) +([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}):'+ str(port) + ' +[^\w]*([0-9]+)', output.decode('utf-8'))
# if matches is not None:
# return matches.group(3)
# else:
# return 0
#class RestartResource(Resource):
# def get(self):
# try:
# response = requests.get('http://' + request.remote_addr + ':1624/api/restartapproved')
# if response.status_code == 200:
# pid = get_pid_of_server_windows(response.json()['port'])
# subprocess.check_output("Taskkill /PID %s /F" % pid)
# else:
# return {}, 400
# except Exception as e:
# print(e)
# return {}, 500
# return {}, 200

View File

@ -1,14 +0,0 @@
from flask import Flask
from flask_restful import Api
from .log_resource import LogResource
#from .restart_resource import RestartResource
import logging
app = Flask(__name__)
def init():
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
api = Api(app)
api.add_resource(LogResource, '/log/<string:path>/<string:retrieval_key>')
#api.add_resource(RestartResource, '/restart')

View File

@ -1,12 +0,0 @@
aniso8601==6.0.0
Click==7.0
Flask==1.0.2
Flask-RESTful==0.3.7
itsdangerous==1.1.0
Jinja2==2.10
MarkupSafe==1.1.1
pip==10.0.1
pytz==2018.9
setuptools==39.0.1
six==1.12.0
Werkzeug==0.15.2

View File

@ -1,15 +0,0 @@
"""
This script runs the GameLogServer application using a development server.
"""
from os import environ
from GameLogServer.server import app, init
if __name__ == '__main__':
HOST = environ.get('SERVER_HOST', '0.0.0.0')
try:
PORT = int(environ.get('SERVER_PORT', '1625'))
except ValueError:
PORT = 5555
init()
app.run('0.0.0.0', PORT, debug=False)

View File

@ -1,6 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28803.352
# Visual Studio 15
VisualStudioVersion = 15.0.26730.16
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8B310-269E-46D4-A612-24601F16065F}"
EndProject
@ -9,8 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
_commands.gsc = _commands.gsc
_customcallbacks.gsc = _customcallbacks.gsc
README.md = README.md
RunPublishPre.cmd = RunPublishPre.cmd
RunPublishRelease.cmd = RunPublishRelease.cmd
version.txt = version.txt
EndProjectSection
EndProject
@ -32,26 +31,7 @@ Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Master", "Master\Master.pyp
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
ProjectSection(SolutionItems) = preProject
Plugins\ScriptPlugins\ParserCoD4x.js = Plugins\ScriptPlugins\ParserCoD4x.js
Plugins\ScriptPlugins\ParserIW4x.js = Plugins\ScriptPlugins\ParserIW4x.js
Plugins\ScriptPlugins\ParserPT6.js = Plugins\ScriptPlugins\ParserPT6.js
Plugins\ScriptPlugins\ParserRektT5M.js = Plugins\ScriptPlugins\ParserRektT5M.js
Plugins\ScriptPlugins\ParserTeknoMW3.js = Plugins\ScriptPlugins\ParserTeknoMW3.js
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
EndProjectSection
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "GameLogServer", "GameLogServer\GameLogServer.pyproj", "{42EFDA12-10D3-4C40-A210-9483520116BC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{A848FCF1-8527-4AA8-A1AA-50D29695C678}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StatsWeb", "Plugins\Web\StatsWeb\StatsWeb.csproj", "{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -291,8 +271,8 @@ Global
{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 = Prerelease|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Prerelease|Any CPU.Build.0 = Prerelease|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
@ -307,76 +287,6 @@ Global
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.Build.0 = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x64.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x64.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x86.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Debug|x86.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x64.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Prerelease|x86.Build.0 = Debug|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Any CPU.Build.0 = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x64.ActiveCfg = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x64.Build.0 = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x86.ActiveCfg = Release|Any CPU
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B}.Release|x86.Build.0 = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x64.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x64.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x86.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Debug|x86.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Any CPU.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x64.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Prerelease|x86.Build.0 = Debug|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Any CPU.Build.0 = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x64.ActiveCfg = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x64.Build.0 = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x86.ActiveCfg = Release|Any CPU
{F5815359-CFC7-44B4-9A3B-C04BACAD5836}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -388,10 +298,6 @@ Global
{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}
{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {26E8B310-269E-46D4-A612-24601F16065F}
{A848FCF1-8527-4AA8-A1AA-50D29695C678} = {26E8B310-269E-46D4-A612-24601F16065F}
{776B348B-F818-4A0F-A625-D0AF8BAD3E9B} = {A848FCF1-8527-4AA8-A1AA-50D29695C678}
{F5815359-CFC7-44B4-9A3B-C04BACAD5836} = {26E8B310-269E-46D4-A612-24601F16065F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

View File

@ -11,13 +11,13 @@
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<LaunchProvider>Standard Python launcher</LaunchProvider>
<LaunchProvider>Web launcher</LaunchProvider>
<WebBrowserUrl>http://localhost</WebBrowserUrl>
<OutputPath>.</OutputPath>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<Name>Master</Name>
<RootNamespace>Master</RootNamespace>
<InterpreterId>MSBuild|env_master|$(MSBuildProjectFullPath)</InterpreterId>
<InterpreterId>MSBuild|dev_env|$(MSBuildProjectFullPath)</InterpreterId>
<IsWindowsApplication>False</IsWindowsApplication>
<PythonRunWebServerCommand>
</PythonRunWebServerCommand>
@ -25,7 +25,6 @@
</PythonDebugWebServerCommand>
<PythonRunWebServerCommandType>script</PythonRunWebServerCommandType>
<PythonDebugWebServerCommandType>script</PythonDebugWebServerCommandType>
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
@ -74,9 +73,6 @@
<Compile Include="master\resources\null.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="Master\resources\server.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\resources\version.py">
<SubType>Code</SubType>
</Compile>
@ -112,18 +108,17 @@
<Folder Include="master\templates\" />
</ItemGroup>
<ItemGroup>
<None Include="FolderProfile.pubxml" />
<Content Include="master\config\master.json" />
<Content Include="master\templates\serverlist.html" />
<None Include="Release.pubxml" />
<Content Include="requirements.txt" />
<Content Include="master\templates\index.html" />
<Content Include="master\templates\layout.html" />
</ItemGroup>
<ItemGroup>
<Interpreter Include="env_master\">
<Id>env_master</Id>
<Interpreter Include="dev_env\">
<Id>dev_env</Id>
<Version>3.6</Version>
<Description>env_master (Python 3.6 (64-bit))</Description>
<Description>dev_env (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>

View File

@ -15,9 +15,9 @@ class Base():
self.scheduler.start()
self.scheduler.add_job(
func=self._remove_staleinstances,
trigger=IntervalTrigger(seconds=60),
trigger=IntervalTrigger(seconds=120),
id='stale_instance_remover',
name='Remove stale instances if no heartbeat in 60 seconds',
name='Remove stale instances if no heartbeat in 120 seconds',
replace_existing=True
)
self.scheduler.add_job(
@ -41,7 +41,7 @@ class Base():
def _remove_staleinstances(self):
for key, value in list(self.instance_list.items()):
if int(time.time()) - value.last_heartbeat > 60:
if int(time.time()) - value.last_heartbeat > 120:
print('[_remove_staleinstances] removing stale instance {id}'.format(id=key))
del self.instance_list[key]
del self.token_list[key]

View File

@ -1,16 +1,14 @@
class ServerModel(object):
def __init__(self, id, port, game, hostname, clientnum, maxclientnum, map, gametype, ip, version):
def __init__(self, id, port, game, hostname, clientnum, maxclientnum, map, gametype):
self.id = id
self.port = port
self.version = version
self.game = game
self.hostname = hostname
self.clientnum = clientnum
self.maxclientnum = maxclientnum
self.map = map
self.gametype = gametype
self.ip = ip
def __repr__(self):
return '<ServerModel(id={id})>'.format(id=self.id)

View File

@ -8,11 +8,10 @@ import datetime
class Authenticate(Resource):
def post(self):
instance_id = request.json['id']
#todo: see why this is failing
#if ctx.get_token(instance_id) is not False:
# return { 'message' : 'that id already has a token'}, 401
#else:
expires = datetime.timedelta(days=30)
if ctx.get_token(instance_id) is not False:
return { 'message' : 'that id already has a token'}, 401
else:
expires = datetime.timedelta(days=1)
token = create_access_token(instance_id, expires_delta=expires)
ctx.add_token(instance_id, token)
return { 'access_token' : token }, 200

View File

@ -4,8 +4,6 @@ from flask_jwt_extended import jwt_required
from marshmallow import ValidationError
from master.schema.instanceschema import InstanceSchema
from master import ctx
import json
from netaddr import IPAddress
class Instance(Resource):
def get(self, id=None):
@ -23,11 +21,6 @@ class Instance(Resource):
@jwt_required
def put(self, id):
try:
for server in request.json['servers']:
if 'ip' not in server or IPAddress(server['ip']).is_private() or IPAddress(server['ip']).is_loopback():
server['ip'] = request.remote_addr
if 'version' not in server:
server['version'] = 'Unknown'
instance = InstanceSchema().load(request.json)
except ValidationError as err:
return {'message' : err.messages }, 400
@ -37,11 +30,6 @@ class Instance(Resource):
@jwt_required
def post(self):
try:
for server in request.json['servers']:
if 'ip' not in server or server['ip'] == 'localhost':
server['ip'] = request.remote_addr
if 'version' not in server:
server['version'] = 'Unknown'
instance = InstanceSchema().load(request.json)
except ValidationError as err:
return {'message' : err.messages }, 400

View File

@ -1,6 +0,0 @@
from flask_restful import Resource
class Server(Resource):
"""description of class"""

View File

@ -6,7 +6,6 @@ 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
from master.resources.server import Server
api.add_resource(Null, '/null')
api.add_resource(Instance, '/instance/', '/instance/<string:id>')
@ -14,4 +13,3 @@ api.add_resource(Version, '/version')
api.add_resource(Authenticate, '/authenticate')
api.add_resource(HistoryGraph, '/history/', '/history/<int:history_count>')
api.add_resource(Localization, '/localization/', '/localization/<string:language_tag>')
api.add_resource(Server, '/server')

View File

@ -4,26 +4,19 @@ from master.models.servermodel import ServerModel
class ServerSchema(Schema):
id = fields.Int(
required=True,
validate=validate.Range(0, 25525525525565535, 'invalid id')
)
ip = fields.Str(
required=True
validate=validate.Range(1, 2147483647, 'invalid id')
)
port = fields.Int(
required=True,
validate=validate.Range(1, 65535, 'invalid port')
)
version = fields.String(
required=False,
validate=validate.Length(0, 128, 'invalid server version')
validate=validate.Range(1, 665535, 'invalid port')
)
game = fields.String(
required=True,
validate=validate.Length(1, 5, 'invalid game name')
validate=validate.Length(1, 8, 'invalid game name')
)
hostname = fields.String(
required=True,
validate=validate.Length(1, 64, 'invalid hostname')
validate=validate.Length(1, 48, 'invalid hostname')
)
clientnum = fields.Int(
required=True,
@ -35,7 +28,7 @@ class ServerSchema(Schema):
)
map = fields.String(
required=True,
validate=validate.Length(0, 64, 'invalid map name')
validate=validate.Length(1, 32, 'invalid map name')
)
gametype = fields.String(
required=True,

View File

@ -39,7 +39,6 @@
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -1,113 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<!-- todo: move this! -->
<style>
.server-row {
cursor: pointer;
}
.modal-content, .nav-item {
background-color: #212529;
color: #fff;
}
.modal-header, .modal-footer {
border-color: #32383e !important;
}
.modal-dark button.close, a.nav-link {
color: #fff;
}
</style>
<div class="modal modal-dark" id="serverModalCenter" tabindex="-1" role="dialog" aria-labelledby="serverModalCenterTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="serverModalTitle">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="h5" id="server_socket"></div>
</div>
<div class="modal-footer">
<button id="connect_button" type="button" class="btn btn-dark">Connect</button>
<button type="button" class="btn btn-dark" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<nav>
<div class="nav nav-tabs" id="server_game_tabs" role="tablist">
{% for game in games %}
<a class="nav-item nav-link {{'active' if loop.first else ''}}" id="{{game}}_servers_tab" data-toggle="tab" href="#{{game}}_servers" role="tab" aria-controls="{{game}}_servers" aria-selected="{{'true' if loop.first else 'false' }}">{{game}}</a>
{% endfor %}
</div>
</nav>
<div class="tab-content" id="server_game_tabs_content">
{% for game, servers in games.items() %}
<div class="tab-pane {{'show active' if loop.first else ''}}" id="{{game}}_servers" role="tabpanel" aria-labelledby="{{game}}_servers_tab">
<table class="table table-dark table-striped table-hover table-responsive-lg">
<thead>
<tr>
<th>Server Name</th>
<th>Map Name</th>
<th>Players</th>
<th>Mode</th>
<th class="text-center">Connect</th>
</tr>
</thead>
<tbody>
{% for server in servers %}
<tr class="server-row" data-toggle="modal" data-target="#serverModalCenter"
data-ip="{{server.ip}}" data-port="{{server.port}}">
<td data-hostname="{{server.hostname}}" class="server-hostname">{{server.hostname}}</td>
<td data-map="{{server.map}} " class="server-map">{{server.map}}</td>
<td data-clientnum="{{server.clientnum}}" data-maxclientnum="{{server.maxclientnum}}"
class="server-clientnum">
{{server.clientnum}}/{{server.maxclientnum}}
</td>
<td data-gametype="{{server.gametype}}" class="server-gametype">{{server.gametype}}</td>
<td class="text-center"><span class="oi oi-play-circle"></span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
</div>
<div class="w-100 small text-right text-muted">
<span>Developed by RaidMax</span><br />
<span>PRERELEASE</span>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(() => {
$('.server-row').off('click');
$('.server-row').on('click', function (e) {
$('#serverModalTitle').text($(this).find('.server-hostname').text());
$('#server_socket').text(`/connect ${$(this).data('ip')}:${$(this).data('port')}`);
});
$('#connect_button').off('click');
$('#connect_button').on('click', (e) => alert('soon...'));
});
</script>
{% endblock %}

View File

@ -4,9 +4,8 @@ Routes and views for the flask application.
from datetime import datetime
from flask import render_template
from master import app, ctx
from master import app
from master.resources.history_graph import HistoryGraph
from collections import defaultdict
@app.route('/')
def home():
@ -20,16 +19,3 @@ def home():
client_count = _history_graph[0]['client_count'],
server_count = _history_graph[0]['server_count']
)
@app.route('/servers')
def servers():
servers = defaultdict(list)
if len(ctx.instance_list.values()) > 0:
ungrouped_servers = [server for instance in ctx.instance_list.values() for server in instance.servers]
for server in ungrouped_servers:
servers[server.game].append(server)
return render_template(
'serverlist.html',
title = 'Server List',
games = servers
)

View File

@ -1,26 +1,16 @@
aniso8601==3.0.2
APScheduler==3.5.3
certifi==2018.10.15
chardet==3.0.4
aniso8601==3.0.0
click==6.7
Flask==1.0.2
Flask==0.12.2
Flask-JWT==0.3.2
Flask-JWT-Extended==3.8.1
Flask-RESTful==0.3.6
idna==2.7
itsdangerous==0.24
Jinja2==2.10
MarkupSafe==1.0
marshmallow==3.0.0b8
pip==9.0.3
psutil==5.4.8
pygal==2.4.0
pip==9.0.1
PyJWT==1.4.2
pytz==2018.7
requests==2.20.0
setuptools==40.5.0
pytz==2018.4
setuptools==39.0.1
six==1.11.0
timeago==1.0.8
tzlocal==1.5.1
urllib3==1.24
Werkzeug==0.14.1

View File

@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<TargetLatestRuntimePatch >true</TargetLatestRuntimePatch>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
<Exec Command="copy &quot;$(TargetDir)Microsoft.SyndicationFeed.ReaderWriter.dll&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
</Target>
</Project>

View File

@ -1,29 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
namespace AutomessageFeed
{
class Configuration : IBaseConfiguration
{
public bool EnableFeed { get; set; }
public string FeedUrl { get; set; }
public int MaxFeedItems { get; set; }
public IBaseConfiguration Generate()
{
EnableFeed = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_ENABLE"]);
if (EnableFeed)
{
FeedUrl = Utilities.PromptString(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_URL"]);
MaxFeedItems = Utilities.PromptInt(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_MAXITEMS"],
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_PROMPT_MAXITEMS_DESC"],
0, int.MaxValue, 0);
}
return this;
}
public string Name() => "AutomessageFeedConfiguration";
}
}

View File

@ -1,85 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Threading.Tasks;
using Microsoft.SyndicationFeed.Rss;
using SharedLibraryCore.Configuration;
using System.Xml;
using Microsoft.SyndicationFeed;
using System.Collections.Generic;
using SharedLibraryCore.Helpers;
using System.Text.RegularExpressions;
namespace AutomessageFeed
{
public class Plugin : IPlugin
{
public string Name => "Automessage Feed";
public float Version => (float)Utilities.GetVersionAsDouble();
public string Author => "RaidMax";
private Configuration _configuration;
private int _currentFeedItem;
private async Task<string> GetNextFeedItem(Server server)
{
var items = new List<string>();
using (var reader = XmlReader.Create(_configuration.FeedUrl, new XmlReaderSettings() { Async = true }))
{
var feedReader = new RssFeedReader(reader);
while (await feedReader.Read())
{
switch (feedReader.ElementType)
{
case SyndicationElementType.Item:
var item = await feedReader.ReadItem();
items.Add(Regex.Replace(item.Title, @"\<.+\>.*\</.+\>", ""));
break;
}
}
}
if (_currentFeedItem < items.Count && (_configuration.MaxFeedItems == 0 || _currentFeedItem < _configuration.MaxFeedItems))
{
_currentFeedItem++;
return items[_currentFeedItem - 1];
}
_currentFeedItem = 0;
return Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_AUTOMESSAGEFEED_NO_ITEMS"];
}
public Task OnEventAsync(GameEvent E, Server S)
{
return Task.CompletedTask;
}
public async Task OnLoadAsync(IManager manager)
{
var cfg = new BaseConfigurationHandler<Configuration>("AutomessageFeedPluginSettings");
if (cfg.Configuration() == null)
{
cfg.Set((Configuration)new Configuration().Generate());
await cfg.Save();
}
_configuration = cfg.Configuration();
manager.GetMessageTokens().Add(new MessageToken("FEED", GetNextFeedItem));
}
public Task OnTickAsync(Server S)
{
throw new NotImplementedException();
}
public Task OnUnloadAsync()
{
return Task.CompletedTask;
}
}
}

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4ScriptCommands
{
class CommandInfo
{
public string Command { get; set; }
public int ClientNumber { get; set; }
public List<string> CommandArguments { get; set; } = new List<string>();
public override string ToString() => $"{Command};{ClientNumber},{string.Join(',', CommandArguments)}";
}
}

View File

@ -1,199 +1,19 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SharedLibraryCore.Objects;
using System.Threading.Tasks;
namespace IW4ScriptCommands.Commands
{
class Balance
class Balance : Command
{
private class TeamAssignment
public Balance() : base("balance", "balance teams", "bal", Player.Permission.Trusted, false, null)
{
public IW4MAdmin.Plugins.Stats.IW4Info.Team CurrentTeam { get; set; }
public int Num { get; set; }
public IW4MAdmin.Plugins.Stats.Models.EFClientStatistics Stats { get; set; }
}
public static string GetTeamAssignments(EFClient client, bool isDisconnect, Server server, string teamsString = "")
public override async Task ExecuteAsync(GameEvent E)
{
var scriptClientTeams = teamsString.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(c => c.Split(','))
.Select(c => new TeamAssignment()
{
CurrentTeam = (IW4MAdmin.Plugins.Stats.IW4Info.Team)Enum.Parse(typeof(IW4MAdmin.Plugins.Stats.IW4Info.Team), c[1]),
Num = server.GetClientsAsList().FirstOrDefault(p => p.ClientNumber == Int32.Parse(c[0]))?.ClientNumber ?? -1,
Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(server.Clients.FirstOrDefault(p => p.ClientNumber == Int32.Parse(c[0])).ClientId, server.EndPoint)
})
.ToList();
// at least one team is full so we can't balance
if (scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis) >= Math.Floor(server.MaxClients / 2.0)
|| scriptClientTeams.Count(ct => ct.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies) >= Math.Floor(server.MaxClients / 2.0))
{
// E.Origin?.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL"]);
return string.Empty;
}
List<string> teamAssignments = new List<string>();
var _c = server.GetClientsAsList();
if (isDisconnect && client != null)
{
_c = _c.Where(c => c.ClientNumber != client.ClientNumber).ToList();
}
var activeClients = _c.Select(c => new TeamAssignment()
{
Num = c.ClientNumber,
Stats = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, server.EndPoint),
CurrentTeam = IW4MAdmin.Plugins.Stats.Plugin.Manager.GetClientStats(c.ClientId, server.EndPoint).Team
})
.Where(c => scriptClientTeams.FirstOrDefault(sc => sc.Num == c.Num)?.CurrentTeam != IW4MAdmin.Plugins.Stats.IW4Info.Team.Spectator)
.Where(c => c.CurrentTeam != scriptClientTeams.FirstOrDefault(p => p.Num == c.Num)?.CurrentTeam)
.OrderByDescending(c => c.Stats.Performance)
.ToList();
var alliesTeam = scriptClientTeams
.Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies)
.Where(c => activeClients.Count(t => t.Num == c.Num) == 0)
.ToList();
var axisTeam = scriptClientTeams
.Where(c => c.CurrentTeam == IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis)
.Where(c => activeClients.Count(t => t.Num == c.Num) == 0)
.ToList();
while (activeClients.Count() > 0)
{
int teamSizeDifference = alliesTeam.Count - axisTeam.Count;
double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 -
axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0;
if (teamSizeDifference == 0)
{
if (performanceDisparity == 0)
{
alliesTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
if (performanceDisparity > 0)
{
axisTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
alliesTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
}
}
else if (teamSizeDifference > 0)
{
if (performanceDisparity > 0)
{
axisTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
axisTeam.Add(activeClients.Last());
activeClients.RemoveAt(activeClients.Count - 1);
}
}
else
{
if (performanceDisparity > 0)
{
alliesTeam.Add(activeClients.First());
activeClients.RemoveAt(0);
}
else
{
alliesTeam.Add(activeClients.Last());
activeClients.RemoveAt(activeClients.Count - 1);
}
}
}
alliesTeam = alliesTeam.OrderByDescending(t => t.Stats.Performance)
.ToList();
axisTeam = axisTeam.OrderByDescending(t => t.Stats.Performance)
.ToList();
while (Math.Abs(alliesTeam.Count - axisTeam.Count) > 1)
{
int teamSizeDifference = alliesTeam.Count - axisTeam.Count;
double performanceDisparity = alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0 -
axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0;
if (teamSizeDifference > 0)
{
if (performanceDisparity > 0)
{
axisTeam.Add(alliesTeam.First());
alliesTeam.RemoveAt(0);
}
else
{
axisTeam.Add(alliesTeam.Last());
alliesTeam.RemoveAt(axisTeam.Count - 1);
}
}
else
{
if (performanceDisparity > 0)
{
alliesTeam.Add(axisTeam.Last());
axisTeam.RemoveAt(axisTeam.Count - 1);
}
else
{
alliesTeam.Add(axisTeam.First());
axisTeam.RemoveAt(0);
}
}
}
foreach (var assignment in alliesTeam)
{
teamAssignments.Add($"{assignment.Num},2");
assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Allies;
}
foreach (var assignment in axisTeam)
{
teamAssignments.Add($"{assignment.Num},3");
assignment.Stats.Team = IW4MAdmin.Plugins.Stats.IW4Info.Team.Axis;
}
//if (alliesTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0 &&
// axisTeam.Count(ac => scriptClientTeams.First(sc => sc.Num == ac.Num).CurrentTeam != ac.CurrentTeam) == 0)
//{
// //E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_BALANCE_FAIL_BALANCED"]);
// return string.Empty;
//}
//if (E.Origin?.Level > Player.Permission.Administrator)
//{
// E.Origin.Tell($"Allies Elo: {(alliesTeam.Count > 0 ? alliesTeam.Average(t => t.Stats.Performance) : 0)}");
// E.Origin.Tell($"Axis Elo: {(axisTeam.Count > 0 ? axisTeam.Average(t => t.Stats.Performance) : 0)}");
//}
//E.Origin.Tell("Balance command sent");
string args = string.Join(",", teamAssignments);
return args;
await E.Owner.ExecuteCommandAsync("sv_iw4madmin_command balance");
await E.Origin.Tell("Balance command sent");
}
}
}

View File

@ -1,53 +0,0 @@
using IW4ScriptCommands.Commands;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WebfrontCore.Controllers.API
{
[Route("api/gsc/[action]")]
public class GscApiController : ApiController
{
[HttpGet("{networkId}")]
public IActionResult ClientInfo(string networkId)
{
var clientInfo = Manager.GetActiveClients()
.FirstOrDefault(c => c.NetworkId == networkId.ConvertGuidToLong());
if (clientInfo != null)
{
var sb = new StringBuilder();
sb.AppendLine($"admin={clientInfo.IsPrivileged()}");
sb.AppendLine($"level={(int)clientInfo.Level}");
sb.AppendLine($"levelstring={clientInfo.Level.ToLocalizedLevelName()}");
sb.AppendLine($"connections={clientInfo.Connections}");
sb.AppendLine($"authenticated={clientInfo.GetAdditionalProperty<bool>("IsLoggedIn") == true}");
return Content(sb.ToString());
}
return Content("");
}
[HttpGet("{networkId}")]
public IActionResult GetTeamAssignments(string networkId, int serverId, string teams = "", bool isDisconnect = false)
{
return Unauthorized();
var client = Manager.GetActiveClients()
.FirstOrDefault(c => c.NetworkId == networkId.ConvertGuidToLong());
var server = Manager.GetServers().First(c => c.EndPoint == serverId);
teams = teams ?? string.Empty;
string assignments = Balance.GetTeamAssignments(client, isDisconnect, server, teams);
return Content(assignments);
}
}
}

View File

@ -1,13 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<TargetFramework>netcoreapp2.0</TargetFramework>
<ApplicationIcon />
<StartupObject />
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
@ -15,11 +12,11 @@
</Target>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\Stats\Stats.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
</ItemGroup>
</Project>

View File

@ -15,30 +15,7 @@ namespace IW4ScriptCommands
public string Author => "RaidMax";
public Task OnEventAsync(GameEvent E, Server S)
{
if (E.Type == GameEvent.EventType.Start)
{
return S.SetDvarAsync("sv_iw4madmin_serverid", S.EndPoint);
}
if (E.Type == GameEvent.EventType.Warn)
{
return S.SetDvarAsync("sv_iw4madmin_command", new CommandInfo()
{
ClientNumber = E.Target.ClientNumber,
Command = "alert",
CommandArguments = new List<string>()
{
"Warning",
"ui_mp_nukebomb_timer",
E.Data
}
}.ToString());
}
return Task.CompletedTask;
}
public Task OnEventAsync(GameEvent E, Server S) => Task.CompletedTask;
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;

View File

@ -1,39 +1,37 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace IW4MAdmin.Plugins.Login.Commands
{
public class CLogin : Command
{
public CLogin() : base("login", Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_DESC"], "li", EFClient.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 = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
Required = true
}
})
{ }
}){ }
public override async Task ExecuteAsync(GameEvent E)
{
bool success = E.Owner.Manager.TokenAuthenticator.AuthorizeToken(E.Origin.NetworkId, E.Data);
var client = E.Owner.Manager.GetPrivilegedClients()[E.Origin.ClientId];
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, client.PasswordSalt));
if (!success)
{
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, E.Origin.PasswordSalt));
success = hashedPassword[0] == E.Origin.Password;
}
if (success)
if (hashedPassword[0] == client.Password)
{
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]);
}
_ = success ?
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
else
{
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
}
}
}
}

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<TargetFramework>netcoreapp2.0</TargetFramework>
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.Login</PackageId>
@ -11,7 +10,6 @@
<Company>Forever None</Company>
<Product>Login Plugin for IW4MAdmin</Product>
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -19,9 +17,11 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -3,9 +3,9 @@ using System.Reflection;
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Plugins.Login
{
@ -22,13 +22,12 @@ namespace IW4MAdmin.Plugins.Login
public Task OnEventAsync(GameEvent E, Server S)
{
if (E.IsRemote || Config.RequirePrivilegedClientLogin == false)
if (E.Remote || Config.RequirePrivilegedClientLogin == false)
return Task.CompletedTask;
if (E.Type == GameEvent.EventType.Connect)
{
AuthorizedClients.TryAdd(E.Origin.ClientId, false);
E.Origin.SetAdditionalProperty("IsLoggedIn", false);
}
if (E.Type == GameEvent.EventType.Disconnect)
@ -38,28 +37,23 @@ namespace IW4MAdmin.Plugins.Login
if (E.Type == GameEvent.EventType.Command)
{
if (E.Origin.Level < EFClient.Permission.Moderator ||
E.Origin.Level == EFClient.Permission.Console)
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.Origin?.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(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_AUTH"]);
}
else
{
E.Origin.SetAdditionalProperty("IsLoggedIn", true);
}
}
return Task.CompletedTask;
}

View File

@ -16,9 +16,9 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
{
OffensiveWords = new List<string>()
{
@"\s*n+.*i+.*g+.*e+.*r+\s*",
@"\s*n+.*i+.*g+.*a+\s*",
@"\s*f+u+.*c+.*k+.*\s*"
"nigger",
"nigga",
"fuck"
};
var loc = Utilities.CurrentLocalization.LocalizationIndex;

View File

@ -1,10 +1,11 @@
using System.Linq;
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Plugins.ProfanityDeterment
{
@ -17,72 +18,68 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
public string Author => "RaidMax";
BaseConfigurationHandler<Configuration> Settings;
ConcurrentDictionary<int, Tracking> ProfanityCounts;
IManager Manager;
public Task OnEventAsync(GameEvent E, Server S)
public async Task OnEventAsync(GameEvent E, Server S)
{
if (!Settings.Configuration().EnableProfanityDeterment)
return Task.CompletedTask;
return;
if (E.Type == GameEvent.EventType.Connect)
{
E.Origin.SetAdditionalProperty("_profanityInfringements", 0);
if (!ProfanityCounts.TryAdd(E.Origin.ClientId, new Tracking(E.Origin)))
{
S.Logger.WriteWarning("Could not add client to profanity tracking");
}
var objectionalWords = Settings.Configuration().OffensiveWords;
bool containsObjectionalWord = objectionalWords.FirstOrDefault(w => E.Origin.Name.ToLower().Contains(w)) != null;
// we want to run regex against it just incase
if (!containsObjectionalWord)
{
foreach (string word in objectionalWords)
{
containsObjectionalWord |= Regex.IsMatch(E.Origin.Name.ToLower(), word, RegexOptions.IgnoreCase);
}
}
if (containsObjectionalWord)
{
E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, Utilities.IW4MAdminClient(E.Owner));
await E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, new Player()
{
ClientId = 1
});
};
}
if (E.Type == GameEvent.EventType.Disconnect)
{
E.Origin.SetAdditionalProperty("_profanityInfringements", 0);
if (!ProfanityCounts.TryRemove(E.Origin.ClientId, out Tracking old))
{
S.Logger.WriteWarning("Could not remove client from profanity tracking");
}
}
if (E.Type == GameEvent.EventType.Say)
{
var objectionalWords = Settings.Configuration().OffensiveWords;
bool containsObjectionalWord = false;
foreach (string word in objectionalWords)
{
containsObjectionalWord |= Regex.IsMatch(E.Data.ToLower(), word, RegexOptions.IgnoreCase);
// break out early because there's at least one objectional word
if (containsObjectionalWord)
{
break;
}
}
bool containsObjectionalWord = objectionalWords.FirstOrDefault(w => E.Data.ToLower().Contains(w)) != null;
if (containsObjectionalWord)
{
int profanityInfringments = E.Origin.GetAdditionalProperty<int>("_profanityInfringements");
if (profanityInfringments >= Settings.Configuration().KickAfterInfringementCount)
var clientProfanity = ProfanityCounts[E.Origin.ClientId];
if (clientProfanity.Infringements >= Settings.Configuration().KickAfterInfringementCount)
{
E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, Utilities.IW4MAdminClient(E.Owner));
await clientProfanity.Client.Kick(Settings.Configuration().ProfanityKickMessage, new Player()
{
ClientId = 1
});
}
else if (profanityInfringments < Settings.Configuration().KickAfterInfringementCount)
else if (clientProfanity.Infringements < Settings.Configuration().KickAfterInfringementCount)
{
E.Origin.SetAdditionalProperty("_profanityInfringements", profanityInfringments + 1);
E.Origin.Warn(Settings.Configuration().ProfanityWarningMessage, Utilities.IW4MAdminClient(E.Owner));
clientProfanity.Infringements++;
await clientProfanity.Client.Warn(Settings.Configuration().ProfanityWarningMessage, new Player()
{
ClientId = 1
});
}
}
}
return Task.CompletedTask;
}
public async Task OnLoadAsync(IManager manager)
@ -94,6 +91,9 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
Settings.Set((Configuration)new Configuration().Generate());
await Settings.Save();
}
ProfanityCounts = new ConcurrentDictionary<int, Tracking>();
Manager = manager;
}
public Task OnTickAsync(Server S) => Task.CompletedTask;

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<TargetFramework>netcoreapp2.0</TargetFramework>
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.ProfanityDeterment</PackageId>
@ -13,13 +12,14 @@
<Description>Warns and kicks players for using profanity</Description>
<Copyright>2018</Copyright>
<Configurations>Debug;Release;Prerelease</Configurations>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Plugins.ProfanityDeterment
{
class Tracking
{
public Player Client { get; private set; }
public int Infringements { get; set; }
public Tracking(Player client)
{
Client = client;
Infringements = 0;
}
}
}

View File

@ -1,38 +0,0 @@
var rconParser;
var eventParser;
var plugin = {
author: 'FrenchFry, RaidMax',
version: 0.5,
name: 'CoD4x Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser();
eventParser = manager.GenerateDynamicEventParser();
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){16,32}|(?:[a-z]|[0-9]){32}|bot[0-9]+) ([0-9+]) *(.{0,32}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$'
rconParser.Configuration.Status.AddMapping(104, 6); // RConName
rconParser.Configuration.Status.AddMapping(105, 8); // RConIPAddress
rconParser.Configuration.Dvar.Pattern = '^"(.+)" is: "(.+)?" default: "(.+)?" info: "(.+)?"$';
rconParser.Configuration.Dvar.AddMapping(109, 2); // DVAR latched value
rconParser.Configuration.Dvar.AddMapping(110, 4); // dvar info
rconParser.Version = 'CoD4 X - win_mingw-x86 build 963 Mar 12 2019';
rconParser.GameName = 1; // IW3
eventParser.Configuration.GameDirectory = 'main';
eventParser.Version = 'CoD4 X - win_mingw-x86 build 963 Mar 12 2019';
eventParser.GameName = 1; // IW3
eventParser.URLProtocolFormat = 'cod4://{{ip}}:{{port}}';
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -1,36 +0,0 @@
var rconParser;
var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.3,
name: 'IW4 Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser();
eventParser = manager.GenerateDynamicEventParser();
rconParser.Configuration.CommandPrefixes.Tell = 'tellraw {0} {1}';
rconParser.Configuration.CommandPrefixes.Say = 'sayraw {0}';
rconParser.Configuration.CommandPrefixes.Kick = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'tempbanclient {0} "{1}"';
eventParser.Configuration.GameDirectory = 'userraw';
rconParser.Version = 'IW4x (v0.6.0)';
rconParser.GameName = 2; // IW4x
eventParser.Version = 'IW4x (v0.6.0)';
eventParser.GameName = 2; // IW4x
eventParser.URLProtocolFormat = 'iw4x://{{ip}}:{{port}}';
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -1,50 +0,0 @@
var rconParser;
var eventParser;
var plugin = {
author: 'RaidMax, Xerxes',
version: 0.4,
name: 'Plutonium T6 Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser();
eventParser = manager.GenerateDynamicEventParser();
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
rconParser.Configuration.CommandPrefixes.Say = 'say {0}';
rconParser.Configuration.CommandPrefixes.Kick = 'clientkick_for_reason {0} "{1}"';
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick_for_reason {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick_for_reason {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConGetDvar = '\xff\xff\xff\xffrcon {0} get {1}';
rconParser.Configuration.Dvar.Pattern = '^(.+) is "(.+)?"$';
rconParser.Configuration.Dvar.AddMapping(106, 1);
rconParser.Configuration.Dvar.AddMapping(107, 2);
rconParser.Configuration.WaitForResponse = false;
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$';
rconParser.Configuration.Status.AddMapping(100, 1);
rconParser.Configuration.Status.AddMapping(101, 2);
rconParser.Configuration.Status.AddMapping(102, 3);
rconParser.Configuration.Status.AddMapping(103, 4);
rconParser.Configuration.Status.AddMapping(104, 5);
rconParser.Configuration.Status.AddMapping(105, 6);
eventParser.Configuration.GameDirectory = 't6r\\data';
rconParser.Version = 'Call of Duty Multiplayer - Ship COD_T6_S MP build 1.0.44 CL(1759941) CODPCAB2 CEG Fri May 9 19:19:19 2014 win-x86 813e66d5';
rconParser.GameName = 7; // T6
eventParser.Version = 'Call of Duty Multiplayer - Ship COD_T6_S MP build 1.0.44 CL(1759941) CODPCAB2 CEG Fri May 9 19:19:19 2014 win-x86 813e66d5';
eventParser.GameName = 7; // T6
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -1,34 +0,0 @@
var rconParser;
var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.1,
name: 'RektT5m Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser();
eventParser = manager.GenerateDynamicEventParser();
eventParser.Configuration.GameDirectory = 'data';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff\1print';
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
rconParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
rconParser.GameName = 6; // T5
eventParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
eventParser.GameName = 6; // T5
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -1,43 +0,0 @@
var rconParser;
var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.2,
name: 'Tekno MW3 Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser();
eventParser = manager.GenerateDynamicEventParser();
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[A-Z]|[0-9]){16,32})\t +(.{0,16}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+\\:-?\\d{1,5}|loopback) *$';
rconParser.Configuration.Status.AddMapping(104, 5); // RConName
rconParser.Configuration.Status.AddMapping(103, 4); // RConNetworkId
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xff';
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
rconParser.Configuration.CommandPrefixes.Say = 'say {0}';
rconParser.Configuration.CommandPrefixes.Kick = 'dropclient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.Ban = 'dropclient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'tempbanclient {0} "{1}"';
rconParser.Configuration.Dvar.AddMapping(107, 1); // RCon DvarValue
rconParser.Configuration.Dvar.Pattern = '^(.*)$';
rconParser.Version = 'IW5 MP 1.4 build 382 latest Thu Jan 19 2012 11:09:49AM win-x86';
rconParser.GameName = 3; // IW5
rconParser.CanGenerateLogPath = false;
eventParser.Configuration.GameDirectory = 'scripts';
eventParser.Version = 'IW5 MP 1.4 build 382 latest Thu Jan 19 2012 11:09:49AM win-x86';
eventParser.GameName = 3; // IW5
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -1,38 +0,0 @@
var plugin = {
author: 'RaidMax',
version: 1.1,
name: 'Shared GUID Kicker Plugin',
onEventAsync: function (gameEvent, server) {
// make sure we only check for IW4(x)
if (server.GameName !== 2) {
return false;
}
// connect or join event
if (gameEvent.Type === 3) {
// this GUID seems to have been packed in a IW4 torrent and results in an unreasonable amount of people using the same GUID
if (gameEvent.Origin.NetworkId === -805366929435212061 ||
gameEvent.Origin.NetworkId === 3150799945255696069 ||
gameEvent.Origin.NetworkId === 5859032128210324569 ||
gameEvent.Origin.NetworkId === 2908745942105435771 ||
gameEvent.Origin.NetworkId === -6492697076432899192 ||
gameEvent.Origin.NetworkId === 1145760003260769995 ||
gameEvent.Origin.NetworkId === -7102887284306116957 ||
gameEvent.Origin.NetworkId === 3474936520447289592 ||
gameEvent.Origin.NetworkId === -1168897558496584395 ||
gameEvent.Origin.NetworkId === 8348020621355817691 ||
gameEvent.Origin.NetworkId === 3259219574061214058) {
gameEvent.Origin.Kick('Your GUID is generic. Delete players/guids.dat and rejoin', _IW4MAdminClient);
}
}
},
onLoadAsync: function (manager) {
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -1,62 +0,0 @@
var plugin = {
author: 'RaidMax',
version: 1.2,
name: 'VPN Detection Plugin',
manager: null,
logger: null,
vpnExceptionIds: [],
checkForVpn: function (origin) {
var exempt = false;
// prevent players that are exempt from being kicked
this.vpnExceptionIds.forEach(function (id) {
if (id === origin.ClientId) {
exempt = true;
return false;
}
});
if (exempt) {
return;
}
var usingVPN = false;
try {
var cl = new System.Net.Http.HttpClient();
var re = cl.GetAsync('https://api.xdefcon.com/proxy/check/?ip=' + origin.IPAddressString).Result;
var co = re.Content;
var parsedJSON = JSON.parse(co.ReadAsStringAsync().Result);
co.Dispose();
re.Dispose();
cl.Dispose();
usingVPN = parsedJSON.success && parsedJSON.proxy;
} catch (e) {
this.logger.WriteWarning('There was a problem checking client IP for VPN ' + e.message);
}
if (usingVPN) {
this.logger.WriteInfo(origin + ' is using a VPN (' + origin.IPAddressString + ')');
origin.Kick(_localization.LocalizationIndex["SERVER_KICK_VPNS_NOTALLOWED"], _IW4MAdminClient);
}
},
onEventAsync: function (gameEvent, server) {
// join event
if (gameEvent.Type === 4) {
this.checkForVpn(gameEvent.Origin);
}
},
onLoadAsync: function (manager) {
this.manager = manager;
this.logger = manager.GetLogger(0);
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -1,13 +1,11 @@
using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using IW4MAdmin.Plugins.Stats.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace IW4MAdmin.Plugins.Stats.Cheat
{
@ -18,158 +16,121 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Bone,
Chest,
Offset,
Strain,
Recoil
Strain
};
public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
public const int MIN_HITS_TO_RUN_DETECTION = 5;
private const int MIN_ANGLE_COUNT = 5;
public List<EFClientKill> TrackedHits { get; set; }
int Kills;
int HitCount;
Dictionary<IW4Info.HitLocation, HitInfo> HitLocationCount;
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
ChangeTracking Tracker;
double AngleDifferenceAverage;
EFClientStatistics ClientStats;
DateTime LastHit;
long LastOffset;
IW4Info.WeaponName LastWeapon;
ILogger Log;
Strain Strain;
readonly DateTime ConnectionTime = DateTime.UtcNow;
private double sessionAverageRecoilAmount;
private EFClientKill lastHit;
private int validRecoilHitCount;
private class HitInfo
{
public int Count { get; set; }
public double Offset { get; set; }
};
public Detection(ILogger log, EFClientStatistics clientStats)
{
Log = log;
HitLocationCount = new Dictionary<IW4Info.HitLocation, HitInfo>();
HitLocationCount = new Dictionary<IW4Info.HitLocation, int>();
foreach (var loc in Enum.GetValues(typeof(IW4Info.HitLocation)))
{
HitLocationCount.Add((IW4Info.HitLocation)loc, new HitInfo());
}
HitLocationCount.Add((IW4Info.HitLocation)loc, 0);
ClientStats = clientStats;
Strain = new Strain();
Tracker = new ChangeTracking<EFACSnapshot>();
TrackedHits = new List<EFClientKill>();
Tracker = new ChangeTracking();
}
/// <summary>
/// Analyze kill and see if performed by a cheater
/// </summary>
/// <param name="hit">kill performed by the player</param>
/// <param name="kill">kill performed by the player</param>
/// <returns>true if detection reached thresholds, false otherwise</returns>
public DetectionPenaltyResult ProcessHit(EFClientKill hit, bool isDamage)
{
var results = new List<DetectionPenaltyResult>();
if ((hit.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
hit.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
hit.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
hit.HitLoc == IW4Info.HitLocation.none || hit.TimeOffset - LastOffset < 0 ||
// hack: prevents false positives
(LastWeapon != hit.Weapon && (hit.TimeOffset - LastOffset) == 50))
public DetectionPenaltyResult ProcessKill(EFClientKill kill, bool isDamage)
{
if ((kill.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
kill.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
kill.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
kill.HitLoc == IW4Info.HitLocation.none)
return new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Any,
ClientPenalty = Penalty.PenaltyType.Any,
};
}
LastWeapon = hit.Weapon;
HitLocationCount[hit.HitLoc].Count++;
HitCount++;
DetectionPenaltyResult result = null;
if (LastHit == DateTime.MinValue)
LastHit = DateTime.UtcNow;
HitLocationCount[kill.HitLoc]++;
if (!isDamage)
{
Kills++;
}
HitCount++;
#region VIEWANGLES
if (hit.AnglesList.Count >= 2)
if (kill.AnglesList.Count >= 2)
{
double realAgainstPredict = Vector3.ViewAngleDistance(hit.AnglesList[0], hit.AnglesList[1], hit.ViewAngles);
double realAgainstPredict = Vector3.ViewAngleDistance(kill.AnglesList[0], kill.AnglesList[1], kill.ViewAngles);
// LIFETIME
var hitLoc = ClientStats.HitLocations
.First(hl => hl.Location == hit.HitLoc);
.First(hl => hl.Location == kill.HitLoc);
float previousAverage = hitLoc.HitOffsetAverage;
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + realAgainstPredict) / hitLoc.HitCount;
hitLoc.HitOffsetAverage = (float)newAverage;
int totalHits = ClientStats.HitLocations.Sum(_hit => _hit.HitCount);
var weightedLifetimeAverage = ClientStats.HitLocations.Where(_hit => _hit.HitCount > 0)
.Sum(_hit => _hit.HitOffsetAverage * _hit.HitCount) / totalHits;
if (weightedLifetimeAverage > Thresholds.MaxOffset(totalHits) &&
if (hitLoc.HitOffsetAverage > Thresholds.MaxOffset &&
hitLoc.HitCount > 100)
{
Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***");
Log.WriteDebug($"Lifetime Average = {newAverage}");
Log.WriteDebug($"Bone = {hitLoc.Location}");
Log.WriteDebug($"HitCount = {hitLoc.HitCount}");
Log.WriteDebug($"ID = {hit.AttackerId}");
Log.WriteDebug($"ID = {kill.AttackerId}");
results.Add(new DetectionPenaltyResult()
result = new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
ClientPenalty = Penalty.PenaltyType.Ban,
Value = hitLoc.HitOffsetAverage,
HitCount = hitLoc.HitCount,
Type = DetectionType.Offset
});
};
}
// SESSION
var sessionHitLoc = HitLocationCount[hit.HitLoc];
sessionHitLoc.Offset = (sessionHitLoc.Offset * (sessionHitLoc.Count - 1) + realAgainstPredict) / sessionHitLoc.Count;
double sessAverage = (AngleDifferenceAverage * (HitCount - 1) + realAgainstPredict) / HitCount;
AngleDifferenceAverage = sessAverage;
int totalSessionHits = HitLocationCount.Sum(_hit => _hit.Value.Count);
var weightedSessionAverage = HitLocationCount.Where(_hit => _hit.Value.Count > 0)
.Sum(_hit => _hit.Value.Offset * _hit.Value.Count) / totalSessionHits;
AngleDifferenceAverage = weightedSessionAverage;
if (weightedSessionAverage > Thresholds.MaxOffset(totalSessionHits) &&
totalSessionHits >= (Thresholds.MediumSampleMinKills * 2))
if (sessAverage > Thresholds.MaxOffset &&
HitCount > 30)
{
Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***");
Log.WriteDebug($"Session Average = {weightedSessionAverage}");
Log.WriteDebug($"Session Average = {sessAverage}");
Log.WriteDebug($"HitCount = {HitCount}");
Log.WriteDebug($"ID = {hit.AttackerId}");
Log.WriteDebug($"ID = {kill.AttackerId}");
results.Add(new DetectionPenaltyResult()
result = new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = weightedSessionAverage,
ClientPenalty = Penalty.PenaltyType.Ban,
Value = sessAverage,
HitCount = HitCount,
Type = DetectionType.Offset,
Location = hitLoc.Location
});
};
}
#if DEBUG
Log.WriteDebug($"PredictVsReal={realAgainstPredict}");
#endif
}
#endregion
#region STRAIN
double currentStrain = Strain.GetStrain(hit.Distance / 0.0254, hit.ViewAngles, Math.Max(50, hit.TimeOffset - LastOffset));
#if DEBUG == true
Log.WriteDebug($"Current Strain: {currentStrain}");
#endif
LastOffset = hit.TimeOffset;
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)
{
@ -179,48 +140,31 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
// flag
if (currentStrain > Thresholds.MaxStrainFlag)
{
results.Add(new DetectionPenaltyResult()
result = new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Flag,
ClientPenalty = Penalty.PenaltyType.Flag,
Value = currentStrain,
HitCount = HitCount,
Type = DetectionType.Strain
});
};
}
// ban
if (currentStrain > Thresholds.MaxStrainBan &&
HitCount >= 5)
if (currentStrain > Thresholds.MaxStrainBan)
{
results.Add(new DetectionPenaltyResult()
result = new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
ClientPenalty = Penalty.PenaltyType.Ban,
Value = currentStrain,
HitCount = HitCount,
Type = DetectionType.Strain
});
};
}
#endregion
#region RECOIL
float hitRecoilAverage = 0;
if (!Plugin.Config.Configuration().RecoilessWeapons.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex)))
{
validRecoilHitCount++;
hitRecoilAverage = (hit.AnglesList.Sum(_angle => _angle.Z) + hit.ViewAngles.Z) / (hit.AnglesList.Count + 1);
sessionAverageRecoilAmount = (sessionAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount;
#if DEBUG
Log.WriteDebug($"Current Strain: {currentStrain}");
#endif
if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && sessionAverageRecoilAmount == 0)
{
results.Add(new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
Value = sessionAverageRecoilAmount,
HitCount = HitCount,
Type = DetectionType.Recoil
});
}
}
#endregion
#region SESSION_RATIOS
@ -230,44 +174,64 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
// determine what the max headshot percentage can be for current number of kills
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.5), Thresholds.HeadshotRatioThresholdHighSample(3.5), 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
double maxBoneRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(2.25), Thresholds.BoneRatioThresholdHighSample(2.25), lerpAmount) + marginOfError;
double maxBoneRatioLerpValueForBan = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(3.25), Thresholds.BoneRatioThresholdHighSample(3.25), lerpAmount) + marginOfError;
// calculate headshot ratio
double currentHeadshotRatio = ((HitLocationCount[IW4Info.HitLocation.head].Count + HitLocationCount[IW4Info.HitLocation.helmet].Count + HitLocationCount[IW4Info.HitLocation.neck].Count) / (double)HitCount);
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.Count / (double)HitCount).Max());
var bone = HitLocationCount.FirstOrDefault(b => b.Value.Count == HitLocationCount.Values.Max(_hit => _hit.Count)).Key;
double currentMaxBoneRatio = (HitLocationCount.Values.Select(v => v / (double)HitCount).Max());
var bone = HitLocationCount.FirstOrDefault(b => b.Value == HitLocationCount.Values.Max()).Key;
#region HEADSHOT_RATIO
// flag on headshot
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
{
// ban on headshot
if (currentHeadshotRatio > maxHeadshotLerpValueForBan)
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
{
results.Add(new DetectionPenaltyResult()
Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**");
Log.WriteDebug($"ClientId: {kill.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
Log.WriteDebug($"**Ratio {currentHeadshotRatio}");
Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
Log.WriteDebug(sb.ToString());
result = new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
ClientPenalty = Penalty.PenaltyType.Ban,
Value = currentHeadshotRatio,
Location = IW4Info.HitLocation.head,
HitCount = HitCount,
Type = DetectionType.Bone
});
};
}
else
{
results.Add(new DetectionPenaltyResult()
Log.WriteDebug("**Maximum Headshot Ratio Reached For Flag**");
Log.WriteDebug($"ClientId: {kill.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
Log.WriteDebug($"**Ratio {currentHeadshotRatio}");
Log.WriteDebug($"**MaxRatio {maxHeadshotLerpValueForFlag}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
Log.WriteDebug(sb.ToString());
result = new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Flag,
ClientPenalty = Penalty.PenaltyType.Flag,
Value = currentHeadshotRatio,
Location = IW4Info.HitLocation.head,
HitCount = HitCount,
Type = DetectionType.Bone
});
};
}
}
#endregion
@ -279,32 +243,52 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
// ban on bone ratio
if (currentMaxBoneRatio > maxBoneRatioLerpValueForBan)
{
results.Add(new DetectionPenaltyResult()
Log.WriteDebug("**Maximum Bone Ratio Reached For Ban**");
Log.WriteDebug($"ClientId: {kill.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
Log.WriteDebug($"**Ratio {currentMaxBoneRatio}");
Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForBan}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
Log.WriteDebug(sb.ToString());
result = new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
ClientPenalty = Penalty.PenaltyType.Ban,
Value = currentMaxBoneRatio,
Location = bone,
HitCount = HitCount,
Type = DetectionType.Bone
});
};
}
else
{
results.Add(new DetectionPenaltyResult()
Log.WriteDebug("**Maximum Bone Ratio Reached For Flag**");
Log.WriteDebug($"ClientId: {kill.AttackerId}");
Log.WriteDebug($"**HitCount: {HitCount}");
Log.WriteDebug($"**Ratio {currentMaxBoneRatio}");
Log.WriteDebug($"**MaxRatio {maxBoneRatioLerpValueForFlag}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
Log.WriteDebug(sb.ToString());
result = new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Flag,
ClientPenalty = Penalty.PenaltyType.Flag,
Value = currentMaxBoneRatio,
Location = bone,
HitCount = HitCount,
Type = DetectionType.Bone
});
};
}
}
#endregion
}
#region CHEST_ABDOMEN_RATIO_SESSION
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count;
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper];
if (chestHits >= Thresholds.MediumSampleMinKills)
{
@ -314,75 +298,142 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(3), Thresholds.ChestAbdomenRatioThresholdHighSample(3), lerpAmount) + marginOfError;
double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdLowSample(4), Thresholds.ChestAbdomenRatioThresholdHighSample(4), lerpAmount) + marginOfError;
double currentChestAbdomenRatio = HitLocationCount[IW4Info.HitLocation.torso_upper].Count / (double)HitLocationCount[IW4Info.HitLocation.torso_lower].Count;
double currentChestAbdomenRatio = HitLocationCount[IW4Info.HitLocation.torso_upper] / (double)HitLocationCount[IW4Info.HitLocation.torso_lower];
if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag)
{
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan && chestHits >= Thresholds.MediumSampleMinKills * 2)
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan && chestHits >= Thresholds.MediumSampleMinKills + 30)
{
results.Add(new DetectionPenaltyResult()
Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Ban**");
Log.WriteDebug($"ClientId: {kill.AttackerId}");
Log.WriteDebug($"**Chest Hits: {chestHits}");
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
Log.WriteDebug(sb.ToString());
result = new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Ban,
ClientPenalty = Penalty.PenaltyType.Ban,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
Type = DetectionType.Chest,
HitCount = chestHits
});
};
}
else
{
results.Add(new DetectionPenaltyResult()
Log.WriteDebug("**Maximum Chest/Abdomen Ratio Reached For Flag**");
Log.WriteDebug($"ClientId: {kill.AttackerId}");
Log.WriteDebug($"**Chest Hits: {chestHits}");
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}");
var sb = new StringBuilder();
foreach (var kvp in HitLocationCount)
sb.Append($"HitLocation: {kvp.Key} -> {kvp.Value}\r\n");
Log.WriteDebug(sb.ToString());
// Log.WriteDebug($"ThresholdReached: {AboveThresholdCount}");
result = new DetectionPenaltyResult()
{
ClientPenalty = EFPenalty.PenaltyType.Flag,
ClientPenalty = Penalty.PenaltyType.Flag,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
Type = DetectionType.Chest,
HitCount = chestHits
});
};
}
}
}
#endregion
#endregion
var snapshot = new EFACSnapshot()
Tracker.OnChange(new DetectionTracking(ClientStats, kill, Strain));
if (result != null)
{
When = hit.When,
ClientId = ClientStats.ClientId,
SessionAngleOffset = AngleDifferenceAverage,
RecoilOffset = hitRecoilAverage,
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds,
CurrentStrain = currentStrain,
CurrentViewAngle = hit.ViewAngles,
Hits = HitCount,
Kills = Kills,
Deaths = ClientStats.SessionDeaths,
HitDestinationId = hit.DeathOrigin.Vector3Id,
HitDestination = hit.DeathOrigin,
HitOriginId = hit.KillOrigin.Vector3Id,
HitOrigin = hit.KillOrigin,
EloRating = ClientStats.EloRating,
HitLocation = hit.HitLoc,
LastStrainAngle = Strain.LastAngle,
PredictedViewAngles = hit.AnglesList,
// this is in "meters"
Distance = hit.Distance,
SessionScore = ClientStats.SessionScore,
HitType = hit.DeathType,
SessionSPM = ClientStats.SessionSPM,
StrainAngleBetween = Strain.LastDistance,
TimeSinceLastEvent = (int)Strain.LastDeltaTime,
WeaponId = hit.Weapon
foreach (string change in Tracker.GetChanges())
{
Log.WriteDebug(change);
Log.WriteDebug("--------------SNAPSHOT END-----------");
}
}
return result ?? new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Any,
};
}
Tracker.OnChange(snapshot);
return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ??
results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
new DetectionPenaltyResult()
public DetectionPenaltyResult ProcessTotalRatio(EFClientStatistics stats)
{
ClientPenalty = EFPenalty.PenaltyType.Any,
int totalChestHits = stats.HitLocations.Single(c => c.Location == IW4Info.HitLocation.torso_upper).HitCount;
if (totalChestHits >= 60)
{
double marginOfError = Thresholds.GetMarginOfError(totalChestHits);
double lerpAmount = Math.Min(1.0, (totalChestHits - 60) / 250.0);
// determine max acceptable ratio of chest to abdomen kills
double chestAbdomenRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(3.0), Thresholds.ChestAbdomenRatioThresholdHighSample(2.0), lerpAmount) + marginOfError;
double chestAbdomenLerpValueForBan = Thresholds.Lerp(Thresholds.ChestAbdomenRatioThresholdHighSample(4.0), Thresholds.ChestAbdomenRatioThresholdHighSample(3.0), lerpAmount) + marginOfError;
double currentChestAbdomenRatio = totalChestHits /
stats.HitLocations.Single(hl => hl.Location == IW4Info.HitLocation.torso_lower).HitCount;
if (currentChestAbdomenRatio > chestAbdomenRatioLerpValueForFlag)
{
if (currentChestAbdomenRatio > chestAbdomenLerpValueForBan)
{
Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Ban**");
Log.WriteDebug($"ClientId: {stats.ClientId}");
Log.WriteDebug($"**Total Chest Hits: {totalChestHits}");
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
Log.WriteDebug($"**MaxRatio {chestAbdomenLerpValueForBan}");
var sb = new StringBuilder();
foreach (var location in stats.HitLocations)
sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
Log.WriteDebug(sb.ToString());
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Ban,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
HitCount = totalChestHits,
Type = DetectionType.Chest
};
}
else
{
Log.WriteDebug("**Maximum Lifetime Chest/Abdomen Ratio Reached For Flag**");
Log.WriteDebug($"ClientId: {stats.ClientId}");
Log.WriteDebug($"**Total Chest Hits: {totalChestHits}");
Log.WriteDebug($"**Ratio {currentChestAbdomenRatio}");
Log.WriteDebug($"**MaxRatio {chestAbdomenRatioLerpValueForFlag}");
var sb = new StringBuilder();
foreach (var location in stats.HitLocations)
sb.Append($"HitLocation: {location.Location} -> {location.HitCount}\r\n");
Log.WriteDebug(sb.ToString());
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Flag,
Value = currentChestAbdomenRatio,
Location = IW4Info.HitLocation.torso_upper,
HitCount = totalChestHits,
Type = DetectionType.Chest
};
}
}
}
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Any
};
}
}

View File

@ -1,11 +1,16 @@
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IW4MAdmin.Plugins.Stats.Cheat
{
class DetectionPenaltyResult
{
public Detection.DetectionType Type { get; set; }
public EFPenalty.PenaltyType ClientPenalty { get; set; }
public Penalty.PenaltyType ClientPenalty { 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

@ -1,79 +0,0 @@
# IW4MAdmin Anticheat
**Initial document draft | 8.30.19**
**IW4MAdmin** anticheat for IW4x uses data from in-game logs to track player locations, hit locations, and view angles
to validate against a known set of play styles.
Every hit event ocurring in the game (Damage or Kill from a gun) is captured by IW4MAdmin and analyzed.
**Session analysis** occurs against all hit events since the player connected to the server.
**Lifetime analysis** occurs against all hit events ever occuring for a given player.
## Detection Types
### Bone
Compares the number of times a particular bone is hit against the number of hits on all other bones.
Many rudimentary aimbots lock onto a particular player bone position such as _Head_, or _Upper Torso_.
This detection method has the highest chance of a false positive, as non-cheaters can play abnormally
(e.g. going for headshots, or using a weapon that unconciously changes their playstyle)
#### Hit Location Reference
| Number | Hit Location |
|--------|-----------------|
| 2 | Head |
| 3 | Neck |
| 4 | Upper Torso |
| 5 | Lower Torso |
| 6 | Upper Right Arm |
| 7 | Upper Left Arm |
| 8 | Lower Right Arm |
| 9 | Lower Left Arm |
| 10 | Right Hand |
| 11 | Left Hand |
| 12 | Upper Right Leg |
| 13 | Upper Left Leg |
| 14 | Lower Right Leg |
| 15 | Right Foot |
| 16 | Left Foot |
### Chest
Identical to *Bone* detection, except it specifically compares the ratio of Upper Torso / Lower Torso hit counts.
It can be thought of as a focused bone detection type, as most aimbots don't aim to unusual bones such as a foot.
As with *Bone* detection, this is prone to false positives.
### Offset
Compares the player angles from three snapshots. The first snapshot being the snapshot immediately before the server registered the hit for a player.
The second snapshot is the "frame" the server registered the kill. The final snapshot is the snapshot immediately after the server registers the hit.
The algorithm is:
```
let a = first snapshot angles
let b = second snapshot angles
let c = third snapshot angles
offset = ((a - b) + (c - b)) - (a-c)
```
This detection method is very effective at detecting silent aimbots.
Silent aimbots "fake" a shot by changing a player's view angles for a single snapshot. From a spectator's position, the single snapshot view angle altering is not visible.
The larger "FOV" (field of view) from a silent aimbot, the more absolute difference between the three snapshots.
Over time if the average distance between these two viewangles is higher than expected, a detection is triggered.
False positives are very rare with this detection. However, extreme client lag can trigger a false positive in special situations.
### Strain
Analyzes the frequency, viewangle distance, and player distance between hits.
The algorithm is:
```
let v = view angle distance between two hits
let t = delta time (time between hits)
let d = distance between the attacker and victim
let s = decay over time
strain = ((v / t) * d)^s
```
A high value indicates fast target switching at large distance intervals, which if done naturally, requires an extreme level of mechanical effort.
"Rage" aimbots commonly switch to targets as fast as possible in any direction.
### Recoil
Compares the average of the last few snapshots of player angles to 0. Client sided no recoil prevents the view angle "Z" axis from changing.
If the average view angle "Z" axis value remains 0, the detection is triggered.
As of 8.30.19, there are no known false positives for this detection.
Several weapons which do not have any recoil from the game are excluded in this detection.
### Format
Detection penalty reasons are as follow:
<_detectionType_>-<_location/value_>@<_hitCount_>
Example:
- detectionType = Strain
- value = 1.39
- hitCount = 136
Result: **Strain-1.39@136**
This reason is only visible to logged in privileged users.

View File

@ -6,15 +6,17 @@ using System.Text;
namespace IW4MAdmin.Plugins.Stats.Cheat
{
class Strain
class Strain : ITrackable
{
private const double StrainDecayBase = 0.9;
private double CurrentStrain;
public double LastDistance { get; private set; }
public Vector3 LastAngle { get; private set; }
public double LastDeltaTime { get; private set; }
private Vector3 LastAngle;
private double LastDeltaTime;
private double LastDistance;
public double GetStrain(double killDistance, Vector3 newAngle, double deltaTime)
public int TimesReachedMaxStrain { get; private set; }
public double GetStrain(bool isDamage, int damage, double killDistance, Vector3 newAngle, double deltaTime)
{
if (LastAngle == null)
LastAngle = newAngle;
@ -23,15 +25,14 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
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];
#if DEBUG == true
Console.WriteLine($"Angle Between = {LastDistance}");
Console.WriteLine($"Distance From Target = {killDistance}");
Console.WriteLine($"Time Offset = {deltaTime}");
Console.WriteLine($"Decay Factor = {decayFactor} ");
#endif
// this happens on first kill
if ((distance[0] == 0 && distance[1] == 0) ||
deltaTime == 0 ||
@ -40,15 +41,23 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
return CurrentStrain;
}
double newStrain = Math.Pow(LastDistance, 0.99) / deltaTime;
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);
}
}

Some files were not shown because too many files have changed in this diff Show More