Compare commits
29 Commits
2.3-Prerel
...
2.3-Prerel
Author | SHA1 | Date | |
---|---|---|---|
e56f574af4 | |||
513f495304 | |||
910faf427b | |||
9117440566 | |||
ad6b6a6465 | |||
eb8145a168 | |||
a560e05df8 | |||
ac06b41a0b | |||
cce6482541 | |||
bc7dc3a71a | |||
2be719d8f9 | |||
8a8dec8bbd | |||
2b3e21d4ba | |||
4590d94d7d | |||
5842073f91 | |||
c783a04a52 | |||
4735864113 | |||
d70d8fd0ae | |||
0dc4e12d61 | |||
778e339a61 | |||
1ef2ba5344 | |||
126f2fcc47 | |||
d5789dac81 | |||
25e2438e7f | |||
19107f9e85 | |||
0e44fa10f7 | |||
ebb54ebfd7 | |||
03a27d113e | |||
39fb3b9966 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
ko_fi: raidmax
|
@ -25,20 +25,20 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jint" Version="3.0.0-beta-1632" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.3" />
|
||||
<PackageReference Include="RestEase" Version="1.4.10" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" />
|
||||
<PackageReference Include="RestEase" Version="1.5.0" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||
<TieredCompilation>true</TieredCompilation>
|
||||
<LangVersion>7.1</LangVersion>
|
||||
<LangVersion>Latest</LangVersion>
|
||||
<StartupObject></StartupObject>
|
||||
</PropertyGroup>
|
||||
|
||||
|
@ -13,6 +13,7 @@ using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using SharedLibraryCore.Services;
|
||||
using System;
|
||||
using System.Collections;
|
||||
@ -54,7 +55,7 @@ namespace IW4MAdmin.Application
|
||||
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
||||
readonly IPageList PageList;
|
||||
private readonly Dictionary<long, ILogger> _loggers = new Dictionary<long, ILogger>();
|
||||
private readonly MetaService _metaService;
|
||||
private readonly IMetaService _metaService;
|
||||
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
|
||||
private readonly CancellationTokenSource _tokenSource;
|
||||
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
|
||||
@ -65,12 +66,15 @@ namespace IW4MAdmin.Application
|
||||
private readonly IEnumerable<IRegisterEvent> _customParserEvents;
|
||||
private readonly IEventHandler _eventHandler;
|
||||
private readonly IScriptCommandFactory _scriptCommandFactory;
|
||||
private readonly IMetaRegistration _metaRegistration;
|
||||
private readonly IScriptPluginServiceResolver _scriptPluginServiceResolver;
|
||||
|
||||
public ApplicationManager(ILogger logger, IMiddlewareActionHandler actionHandler, IEnumerable<IManagerCommand> commands,
|
||||
ITranslationLookup translationLookup, IConfigurationHandler<CommandConfiguration> commandConfiguration,
|
||||
IConfigurationHandler<ApplicationConfiguration> appConfigHandler, IGameServerInstanceFactory serverInstanceFactory,
|
||||
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
|
||||
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory)
|
||||
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IMetaService metaService,
|
||||
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver)
|
||||
{
|
||||
MiddlewareActionHandler = actionHandler;
|
||||
_servers = new ConcurrentBag<Server>();
|
||||
@ -85,7 +89,7 @@ namespace IW4MAdmin.Application
|
||||
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(parserRegexFactory) };
|
||||
TokenAuthenticator = new TokenAuthentication();
|
||||
_logger = logger;
|
||||
_metaService = new MetaService();
|
||||
_metaService = metaService;
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
_loggers.Add(0, logger);
|
||||
_commands = commands.ToList();
|
||||
@ -96,6 +100,8 @@ namespace IW4MAdmin.Application
|
||||
_customParserEvents = customParserEvents;
|
||||
_eventHandler = eventHandler;
|
||||
_scriptCommandFactory = scriptCommandFactory;
|
||||
_metaRegistration = metaRegistration;
|
||||
_scriptPluginServiceResolver = scriptPluginServiceResolver;
|
||||
Plugins = plugins;
|
||||
}
|
||||
|
||||
@ -273,12 +279,12 @@ namespace IW4MAdmin.Application
|
||||
{
|
||||
if (plugin is ScriptPlugin scriptPlugin)
|
||||
{
|
||||
await scriptPlugin.Initialize(this, _scriptCommandFactory);
|
||||
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
|
||||
scriptPlugin.Watcher.Changed += async (sender, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await scriptPlugin.Initialize(this, _scriptCommandFactory);
|
||||
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
@ -445,7 +451,8 @@ namespace IW4MAdmin.Application
|
||||
Name = cmd.Name,
|
||||
Alias = cmd.Alias,
|
||||
MinimumPermission = cmd.Permission,
|
||||
AllowImpersonation = cmd.AllowImpersonation
|
||||
AllowImpersonation = cmd.AllowImpersonation,
|
||||
SupportedGames = cmd.SupportedGames
|
||||
});
|
||||
}
|
||||
|
||||
@ -453,133 +460,7 @@ namespace IW4MAdmin.Application
|
||||
await _commandConfiguration.Save();
|
||||
#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);
|
||||
|
||||
if (client == null)
|
||||
{
|
||||
_logger.WriteWarning($"No client found with id {clientId} when generating profile meta");
|
||||
return metaList;
|
||||
}
|
||||
|
||||
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
|
||||
_metaRegistration.Register();
|
||||
|
||||
#region CUSTOM_EVENTS
|
||||
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
|
||||
|
12
Application/BuildScripts/DownloadTranslations.ps1
Normal file
12
Application/BuildScripts/DownloadTranslations.ps1
Normal file
@ -0,0 +1,12 @@
|
||||
param (
|
||||
[string]$OutputDir = $(throw "-OutputDir is required.")
|
||||
)
|
||||
|
||||
$localizations = @("en-US", "ru-RU", "es-EC", "pt-BR", "de-DE")
|
||||
foreach($localization in $localizations)
|
||||
{
|
||||
$url = "http://api.raidmax.org:5000/localization/{0}" -f $localization
|
||||
$filePath = "{0}Localization\IW4MAdmin.{1}.json" -f $OutputDir, $localization
|
||||
$response = Invoke-WebRequest $url
|
||||
Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8
|
||||
}
|
@ -26,6 +26,10 @@ if not exist "%PublishDir%\Lib\" md "%PublishDir%\Lib\"
|
||||
move "%PublishDir%\*.dll" "%PublishDir%\Lib\"
|
||||
move "%PublishDir%\*.json" "%PublishDir%\Lib\"
|
||||
move "%PublishDir%\runtimes" "%PublishDir%\Lib\runtimes"
|
||||
move "%PublishDir%\ru" "%PublishDir%\Lib\ru"
|
||||
move "%PublishDir%\de" "%PublishDir%\Lib\de"
|
||||
move "%PublishDir%\pt" "%PublishDir%\Lib\pt"
|
||||
move "%PublishDir%\es" "%PublishDir%\Lib\es"
|
||||
if exist "%PublishDir%\refs" move "%PublishDir%\refs" "%PublishDir%\Lib\refs"
|
||||
|
||||
echo making start scripts
|
||||
|
@ -1,3 +1,6 @@
|
||||
set SolutionDir=%1
|
||||
set ProjectDir=%2
|
||||
set TargetDir=%3
|
||||
set TargetDir=%3
|
||||
|
||||
echo D | xcopy "%SolutionDir%Plugins\ScriptPlugins\*.js" "%TargetDir%Plugins" /y
|
||||
powershell -File "%ProjectDir%BuildScripts\DownloadTranslations.ps1" %TargetDir%
|
@ -130,7 +130,6 @@ namespace IW4MAdmin.Application.EventParsers
|
||||
|
||||
int clientNumber = int.Parse(matchResult.Values[Configuration.Say.GroupMapping[ParserRegex.GroupType.OriginClientNumber]]);
|
||||
|
||||
// todo: these need to defined outside of here
|
||||
if (message.StartsWith(_appConfig.CommandPrefix) || message.StartsWith(_appConfig.BroadcastCommandPrefix))
|
||||
{
|
||||
return new GameEvent()
|
||||
@ -256,6 +255,7 @@ namespace IW4MAdmin.Application.EventParsers
|
||||
ClientNumber = Convert.ToInt32(match.Values[Configuration.Join.GroupMapping[ParserRegex.GroupType.OriginClientNumber]].ToString()),
|
||||
State = EFClient.ClientState.Connecting,
|
||||
},
|
||||
Extra = originIdString,
|
||||
RequiredEntity = GameEvent.EventRequiredEntity.None,
|
||||
IsBlocking = true,
|
||||
GameTime = gameTime,
|
||||
|
@ -13,17 +13,19 @@ namespace IW4MAdmin.Application.Factories
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
private readonly IRConConnectionFactory _rconConnectionFactory;
|
||||
private readonly IGameLogReaderFactory _gameLogReaderFactory;
|
||||
private readonly IMetaService _metaService;
|
||||
|
||||
/// <summary>
|
||||
/// base constructor
|
||||
/// </summary>
|
||||
/// <param name="translationLookup"></param>
|
||||
/// <param name="rconConnectionFactory"></param>
|
||||
public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory)
|
||||
public GameServerInstanceFactory(ITranslationLookup translationLookup, IRConConnectionFactory rconConnectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService)
|
||||
{
|
||||
_translationLookup = translationLookup;
|
||||
_rconConnectionFactory = rconConnectionFactory;
|
||||
_gameLogReaderFactory = gameLogReaderFactory;
|
||||
_metaService = metaService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -34,7 +36,7 @@ namespace IW4MAdmin.Application.Factories
|
||||
/// <returns></returns>
|
||||
public Server CreateServer(ServerConfiguration config, IManager manager)
|
||||
{
|
||||
return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory, _gameLogReaderFactory);
|
||||
return new IW4MServer(manager, config, _translationLookup, _rconConnectionFactory, _gameLogReaderFactory, _metaService);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ namespace IW4MAdmin.Application.Factories
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction)
|
||||
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, bool isTargetRequired, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction)
|
||||
{
|
||||
var permissionEnum = Enum.Parse<Permission>(permission);
|
||||
var argsArray = args.Select(_arg => new CommandArgument
|
||||
@ -34,7 +34,7 @@ namespace IW4MAdmin.Application.Factories
|
||||
Required = _arg.Item2
|
||||
}).ToArray();
|
||||
|
||||
return new ScriptCommand(name, alias, description, permissionEnum, argsArray, executeAction, _config, _transLookup);
|
||||
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction, _config, _transLookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Services;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -26,15 +25,17 @@ namespace IW4MAdmin
|
||||
private static readonly SharedLibraryCore.Localization.TranslationLookup loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
public GameLogEventDetection LogEvent;
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
private readonly IMetaService _metaService;
|
||||
private const int REPORT_FLAG_COUNT = 4;
|
||||
private int lastGameTime = 0;
|
||||
|
||||
public int Id { get; private set; }
|
||||
|
||||
public IW4MServer(IManager mgr, ServerConfiguration cfg, ITranslationLookup lookup,
|
||||
IRConConnectionFactory connectionFactory, IGameLogReaderFactory gameLogReaderFactory) : base(cfg, mgr, connectionFactory, gameLogReaderFactory)
|
||||
IRConConnectionFactory connectionFactory, IGameLogReaderFactory gameLogReaderFactory, IMetaService metaService) : base(cfg, mgr, connectionFactory, gameLogReaderFactory)
|
||||
{
|
||||
_translationLookup = lookup;
|
||||
_metaService = metaService;
|
||||
}
|
||||
|
||||
override public async Task<EFClient> OnClientConnected(EFClient clientFromLog)
|
||||
@ -475,8 +476,8 @@ namespace IW4MAdmin
|
||||
Time = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await new MetaService().AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin);
|
||||
await new MetaService().AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin);
|
||||
await _metaService.AddPersistentMeta("LastMapPlayed", CurrentMap.Alias, E.Origin);
|
||||
await _metaService.AddPersistentMeta("LastServerPlayed", E.Owner.Hostname, E.Origin);
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.PreDisconnect)
|
||||
@ -540,14 +541,18 @@ namespace IW4MAdmin
|
||||
.First(_qm => _qm.Game == GameName)
|
||||
.Messages[E.Data.Substring(1)];
|
||||
}
|
||||
catch { }
|
||||
catch
|
||||
{
|
||||
message = E.Data.Substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
ChatHistory.Add(new ChatInfo()
|
||||
{
|
||||
Name = E.Origin.Name,
|
||||
Message = message,
|
||||
Time = DateTime.UtcNow
|
||||
Time = DateTime.UtcNow,
|
||||
IsHidden = !string.IsNullOrEmpty(GamePassword)
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -610,13 +615,10 @@ namespace IW4MAdmin
|
||||
|
||||
if (E.Type == GameEvent.EventType.Broadcast)
|
||||
{
|
||||
#if DEBUG == false
|
||||
// this is a little ugly but I don't want to change the abstract class
|
||||
if (E.Data != null)
|
||||
if (!Utilities.IsDevelopment && E.Data != null) // hides broadcast when in development mode
|
||||
{
|
||||
await E.Owner.ExecuteCommandAsync(E.Data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
lock (ChatHistory)
|
||||
@ -783,7 +785,7 @@ namespace IW4MAdmin
|
||||
|
||||
var polledClients = await PollPlayersAsync();
|
||||
|
||||
foreach (var disconnectingClient in polledClients[1])
|
||||
foreach (var disconnectingClient in polledClients[1].Where(_client => !_client.IsZombieClient /* ignores "fake" zombie clients */))
|
||||
{
|
||||
disconnectingClient.CurrentServer = this;
|
||||
var e = new GameEvent()
|
||||
@ -814,6 +816,7 @@ namespace IW4MAdmin
|
||||
Origin = client,
|
||||
Owner = this,
|
||||
IsBlocking = true,
|
||||
Extra = client.GetAdditionalProperty<string>("BotGuid"),
|
||||
Source = GameEvent.EventSource.Status
|
||||
};
|
||||
|
||||
@ -984,6 +987,7 @@ namespace IW4MAdmin
|
||||
var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log");
|
||||
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync");
|
||||
var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip");
|
||||
var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: "");
|
||||
|
||||
if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName)
|
||||
{
|
||||
@ -1017,6 +1021,7 @@ namespace IW4MAdmin
|
||||
this.FSGame = game.Value;
|
||||
this.Gametype = gametype;
|
||||
this.IP = ip.Value == "localhost" ? ServerConfig.IPAddress : ip.Value ?? ServerConfig.IPAddress;
|
||||
this.GamePassword = gamePassword.Value;
|
||||
UpdateMap(mapname);
|
||||
|
||||
if (RconParser.CanGenerateLogPath)
|
||||
@ -1083,9 +1088,11 @@ namespace IW4MAdmin
|
||||
Logger.WriteInfo($"Log file is {LogPath}");
|
||||
|
||||
_ = Task.Run(() => LogEvent.PollForChanges());
|
||||
#if !DEBUG
|
||||
Broadcast(loc["BROADCAST_ONLINE"]);
|
||||
#endif
|
||||
|
||||
if (!Utilities.IsDevelopment)
|
||||
{
|
||||
Broadcast(loc["BROADCAST_ONLINE"]);
|
||||
}
|
||||
}
|
||||
|
||||
public Uri[] GenerateUriForLog(string logPath, string gameLogServerUrl)
|
||||
@ -1233,6 +1240,7 @@ namespace IW4MAdmin
|
||||
if (targetClient.IsIngame)
|
||||
{
|
||||
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}");
|
||||
Logger.WriteDebug($"Executing tempban kick command for {targetClient}");
|
||||
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,23 @@
|
||||
using IW4MAdmin.Application.EventParsers;
|
||||
using IW4MAdmin.Application.Factories;
|
||||
using IW4MAdmin.Application.Helpers;
|
||||
using IW4MAdmin.Application.Meta;
|
||||
using IW4MAdmin.Application.Migration;
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using RestEase;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using SharedLibraryCore.Repositories;
|
||||
using SharedLibraryCore.Services;
|
||||
using Stats.Dtos;
|
||||
using StatsWeb;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@ -128,7 +135,11 @@ namespace IW4MAdmin.Application
|
||||
await ApplicationTask;
|
||||
}
|
||||
|
||||
catch { }
|
||||
catch (Exception e)
|
||||
{
|
||||
string failMessage = translationLookup == null ? "Failed to initalize IW4MAdmin" : translationLookup["MANAGER_INIT_FAIL"];
|
||||
Console.WriteLine($"{failMessage}: {e.GetExceptionInfo()}");
|
||||
}
|
||||
|
||||
if (ServerManager.IsRestartRequested)
|
||||
{
|
||||
@ -222,7 +233,7 @@ namespace IW4MAdmin.Application
|
||||
serviceCollection.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection)
|
||||
.AddSingleton(new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings") as IConfigurationHandler<ApplicationConfiguration>)
|
||||
.AddSingleton(new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as IConfigurationHandler<CommandConfiguration>)
|
||||
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration())
|
||||
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<ApplicationConfiguration>>().Configuration() ?? new ApplicationConfiguration())
|
||||
.AddSingleton(_serviceProvider => _serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>().Configuration() ?? new CommandConfiguration())
|
||||
.AddSingleton<ILogger>(_serviceProvider => defaultLogger)
|
||||
.AddSingleton<IPluginImporter, PluginImporter>()
|
||||
@ -235,6 +246,14 @@ namespace IW4MAdmin.Application
|
||||
.AddSingleton<IGameLogReaderFactory, GameLogReaderFactory>()
|
||||
.AddSingleton<IScriptCommandFactory, ScriptCommandFactory>()
|
||||
.AddSingleton<IAuditInformationRepository, AuditInformationRepository>()
|
||||
.AddSingleton<IEntityService<EFClient>, ClientService>()
|
||||
.AddSingleton<IMetaService, MetaService>()
|
||||
.AddSingleton<IMetaRegistration, MetaRegistration>()
|
||||
.AddSingleton<IScriptPluginServiceResolver, ScriptPluginServiceResolver>()
|
||||
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>, ReceivedPenaltyResourceQueryHelper>()
|
||||
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>, AdministeredPenaltyResourceQueryHelper>()
|
||||
.AddSingleton<IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>, UpdatedAliasResourceQueryHelper>()
|
||||
.AddSingleton<IResourceQueryHelper<ChatSearchQuery, MessageResponse>, ChatResourceQueryHelper>()
|
||||
.AddTransient<IParserPatternMatcher, ParserPatternMatcher>()
|
||||
.AddSingleton(_serviceProvider =>
|
||||
{
|
||||
|
64
Application/Meta/AdministeredPenaltyResourceQueryHelper.cs
Normal file
64
Application/Meta/AdministeredPenaltyResourceQueryHelper.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
|
||||
namespace IW4MAdmin.Application.Meta
|
||||
{
|
||||
/// <summary>
|
||||
/// implementation of IResourceQueryHelper
|
||||
/// query helper that retrieves administered penalties for provided client id
|
||||
/// </summary>
|
||||
public class AdministeredPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse>
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
|
||||
public AdministeredPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query)
|
||||
{
|
||||
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
var iqPenalties = ctx.Penalties.AsNoTracking()
|
||||
.Where(_penalty => query.ClientId == _penalty.PunisherId)
|
||||
.Where(_penalty => _penalty.When < query.Before)
|
||||
.OrderByDescending(_penalty => _penalty.When);
|
||||
|
||||
var penalties = await iqPenalties
|
||||
.Take(query.Count)
|
||||
.Select(_penalty => new AdministeredPenaltyResponse()
|
||||
{
|
||||
PenaltyId = _penalty.PenaltyId,
|
||||
Offense = _penalty.Offense,
|
||||
AutomatedOffense = _penalty.AutomatedOffense,
|
||||
ClientId = _penalty.OffenderId,
|
||||
OffenderName = _penalty.Offender.CurrentAlias.Name,
|
||||
OffenderClientId = _penalty.Offender.ClientId,
|
||||
PunisherClientId = _penalty.PunisherId,
|
||||
PunisherName = _penalty.Punisher.CurrentAlias.Name,
|
||||
PenaltyType = _penalty.Type,
|
||||
When = _penalty.When,
|
||||
ExpirationDate = _penalty.Expires,
|
||||
IsLinked = _penalty.OffenderId != query.ClientId,
|
||||
IsSensitive = _penalty.Type == EFPenalty.PenaltyType.Flag
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return new ResourceQueryHelperResult<AdministeredPenaltyResponse>
|
||||
{
|
||||
// todo: might need to do count at some point
|
||||
RetrievedResultCount = penalties.Count,
|
||||
Results = penalties
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
165
Application/Meta/MetaRegistration.cs
Normal file
165
Application/Meta/MetaRegistration.cs
Normal file
@ -0,0 +1,165 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Application.Meta
|
||||
{
|
||||
public class MetaRegistration : IMetaRegistration
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private ITranslationLookup _transLookup;
|
||||
private readonly IMetaService _metaService;
|
||||
private readonly IEntityService<EFClient> _clientEntityService;
|
||||
private readonly IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> _receivedPenaltyHelper;
|
||||
private readonly IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> _administeredPenaltyHelper;
|
||||
private readonly IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> _updatedAliasHelper;
|
||||
|
||||
public MetaRegistration(ILogger logger, IMetaService metaService, ITranslationLookup transLookup, IEntityService<EFClient> clientEntityService,
|
||||
IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse> receivedPenaltyHelper,
|
||||
IResourceQueryHelper<ClientPaginationRequest, AdministeredPenaltyResponse> administeredPenaltyHelper,
|
||||
IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse> updatedAliasHelper)
|
||||
{
|
||||
_logger = logger;
|
||||
_transLookup = transLookup;
|
||||
_metaService = metaService;
|
||||
_clientEntityService = clientEntityService;
|
||||
_receivedPenaltyHelper = receivedPenaltyHelper;
|
||||
_administeredPenaltyHelper = administeredPenaltyHelper;
|
||||
_updatedAliasHelper = updatedAliasHelper;
|
||||
}
|
||||
|
||||
public void Register()
|
||||
{
|
||||
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, GetProfileMeta);
|
||||
_metaService.AddRuntimeMeta<ClientPaginationRequest, ReceivedPenaltyResponse>(MetaType.ReceivedPenalty, GetReceivedPenaltiesMeta);
|
||||
_metaService.AddRuntimeMeta<ClientPaginationRequest, AdministeredPenaltyResponse>(MetaType.Penalized, GetAdministeredPenaltiesMeta);
|
||||
_metaService.AddRuntimeMeta<ClientPaginationRequest, UpdatedAliasResponse>(MetaType.AliasUpdate, GetUpdatedAliasMeta);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<InformationResponse>> GetProfileMeta(ClientPaginationRequest request)
|
||||
{
|
||||
var metaList = new List<InformationResponse>();
|
||||
var lastMapMeta = await _metaService.GetPersistentMeta("LastMapPlayed", new EFClient() { ClientId = request.ClientId });
|
||||
|
||||
if (lastMapMeta != null)
|
||||
{
|
||||
metaList.Add(new InformationResponse()
|
||||
{
|
||||
ClientId = request.ClientId,
|
||||
MetaId = lastMapMeta.MetaId,
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_MAP"],
|
||||
Value = lastMapMeta.Value,
|
||||
ShouldDisplay = true,
|
||||
Type = MetaType.Information,
|
||||
Column = 1,
|
||||
Order = 6
|
||||
});
|
||||
}
|
||||
|
||||
var lastServerMeta = await _metaService.GetPersistentMeta("LastServerPlayed", new EFClient() { ClientId = request.ClientId });
|
||||
|
||||
if (lastServerMeta != null)
|
||||
{
|
||||
metaList.Add(new InformationResponse()
|
||||
{
|
||||
ClientId = request.ClientId,
|
||||
MetaId = lastServerMeta.MetaId,
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_LAST_SERVER"],
|
||||
Value = lastServerMeta.Value,
|
||||
ShouldDisplay = true,
|
||||
Type = MetaType.Information,
|
||||
Column = 0,
|
||||
Order = 6
|
||||
});
|
||||
}
|
||||
|
||||
var client = await _clientEntityService.Get(request.ClientId);
|
||||
|
||||
if (client == null)
|
||||
{
|
||||
_logger.WriteWarning($"No client found with id {request.ClientId} when generating profile meta");
|
||||
return metaList;
|
||||
}
|
||||
|
||||
metaList.Add(new InformationResponse()
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Key = _transLookup["WEBFRONT_PROFILE_META_PLAY_TIME"],
|
||||
Value = TimeSpan.FromHours(client.TotalConnectionTime / 3600.0).HumanizeForCurrentCulture(),
|
||||
ShouldDisplay = true,
|
||||
Column = 1,
|
||||
Order = 0,
|
||||
Type = MetaType.Information
|
||||
});
|
||||
|
||||
metaList.Add(new InformationResponse()
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Key = _transLookup["WEBFRONT_PROFILE_META_FIRST_SEEN"],
|
||||
Value = (DateTime.UtcNow - client.FirstConnection).HumanizeForCurrentCulture(),
|
||||
ShouldDisplay = true,
|
||||
Column = 1,
|
||||
Order = 1,
|
||||
Type = MetaType.Information
|
||||
});
|
||||
|
||||
metaList.Add(new InformationResponse()
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Key = _transLookup["WEBFRONT_PROFILE_META_LAST_SEEN"],
|
||||
Value = (DateTime.UtcNow - client.LastConnection).HumanizeForCurrentCulture(),
|
||||
ShouldDisplay = true,
|
||||
Column = 1,
|
||||
Order = 2,
|
||||
Type = MetaType.Information
|
||||
});
|
||||
|
||||
metaList.Add(new InformationResponse()
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_CONNECTIONS"],
|
||||
Value = client.Connections.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
ShouldDisplay = true,
|
||||
Column = 1,
|
||||
Order = 3,
|
||||
Type = MetaType.Information
|
||||
});
|
||||
|
||||
metaList.Add(new InformationResponse()
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
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"],
|
||||
IsSensitive = true,
|
||||
Column = 1,
|
||||
Order = 4,
|
||||
Type = MetaType.Information
|
||||
});
|
||||
|
||||
return metaList;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<ReceivedPenaltyResponse>> GetReceivedPenaltiesMeta(ClientPaginationRequest request)
|
||||
{
|
||||
var penalties = await _receivedPenaltyHelper.QueryResource(request);
|
||||
return penalties.Results;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<AdministeredPenaltyResponse>> GetAdministeredPenaltiesMeta(ClientPaginationRequest request)
|
||||
{
|
||||
var penalties = await _administeredPenaltyHelper.QueryResource(request);
|
||||
return penalties.Results;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<UpdatedAliasResponse>> GetUpdatedAliasMeta(ClientPaginationRequest request)
|
||||
{
|
||||
var aliases = await _updatedAliasHelper.QueryResource(request);
|
||||
return aliases.Results;
|
||||
}
|
||||
}
|
||||
}
|
72
Application/Meta/ReceivedPenaltyResourceQueryHelper.cs
Normal file
72
Application/Meta/ReceivedPenaltyResourceQueryHelper.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
|
||||
namespace IW4MAdmin.Application.Meta
|
||||
{
|
||||
/// <summary>
|
||||
/// implementation of IResourceQueryHelper
|
||||
/// used to pull in penalties applied to a given client id
|
||||
/// </summary>
|
||||
public class ReceivedPenaltyResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, ReceivedPenaltyResponse>
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
|
||||
public ReceivedPenaltyResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query)
|
||||
{
|
||||
var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
|
||||
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
var linkId = await ctx.Clients.AsNoTracking()
|
||||
.Where(_client => _client.ClientId == query.ClientId)
|
||||
.Select(_client => _client.AliasLinkId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var iqPenalties = ctx.Penalties.AsNoTracking()
|
||||
.Where(_penalty => _penalty.OffenderId == query.ClientId || (linkedPenaltyType.Contains(_penalty.Type) && _penalty.LinkId == linkId))
|
||||
.Where(_penalty => _penalty.When < query.Before)
|
||||
.OrderByDescending(_penalty => _penalty.When);
|
||||
|
||||
var penalties = await iqPenalties
|
||||
.Take(query.Count)
|
||||
.Select(_penalty => new ReceivedPenaltyResponse()
|
||||
{
|
||||
PenaltyId = _penalty.PenaltyId,
|
||||
ClientId = query.ClientId,
|
||||
Offense = _penalty.Offense,
|
||||
AutomatedOffense = _penalty.AutomatedOffense,
|
||||
OffenderClientId = _penalty.OffenderId,
|
||||
OffenderName = _penalty.Offender.CurrentAlias.Name,
|
||||
PunisherClientId = _penalty.PunisherId,
|
||||
PunisherName = _penalty.Punisher.CurrentAlias.Name,
|
||||
PenaltyType = _penalty.Type,
|
||||
When = _penalty.When,
|
||||
ExpirationDate = _penalty.Expires,
|
||||
IsLinked = _penalty.OffenderId != query.ClientId,
|
||||
IsSensitive = _penalty.Type == EFPenalty.PenaltyType.Flag
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return new ResourceQueryHelperResult<ReceivedPenaltyResponse>
|
||||
{
|
||||
// todo: maybe actually count
|
||||
RetrievedResultCount = penalties.Count,
|
||||
Results = penalties
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
60
Application/Meta/UpdatedAliasResourceQueryHelper.cs
Normal file
60
Application/Meta/UpdatedAliasResourceQueryHelper.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Application.Meta
|
||||
{
|
||||
/// <summary>
|
||||
/// implementation if IResrouceQueryHerlp
|
||||
/// used to pull alias changes for given client id
|
||||
/// </summary>
|
||||
public class UpdatedAliasResourceQueryHelper : IResourceQueryHelper<ClientPaginationRequest, UpdatedAliasResponse>
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
|
||||
public UpdatedAliasResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
public async Task<ResourceQueryHelperResult<UpdatedAliasResponse>> QueryResource(ClientPaginationRequest query)
|
||||
{
|
||||
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||
int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId;
|
||||
|
||||
var iqAliasUpdates = ctx.Aliases
|
||||
.Where(_alias => _alias.LinkId == linkId)
|
||||
.Where(_alias => _alias.DateAdded < query.Before)
|
||||
.Where(_alias => _alias.IPAddress != null)
|
||||
.OrderByDescending(_alias => _alias.DateAdded)
|
||||
.Select(_alias => new UpdatedAliasResponse
|
||||
{
|
||||
MetaId = _alias.AliasId,
|
||||
Name = _alias.Name,
|
||||
IPAddress = _alias.IPAddress.ConvertIPtoString(),
|
||||
When = _alias.DateAdded,
|
||||
Type = MetaType.AliasUpdate,
|
||||
IsSensitive = true
|
||||
});
|
||||
|
||||
var result = (await iqAliasUpdates
|
||||
.Take(query.Count)
|
||||
.ToListAsync())
|
||||
.Distinct();
|
||||
|
||||
|
||||
return new ResourceQueryHelperResult<UpdatedAliasResponse>
|
||||
{
|
||||
Results = result, // we can potentially have duplicates
|
||||
RetrievedResultCount = result.Count()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
187
Application/Misc/MetaService.cs
Normal file
187
Application/Misc/MetaService.cs
Normal file
@ -0,0 +1,187 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
/// <summary>
|
||||
/// implementation of IMetaService
|
||||
/// used to add and retrieve runtime and persistent meta
|
||||
/// </summary>
|
||||
public class MetaService : IMetaService
|
||||
{
|
||||
private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public MetaService(ILogger logger, IDatabaseContextFactory contextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_metaActions = new Dictionary<MetaType, List<dynamic>>();
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client)
|
||||
{
|
||||
// this seems to happen if the client disconnects before they've had time to authenticate and be added
|
||||
if (client.ClientId < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var ctx = _contextFactory.CreateContext();
|
||||
|
||||
var existingMeta = await ctx.EFMeta
|
||||
.Where(_meta => _meta.Key == metaKey)
|
||||
.Where(_meta => _meta.ClientId == client.ClientId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existingMeta != null)
|
||||
{
|
||||
existingMeta.Value = metaValue;
|
||||
existingMeta.Updated = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
ctx.EFMeta.Add(new EFMeta()
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Created = DateTime.UtcNow,
|
||||
Key = metaKey,
|
||||
Value = metaValue
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
|
||||
{
|
||||
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
return await ctx.EFMeta
|
||||
.Where(_meta => _meta.Key == metaKey)
|
||||
.Where(_meta => _meta.ClientId == client.ClientId)
|
||||
.Select(_meta => new EFMeta()
|
||||
{
|
||||
MetaId = _meta.MetaId,
|
||||
Key = _meta.Key,
|
||||
ClientId = _meta.ClientId,
|
||||
Value = _meta.Value
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public void AddRuntimeMeta<T, V>(MetaType metaKey, Func<T, Task<IEnumerable<V>>> metaAction) where T : PaginationRequest where V : IClientMeta
|
||||
{
|
||||
if (!_metaActions.ContainsKey(metaKey))
|
||||
{
|
||||
_metaActions.Add(metaKey, new List<dynamic>() { metaAction });
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
_metaActions[metaKey].Add(metaAction);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request)
|
||||
{
|
||||
var meta = new List<IClientMeta>();
|
||||
|
||||
foreach (var (type, actions) in _metaActions)
|
||||
{
|
||||
// information is not listed chronologically
|
||||
if (type != MetaType.Information)
|
||||
{
|
||||
var metaItems = await actions[0](request);
|
||||
meta.AddRange(metaItems);
|
||||
}
|
||||
}
|
||||
|
||||
return meta.OrderByDescending(_meta => _meta.When)
|
||||
.Take(request.Count)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType) where T : IClientMeta
|
||||
{
|
||||
IEnumerable<T> meta;
|
||||
if (metaType == MetaType.Information)
|
||||
{
|
||||
var allMeta = new List<T>();
|
||||
|
||||
foreach (var individualMetaRegistration in _metaActions[metaType])
|
||||
{
|
||||
allMeta.AddRange(await individualMetaRegistration(request));
|
||||
}
|
||||
|
||||
return ProcessInformationMeta(allMeta);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
meta = await _metaActions[metaType][0](request) as IEnumerable<T>;
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
private static IEnumerable<T> ProcessInformationMeta<T>(IEnumerable<T> meta) where T : IClientMeta
|
||||
{
|
||||
var table = new List<List<T>>();
|
||||
var metaWithColumn = meta
|
||||
.Where(_meta => _meta.Column != null);
|
||||
|
||||
var columnGrouping = metaWithColumn
|
||||
.GroupBy(_meta => _meta.Column);
|
||||
|
||||
var metaToSort = meta.Except(metaWithColumn).ToList();
|
||||
|
||||
foreach (var metaItem in columnGrouping)
|
||||
{
|
||||
table.Add(new List<T>(metaItem));
|
||||
}
|
||||
|
||||
while (metaToSort.Count > 0)
|
||||
{
|
||||
var sortingMeta = metaToSort.First();
|
||||
|
||||
int indexOfSmallestColumn()
|
||||
{
|
||||
int index = 0;
|
||||
int smallestColumnSize = int.MaxValue;
|
||||
for (int i = 0; i < table.Count; i++)
|
||||
{
|
||||
if (table[i].Count < smallestColumnSize)
|
||||
{
|
||||
smallestColumnSize = table[i].Count;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
int columnIndex = indexOfSmallestColumn();
|
||||
|
||||
sortingMeta.Column = columnIndex;
|
||||
sortingMeta.Order = columnGrouping
|
||||
.First(_group => _group.Key == columnIndex)
|
||||
.Count();
|
||||
|
||||
table[columnIndex].Add(sortingMeta);
|
||||
|
||||
metaToSort.Remove(sortingMeta);
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
private readonly Action<GameEvent> _executeAction;
|
||||
|
||||
public ScriptCommand(string name, string alias, string description, Permission permission,
|
||||
public ScriptCommand(string name, string alias, string description, bool isTargetRequired, Permission permission,
|
||||
CommandArgument[] args, Action<GameEvent> executeAction, CommandConfiguration config, ITranslationLookup layout)
|
||||
: base(config, layout)
|
||||
{
|
||||
@ -24,6 +24,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
Name = name;
|
||||
Alias = alias;
|
||||
Description = description;
|
||||
RequiresTarget = isTargetRequired;
|
||||
Permission = permission;
|
||||
Arguments = args;
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
_onProcessing.Dispose();
|
||||
}
|
||||
|
||||
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory)
|
||||
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory, IScriptPluginServiceResolver serviceResolver)
|
||||
{
|
||||
await _onProcessing.WaitAsync();
|
||||
|
||||
@ -114,6 +114,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
_scriptEngine.Execute(script);
|
||||
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
|
||||
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
|
||||
dynamic pluginObject = _scriptEngine.GetValue("plugin").ToObject();
|
||||
|
||||
Author = pluginObject.author;
|
||||
@ -164,6 +165,11 @@ namespace IW4MAdmin.Application.Misc
|
||||
successfullyLoaded = true;
|
||||
}
|
||||
|
||||
catch (JavaScriptException ex)
|
||||
{
|
||||
throw new PluginException($"An error occured while initializing script plugin: {ex.Error} (Line: {ex.Location.Start.Line}, Character: {ex.Location.Start.Column})") { PluginFile = _fileName };
|
||||
}
|
||||
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
@ -246,6 +252,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
string alias = dynamicCommand.alias;
|
||||
string description = dynamicCommand.description;
|
||||
string permission = dynamicCommand.permission;
|
||||
bool targetRequired = false;
|
||||
|
||||
List<(string, bool)> args = new List<(string, bool)>();
|
||||
dynamic arguments = null;
|
||||
@ -260,6 +267,16 @@ namespace IW4MAdmin.Application.Misc
|
||||
// arguments are optional
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
targetRequired = dynamicCommand.targetRequired;
|
||||
}
|
||||
|
||||
catch (RuntimeBinderException)
|
||||
{
|
||||
// arguments are optional
|
||||
}
|
||||
|
||||
if (arguments != null)
|
||||
{
|
||||
foreach (var arg in dynamicCommand.arguments)
|
||||
@ -284,7 +301,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
}
|
||||
|
||||
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, args, execute));
|
||||
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission, targetRequired, args, execute));
|
||||
}
|
||||
|
||||
return commandList;
|
||||
|
48
Application/Misc/ScriptPluginServiceResolver.cs
Normal file
48
Application/Misc/ScriptPluginServiceResolver.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
/// <summary>
|
||||
/// implementation of IScriptPluginServiceResolver
|
||||
/// </summary>
|
||||
public class ScriptPluginServiceResolver : IScriptPluginServiceResolver
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public ScriptPluginServiceResolver(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public object ResolveService(string serviceName)
|
||||
{
|
||||
var serviceType = DetermineRootType(serviceName);
|
||||
return _serviceProvider.GetService(serviceType);
|
||||
}
|
||||
|
||||
public object ResolveService(string serviceName, string[] genericParameters)
|
||||
{
|
||||
var serviceType = DetermineRootType(serviceName, genericParameters.Length);
|
||||
var genericTypes = genericParameters.Select(_genericTypeParam => DetermineRootType(_genericTypeParam));
|
||||
var resolvedServiceType = serviceType.MakeGenericType(genericTypes.ToArray());
|
||||
return _serviceProvider.GetService(resolvedServiceType);
|
||||
}
|
||||
|
||||
private Type DetermineRootType(string serviceName, int genericParamCount = 0)
|
||||
{
|
||||
var typeCollection = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(t => t.GetTypes());
|
||||
string generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
|
||||
var serviceType = typeCollection.FirstOrDefault(_type => _type.Name.ToLower() == generatedName);
|
||||
|
||||
if (serviceType == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No object type '{serviceName}' defined in loaded assemblies");
|
||||
}
|
||||
|
||||
return serviceType;
|
||||
}
|
||||
}
|
||||
}
|
@ -203,10 +203,11 @@ namespace IW4MAdmin.Application.RconParsers
|
||||
|
||||
long networkId;
|
||||
string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
|
||||
string networkIdString;
|
||||
|
||||
try
|
||||
{
|
||||
string networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
|
||||
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
|
||||
|
||||
networkId = networkIdString.IsBotGuid() ?
|
||||
name.GenerateGuidFromString() :
|
||||
@ -234,6 +235,8 @@ namespace IW4MAdmin.Application.RconParsers
|
||||
State = EFClient.ClientState.Connecting
|
||||
};
|
||||
|
||||
client.SetAdditionalProperty("BotGuid", networkIdString);
|
||||
|
||||
StatusPlayers.Add(client);
|
||||
}
|
||||
}
|
||||
|
BIN
GameFiles/PT6/t6r/data/maps/mp/gametypes/_clientids.gsc
Normal file
BIN
GameFiles/PT6/t6r/data/maps/mp/gametypes/_clientids.gsc
Normal file
Binary file not shown.
283
GameFiles/PT6/t6r/data/maps/mp/gametypes/_clientids.gsc.src
Normal file
283
GameFiles/PT6/t6r/data/maps/mp/gametypes/_clientids.gsc.src
Normal file
@ -0,0 +1,283 @@
|
||||
#include maps\mp\_utility;
|
||||
#include maps\mp\gametypes\_hud_util;
|
||||
#include common_scripts\utility;
|
||||
|
||||
init()
|
||||
{
|
||||
level.clientid = 0;
|
||||
level thread onplayerconnect();
|
||||
level thread IW4MA_init();
|
||||
}
|
||||
|
||||
onplayerconnect()
|
||||
{
|
||||
for ( ;; )
|
||||
{
|
||||
level waittill( "connecting", player );
|
||||
player.clientid = level.clientid;
|
||||
level.clientid++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
IW4MA_init()
|
||||
{
|
||||
SetDvarIfUninitialized( "sv_customcallbacks", true );
|
||||
SetDvarIfUninitialized( "sv_framewaittime", 0.05 );
|
||||
SetDvarIfUninitialized( "sv_additionalwaittime", 0.1 );
|
||||
SetDvarIfUninitialized( "sv_maxstoredframes", 12 );
|
||||
SetDvarIfUninitialized( "sv_printradarupdates", 0 );
|
||||
SetDvarIfUninitialized( "sv_printradar_updateinterval", 500 );
|
||||
SetDvarIfUninitialized( "sv_iw4madmin_url", "http://127.0.0.1:1624" );
|
||||
|
||||
level thread IW4MA_onPlayerConnect();
|
||||
if (getDvarInt("sv_printradarupdates") == 1)
|
||||
{
|
||||
level thread runRadarUpdates();
|
||||
}
|
||||
|
||||
level waittill( "prematch_over" );
|
||||
level.callbackPlayerKilled = ::Callback_PlayerKilled;
|
||||
level.callbackPlayerDamage = ::Callback_PlayerDamage;
|
||||
level.callbackPlayerDisconnect = ::Callback_PlayerDisconnect;
|
||||
}
|
||||
|
||||
//Does not exist in T6
|
||||
SetDvarIfUninitialized(dvar, val)
|
||||
{
|
||||
curval = getDvar(dvar);
|
||||
if (curval == "")
|
||||
SetDvar(dvar,val);
|
||||
}
|
||||
|
||||
IW4MA_onPlayerConnect( player )
|
||||
{
|
||||
for( ;; )
|
||||
{
|
||||
level waittill( "connected", player );
|
||||
player thread waitForFrameThread();
|
||||
//player thread waitForAttack();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Does not work in T6
|
||||
/*waitForAttack()
|
||||
{
|
||||
self endon( "disconnect" );
|
||||
|
||||
self.lastAttackTime = 0;
|
||||
|
||||
for( ;; )
|
||||
{
|
||||
self notifyOnPlayerCommand( "player_shot", "+attack" );
|
||||
self waittill( "player_shot" );
|
||||
|
||||
self.lastAttackTime = getTime();
|
||||
}
|
||||
}*/
|
||||
|
||||
runRadarUpdates()
|
||||
{
|
||||
interval = int(getDvar("sv_printradar_updateinterval"));
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
for ( i = 0; i <= 17; i++ )
|
||||
{
|
||||
player = level.players[i];
|
||||
|
||||
if ( isDefined( player ) )
|
||||
{
|
||||
payload = player.guid + ";" + player.origin + ";" + player getPlayerAngles() + ";" + player.team + ";" + player.kills + ";" + player.deaths + ";" + player.score + ";" + player GetCurrentWeapon() + ";" + player.health + ";" + isAlive(player) + ";" + player.timePlayed["total"];
|
||||
logPrint( "LiveRadar;" + payload + "\n" );
|
||||
}
|
||||
}
|
||||
|
||||
wait( interval / 1000 );
|
||||
}
|
||||
}
|
||||
|
||||
hitLocationToBone( hitloc )
|
||||
{
|
||||
switch( hitloc )
|
||||
{
|
||||
case "helmet":
|
||||
return "j_helmet";
|
||||
case "head":
|
||||
return "j_head";
|
||||
case "neck":
|
||||
return "j_neck";
|
||||
case "torso_upper":
|
||||
return "j_spineupper";
|
||||
case "torso_lower":
|
||||
return "j_spinelower";
|
||||
case "right_arm_upper":
|
||||
return "j_shoulder_ri";
|
||||
case "left_arm_upper":
|
||||
return "j_shoulder_le";
|
||||
case "right_arm_lower":
|
||||
return "j_elbow_ri";
|
||||
case "left_arm_lower":
|
||||
return "j_elbow_le";
|
||||
case "right_hand":
|
||||
return "j_wrist_ri";
|
||||
case "left_hand":
|
||||
return "j_wrist_le";
|
||||
case "right_leg_upper":
|
||||
return "j_hip_ri";
|
||||
case "left_leg_upper":
|
||||
return "j_hip_le";
|
||||
case "right_leg_lower":
|
||||
return "j_knee_ri";
|
||||
case "left_leg_lower":
|
||||
return "j_knee_le";
|
||||
case "right_foot":
|
||||
return "j_ankle_ri";
|
||||
case "left_foot":
|
||||
return "j_ankle_le";
|
||||
default:
|
||||
return "tag_origin";
|
||||
}
|
||||
}
|
||||
|
||||
waitForFrameThread()
|
||||
{
|
||||
self endon( "disconnect" );
|
||||
|
||||
self.currentAnglePosition = 0;
|
||||
self.anglePositions = [];
|
||||
|
||||
for (i = 0; i < getDvarInt( "sv_maxstoredframes" ); i++)
|
||||
{
|
||||
self.anglePositions[i] = self getPlayerAngles();
|
||||
}
|
||||
|
||||
for( ;; )
|
||||
{
|
||||
self.anglePositions[self.currentAnglePosition] = self getPlayerAngles();
|
||||
wait( getDvarFloat( "sv_framewaittime" ) );
|
||||
self.currentAnglePosition = (self.currentAnglePosition + 1) % getDvarInt( "sv_maxstoredframes" );
|
||||
}
|
||||
}
|
||||
|
||||
waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
|
||||
{
|
||||
currentIndex = self.currentAnglePosition;
|
||||
wait( 0.05 * afterFrameCount );
|
||||
|
||||
self.angleSnapshot = [];
|
||||
|
||||
for( j = 0; j < self.anglePositions.size; j++ )
|
||||
{
|
||||
self.angleSnapshot[j] = self.anglePositions[j];
|
||||
}
|
||||
|
||||
anglesStr = "";
|
||||
collectedFrames = 0;
|
||||
i = currentIndex - beforeFrameCount;
|
||||
|
||||
while (collectedFrames < beforeFrameCount)
|
||||
{
|
||||
fixedIndex = i;
|
||||
if (i < 0)
|
||||
{
|
||||
fixedIndex = self.angleSnapshot.size - abs(i);
|
||||
}
|
||||
anglesStr += self.angleSnapshot[int(fixedIndex)] + ":";
|
||||
collectedFrames++;
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i == currentIndex)
|
||||
{
|
||||
anglesStr += self.angleSnapshot[i] + ":";
|
||||
i++;
|
||||
}
|
||||
|
||||
collectedFrames = 0;
|
||||
|
||||
while (collectedFrames < afterFrameCount)
|
||||
{
|
||||
fixedIndex = i;
|
||||
if (i > self.angleSnapshot.size - 1)
|
||||
{
|
||||
fixedIndex = i % self.angleSnapshot.size;
|
||||
}
|
||||
anglesStr += self.angleSnapshot[int(fixedIndex)] + ":";
|
||||
collectedFrames++;
|
||||
i++;
|
||||
}
|
||||
|
||||
lastAttack = 100;//int(getTime()) - int(self.lastAttackTime);
|
||||
isAlive = isAlive(self);
|
||||
|
||||
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
||||
}
|
||||
|
||||
vectorScale( vector, scale )
|
||||
{
|
||||
return ( vector[0] * scale, vector[1] * scale, vector[2] * scale );
|
||||
}
|
||||
|
||||
Process_Hit( type, attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon )
|
||||
{
|
||||
if (sMeansOfDeath == "MOD_FALLING" || !isPlayer(attacker))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
victim = self;
|
||||
_attacker = attacker;
|
||||
|
||||
if ( !isPlayer( attacker ) && isDefined( attacker.owner ) )
|
||||
{
|
||||
_attacker = attacker.owner;
|
||||
}
|
||||
|
||||
else if( !isPlayer( attacker ) && sMeansOfDeath == "MOD_FALLING" )
|
||||
{
|
||||
_attacker = victim;
|
||||
}
|
||||
|
||||
location = victim GetTagOrigin( hitLocationToBone( sHitLoc ) );
|
||||
isKillstreakKill = false;
|
||||
if(!isPlayer(attacker))
|
||||
{
|
||||
isKillstreakKill = true;
|
||||
}
|
||||
if(maps/mp/killstreaks/_killstreaks::iskillstreakweapon(sWeapon))
|
||||
{
|
||||
isKillstreakKill = true;
|
||||
}
|
||||
|
||||
logLine = "Script" + type + ";" + _attacker.guid + ";" + victim.guid + ";" + _attacker GetTagOrigin("tag_eye") + ";" + location + ";" + iDamage + ";" + sWeapon + ";" + sHitLoc + ";" + sMeansOfDeath + ";" + _attacker getPlayerAngles() + ";" + int(gettime()) + ";" + isKillstreakKill + ";" + _attacker playerADS() + ";0;0";
|
||||
attacker thread waitForAdditionalAngles( logLine, 2, 2 );
|
||||
}
|
||||
|
||||
Callback_PlayerDamage( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex )
|
||||
{
|
||||
if ( level.teamBased && isDefined( attacker ) && ( self != attacker ) && isDefined( attacker.team ) && ( self.pers[ "team" ] == attacker.team ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ( self.health - iDamage > 0 )
|
||||
{
|
||||
self Process_Hit( "Damage", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
|
||||
}
|
||||
|
||||
self [[maps/mp/gametypes/_globallogic_player::callback_playerdamage]]( eInflictor, attacker, iDamage, iDFlags, sMeansOfDeath, sWeapon, vPoint, vDir, sHitLoc, psOffsetTime, boneIndex );
|
||||
}
|
||||
|
||||
Callback_PlayerKilled(eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration)
|
||||
{
|
||||
Process_Hit( "Kill", attacker, sHitLoc, sMeansOfDeath, iDamage, sWeapon );
|
||||
self [[maps/mp/gametypes/_globallogic_player::callback_playerkilled]]( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, vDir, sHitLoc, psOffsetTime, deathAnimDuration );
|
||||
}
|
||||
|
||||
Callback_PlayerDisconnect()
|
||||
{
|
||||
level notify( "disconnected", self );
|
||||
self [[maps/mp/gametypes/_globallogic_player::callback_playerdisconnect]]();
|
||||
}
|
@ -8,8 +8,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
GameFiles\IW4x\userraw\scripts\_commands.gsc = GameFiles\IW4x\userraw\scripts\_commands.gsc
|
||||
GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc
|
||||
azure-pipelines.yml = azure-pipelines.yml
|
||||
PostPublish.ps1 = PostPublish.ps1
|
||||
pre-release-pipeline.yml = pre-release-pipeline.yml
|
||||
README.md = README.md
|
||||
RunPublishPre.cmd = RunPublishPre.cmd
|
||||
RunPublishRelease.cmd = RunPublishRelease.cmd
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.3" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.3" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -74,14 +74,14 @@ namespace LiveRadar.Web.Controllers
|
||||
[Route("Radar/Update")]
|
||||
public IActionResult Update(string payload)
|
||||
{
|
||||
var radarUpdate = RadarEvent.Parse(payload);
|
||||
/*var radarUpdate = RadarEvent.Parse(payload);
|
||||
var client = _manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
|
||||
|
||||
if (client != null)
|
||||
{
|
||||
radarUpdate.Name = client.Name.StripColors();
|
||||
client.SetAdditionalProperty("LiveRadar", radarUpdate);
|
||||
}
|
||||
}*/
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
33
Plugins/LiveRadar/Events/Script.cs
Normal file
33
Plugins/LiveRadar/Events/Script.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using EventGeneratorCallback = System.ValueTuple<string, string,
|
||||
System.Func<string, SharedLibraryCore.Interfaces.IEventParserConfiguration,
|
||||
SharedLibraryCore.GameEvent,
|
||||
SharedLibraryCore.GameEvent>>;
|
||||
|
||||
namespace LiveRadar.Events
|
||||
{
|
||||
public class Script : IRegisterEvent
|
||||
{
|
||||
private const string EVENT_LIVERADAR = "LiveRadar";
|
||||
private EventGeneratorCallback LiveRadar()
|
||||
{
|
||||
return (EVENT_LIVERADAR, EVENT_LIVERADAR, (string eventLine, IEventParserConfiguration config, GameEvent autoEvent) =>
|
||||
{
|
||||
string[] lineSplit = eventLine.Split(";");
|
||||
|
||||
autoEvent.Type = GameEvent.EventType.Other;
|
||||
autoEvent.Subtype = EVENT_LIVERADAR;
|
||||
autoEvent.Origin = new EFClient() { NetworkId = 0 };
|
||||
autoEvent.Extra = lineSplit[1]; // guid
|
||||
|
||||
return autoEvent;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public IEnumerable<EventGeneratorCallback> Events => new[] { LiveRadar() };
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.3" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -3,6 +3,7 @@ using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -17,12 +18,14 @@ namespace LiveRadar
|
||||
public string Author => "RaidMax";
|
||||
|
||||
private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler;
|
||||
private readonly Dictionary<string, long> _botGuidLookups;
|
||||
private bool addedPage;
|
||||
private readonly object lockObject = new object();
|
||||
|
||||
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory)
|
||||
{
|
||||
_configurationHandler = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration");
|
||||
_botGuidLookups = new Dictionary<string, long>();
|
||||
}
|
||||
|
||||
public Task OnEventAsync(GameEvent E, Server S)
|
||||
@ -41,28 +44,45 @@ namespace LiveRadar
|
||||
}
|
||||
}
|
||||
|
||||
if (E.Type == GameEvent.EventType.Unknown)
|
||||
if (E.Type == GameEvent.EventType.PreConnect && E.Origin.IsBot)
|
||||
{
|
||||
if (E.Data?.StartsWith("LiveRadar") ?? false)
|
||||
string botKey = $"BotGuid_{E.Extra}";
|
||||
lock (lockObject)
|
||||
{
|
||||
try
|
||||
if (!_botGuidLookups.ContainsKey(botKey))
|
||||
{
|
||||
var radarUpdate = RadarEvent.Parse(E.Data);
|
||||
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
|
||||
_botGuidLookups.Add(botKey, E.Origin.NetworkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (client != null)
|
||||
{
|
||||
radarUpdate.Name = client.Name.StripColors();
|
||||
client.SetAdditionalProperty("LiveRadar", radarUpdate);
|
||||
}
|
||||
if (E.Type == GameEvent.EventType.Other && E.Subtype == "LiveRadar")
|
||||
{
|
||||
try
|
||||
{
|
||||
string botKey = $"BotGuid_{E.Extra}";
|
||||
long generatedBotGuid;
|
||||
|
||||
lock (lockObject)
|
||||
{
|
||||
generatedBotGuid = _botGuidLookups.ContainsKey(botKey) ? _botGuidLookups[botKey] : 0;
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
var radarUpdate = RadarEvent.Parse(E.Data, generatedBotGuid);
|
||||
var client = S.Manager.GetActiveClients().FirstOrDefault(_client => _client.NetworkId == radarUpdate.Guid);
|
||||
|
||||
if (client != null)
|
||||
{
|
||||
S.Logger.WriteWarning($"Could not parse live radar output: {e.Data}");
|
||||
S.Logger.WriteDebug(e.GetExceptionInfo());
|
||||
radarUpdate.Name = client.Name.StripColors();
|
||||
client.SetAdditionalProperty("LiveRadar", radarUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
S.Logger.WriteWarning($"Could not parse live radar output: {e.Data}");
|
||||
S.Logger.WriteDebug(e.GetExceptionInfo());
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
@ -1,9 +1,7 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace LiveRadar
|
||||
{
|
||||
@ -39,13 +37,13 @@ namespace LiveRadar
|
||||
return false;
|
||||
}
|
||||
|
||||
public static RadarEvent Parse(string input)
|
||||
public static RadarEvent Parse(string input, long generatedBotGuid)
|
||||
{
|
||||
var items = input.Split(';').Skip(1).ToList();
|
||||
|
||||
var parsedEvent = new RadarEvent()
|
||||
{
|
||||
Guid = items[0].ConvertGuidToLong(System.Globalization.NumberStyles.HexNumber),
|
||||
Guid = generatedBotGuid,
|
||||
Location = Vector3.Parse(items[1]),
|
||||
ViewAngles = Vector3.Parse(items[2]).FixIW4Angles(),
|
||||
Team = items[3],
|
||||
|
@ -23,7 +23,7 @@
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.3" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.3" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -20,6 +20,7 @@ var plugin = {
|
||||
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.CommandPrefixes.RConGetInfo = undefined; // adding this in here temporarily until getInfo is fixed in new T6 version
|
||||
|
||||
rconParser.Configuration.Dvar.Pattern = '^(.+) is "(.+)?"$';
|
||||
rconParser.Configuration.Dvar.AddMapping(106, 1);
|
||||
|
@ -3,7 +3,7 @@ var eventParser;
|
||||
|
||||
var plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 0.6,
|
||||
version: 0.7,
|
||||
name: 'Tekno MW3 Parser',
|
||||
isParser: true,
|
||||
|
||||
@ -29,6 +29,7 @@ var plugin = {
|
||||
rconParser.Configuration.Dvar.Pattern = '^(.*)$';
|
||||
|
||||
rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1');
|
||||
rconParser.Configuration.OverrideDvarNameMapping.Add('_website', 'sv_clanWebsite');
|
||||
|
||||
rconParser.Version = 'IW5 MP 1.4 build 382 latest Thu Jan 19 2012 11:09:49AM win-x86';
|
||||
rconParser.GameName = 3; // IW5
|
||||
|
@ -7,6 +7,8 @@ let commands = [{
|
||||
alias: "pp",
|
||||
// required
|
||||
permission: "User",
|
||||
// optional (defaults to false)
|
||||
targetRequired: false,
|
||||
// optional
|
||||
arguments: [{
|
||||
name: "times to ping",
|
||||
@ -44,6 +46,8 @@ let plugin = {
|
||||
},
|
||||
|
||||
onLoadAsync: function (manager) {
|
||||
this.logger = _serviceResolver.ResolveService("ILogger");
|
||||
this.logger.WriteDebug("sample plugin loaded");
|
||||
},
|
||||
|
||||
onUnloadAsync: function () {
|
||||
|
@ -20,7 +20,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
Offset,
|
||||
Strain,
|
||||
Recoil,
|
||||
Snap
|
||||
Snap,
|
||||
Button
|
||||
};
|
||||
|
||||
public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
|
||||
@ -38,11 +39,12 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
ILogger Log;
|
||||
Strain Strain;
|
||||
readonly DateTime ConnectionTime = DateTime.UtcNow;
|
||||
private double sessionAverageRecoilAmount;
|
||||
private double mapAverageRecoilAmount;
|
||||
private double sessionAverageSnapAmount;
|
||||
private int sessionSnapHits;
|
||||
private EFClientKill lastHit;
|
||||
private int validRecoilHitCount;
|
||||
private int validButtonHitCount;
|
||||
|
||||
private class HitInfo
|
||||
{
|
||||
@ -282,18 +284,30 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
|
||||
#region RECOIL
|
||||
float hitRecoilAverage = 0;
|
||||
if (!Plugin.Config.Configuration().RecoilessWeapons.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex)))
|
||||
bool shouldIgnoreDetection = false;
|
||||
try
|
||||
{
|
||||
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Recoil]
|
||||
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
|
||||
}
|
||||
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
if (!shouldIgnoreDetection)
|
||||
{
|
||||
validRecoilHitCount++;
|
||||
hitRecoilAverage = (hit.AnglesList.Sum(_angle => _angle.Z) + hit.ViewAngles.Z) / (hit.AnglesList.Count + 1);
|
||||
sessionAverageRecoilAmount = (sessionAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount;
|
||||
mapAverageRecoilAmount = (mapAverageRecoilAmount * (validRecoilHitCount - 1) + hitRecoilAverage) / validRecoilHitCount;
|
||||
|
||||
if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && sessionAverageRecoilAmount == 0)
|
||||
if (validRecoilHitCount >= Thresholds.LowSampleMinKills && Kills > Thresholds.LowSampleMinKillsRecoil && mapAverageRecoilAmount == 0)
|
||||
{
|
||||
results.Add(new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = EFPenalty.PenaltyType.Ban,
|
||||
Value = sessionAverageRecoilAmount,
|
||||
Value = mapAverageRecoilAmount,
|
||||
HitCount = HitCount,
|
||||
Type = DetectionType.Recoil
|
||||
});
|
||||
@ -301,6 +315,37 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region BUTTON
|
||||
try
|
||||
{
|
||||
shouldIgnoreDetection = false;
|
||||
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Button]
|
||||
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
|
||||
}
|
||||
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
if (!shouldIgnoreDetection)
|
||||
{
|
||||
validButtonHitCount++;
|
||||
}
|
||||
|
||||
double lastDiff = hit.TimeOffset - hit.TimeSinceLastAttack;
|
||||
if (validButtonHitCount > 0 && lastDiff <= 0)
|
||||
{
|
||||
results.Add(new DetectionPenaltyResult()
|
||||
{
|
||||
ClientPenalty = EFPenalty.PenaltyType.Ban,
|
||||
Value = lastDiff,
|
||||
HitCount = HitCount,
|
||||
Type = DetectionType.Button
|
||||
});
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SESSION_RATIOS
|
||||
if (Kills >= Thresholds.LowSampleMinKills)
|
||||
{
|
||||
@ -384,7 +429,19 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
#region CHEST_ABDOMEN_RATIO_SESSION
|
||||
int chestHits = HitLocationCount[IW4Info.HitLocation.torso_upper].Count;
|
||||
|
||||
if (chestHits >= Thresholds.MediumSampleMinKills)
|
||||
try
|
||||
{
|
||||
shouldIgnoreDetection = false; // reset previous value
|
||||
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[hit.GameName][DetectionType.Chest]
|
||||
.Any(_weaponRegex => Regex.IsMatch(hit.Weapon.ToString(), _weaponRegex));
|
||||
}
|
||||
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
if (chestHits >= Thresholds.MediumSampleMinKills && !shouldIgnoreDetection)
|
||||
{
|
||||
double marginOfError = Thresholds.GetMarginOfError(chestHits);
|
||||
double lerpAmount = Math.Min(1.0, (chestHits - Thresholds.MediumSampleMinKills) / (double)(Thresholds.HighSampleMinKills - Thresholds.LowSampleMinKills));
|
||||
@ -466,5 +523,11 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public void OnMapChange()
|
||||
{
|
||||
mapAverageRecoilAmount = 0;
|
||||
validRecoilHitCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
|
||||
List<string> mostPlayed = new List<string>()
|
||||
{
|
||||
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--"
|
||||
$"^5--{translationLookup["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--"
|
||||
};
|
||||
|
||||
using (var db = new DatabaseContext(true))
|
||||
@ -51,8 +51,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
|
||||
var iqList = await iqStats.ToListAsync();
|
||||
|
||||
mostPlayed.AddRange(iqList.Select(stats =>
|
||||
$"^3{stats.Name}^7 - ^5{stats.Kills} ^7{translationLookup["PLUGINS_STATS_TEXT_KILLS"]} | ^5{Utilities.GetTimePassed(DateTime.UtcNow.AddSeconds(-stats.TotalConnectionTime), false)} ^7{translationLookup["WEBFRONT_PROFILE_PLAYER"].ToLower()}"));
|
||||
mostPlayed.AddRange(iqList.Select(stats => translationLookup["COMMANDS_MOST_PLAYED_FORMAT"].FormatExt(stats.Name, stats.Kills, (DateTime.UtcNow - DateTime.UtcNow.AddSeconds(-stats.TotalConnectionTime)).HumanizeForCurrentCulture())));
|
||||
}
|
||||
|
||||
|
||||
|
24
Plugins/Stats/Config/AnticheatConfiguration.cs
Normal file
24
Plugins/Stats/Config/AnticheatConfiguration.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace Stats.Config
|
||||
{
|
||||
public class AnticheatConfiguration
|
||||
{
|
||||
public bool Enable { get; set; }
|
||||
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; } = new Dictionary<long, DetectionType[]>();
|
||||
public IList<long> IgnoredClientIds { get; set; } = new List<long>();
|
||||
public IDictionary<Game, IDictionary<DetectionType, string[]>> IgnoredDetectionSpecification{ get; set; } = new Dictionary<Game, IDictionary<DetectionType, string[]>>
|
||||
{
|
||||
{
|
||||
Game.IW4, new Dictionary<DetectionType, string[]>
|
||||
{
|
||||
{ DetectionType.Chest, new[] { "m21.+" } },
|
||||
{ DetectionType.Recoil, new[] { "ranger.*_mp", "model1887.*_mp", ".+shotgun.*_mp" } },
|
||||
{ DetectionType.Button, new[] { ".*akimbo.*" } }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using Stats.Config;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
|
||||
|
||||
@ -8,21 +9,41 @@ namespace IW4MAdmin.Plugins.Stats.Config
|
||||
{
|
||||
public class StatsConfiguration : IBaseConfiguration
|
||||
{
|
||||
public bool EnableAntiCheat { get; set; }
|
||||
[Obsolete]
|
||||
public bool? EnableAntiCheat { get; set; }
|
||||
public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
|
||||
public List<StreakMessageConfiguration> DeathstreakMessages { get; set; }
|
||||
public List<string> RecoilessWeapons { get; set; }
|
||||
public int TopPlayersMinPlayTime { get; set; }
|
||||
public bool StoreClientKills { get; set; }
|
||||
public int MostKillsMaxInactivityDays { get; set; } = 30;
|
||||
public int MostKillsClientLimit { get; set; } = 5;
|
||||
public IDictionary<DetectionType, DistributionConfiguration> DetectionDistributions { get; set; }
|
||||
[Obsolete]
|
||||
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; }
|
||||
public AnticheatConfiguration AnticheatConfiguration { get; set; } = new AnticheatConfiguration();
|
||||
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
public void ApplyMigration()
|
||||
{
|
||||
if (ServerDetectionTypes != null)
|
||||
{
|
||||
AnticheatConfiguration.ServerDetectionTypes = ServerDetectionTypes;
|
||||
}
|
||||
|
||||
ServerDetectionTypes = null;
|
||||
|
||||
if (EnableAntiCheat != null)
|
||||
{
|
||||
AnticheatConfiguration.Enable = EnableAntiCheat.Value;
|
||||
}
|
||||
|
||||
EnableAntiCheat = null;
|
||||
}
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
|
||||
public string Name() => "StatsPluginSettings";
|
||||
public IBaseConfiguration Generate()
|
||||
{
|
||||
EnableAntiCheat = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]);
|
||||
AnticheatConfiguration.Enable = Utilities.PromptBool(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_SETUP_ENABLEAC"]);
|
||||
KillstreakMessages = new List<StreakMessageConfiguration>()
|
||||
{
|
||||
new StreakMessageConfiguration(){
|
||||
@ -57,16 +78,9 @@ namespace IW4MAdmin.Plugins.Stats.Config
|
||||
},
|
||||
};
|
||||
|
||||
RecoilessWeapons = new List<string>()
|
||||
{
|
||||
"ranger.*_mp",
|
||||
"model1887.*_mp",
|
||||
".+shotgun.*_mp"
|
||||
};
|
||||
|
||||
TopPlayersMinPlayTime = 3600 * 3;
|
||||
StoreClientKills = false;
|
||||
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using System;
|
||||
|
||||
namespace StatsWeb.Dtos
|
||||
namespace Stats.Dtos
|
||||
{
|
||||
public class ChatSearchQuery : PaginationInfo
|
||||
public class ChatSearchQuery : ClientPaginationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// specifies the partial content of the message to search for
|
||||
@ -18,7 +18,7 @@ namespace StatsWeb.Dtos
|
||||
/// <summary>
|
||||
/// identifier for the client
|
||||
/// </summary>
|
||||
public int? ClientId { get; set; }
|
||||
public new int? ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// only look for messages sent after this date
|
||||
@ -29,5 +29,10 @@ namespace StatsWeb.Dtos
|
||||
/// only look for messages sent before this date0
|
||||
/// </summary>
|
||||
public DateTime SentBefore { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// indicates if the chat is on the meta page
|
||||
/// </summary>
|
||||
public bool IsProfileMeta { get; set; }
|
||||
}
|
||||
}
|
@ -156,7 +156,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
Deaths = s.Deaths,
|
||||
Kills = s.Kills,
|
||||
KDR = Math.Round(s.KDR, 2),
|
||||
LastSeen = Utilities.GetTimePassed(clientRatingsDict[s.ClientId].LastConnection, false),
|
||||
LastSeen = (DateTime.UtcNow - clientRatingsDict[s.ClientId].LastConnection).HumanizeForCurrentCulture(),
|
||||
Name = clientRatingsDict[s.ClientId].Name,
|
||||
Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2),
|
||||
RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
|
||||
@ -248,6 +248,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ctx.Entry(server).Property(_prop => _prop.HostName).IsModified = true;
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
ctx.Entry(server).Property(_prop => _prop.IsPasswordProtected).IsModified = true;
|
||||
server.IsPasswordProtected = !string.IsNullOrEmpty(sv.GamePassword);
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
// check to see if the stats have ever been initialized
|
||||
@ -477,7 +481,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
IsKill = !isDamage,
|
||||
AnglesList = snapshotAngles,
|
||||
IsAlive = isAlive == "1",
|
||||
TimeSinceLastAttack = long.Parse(lastAttackTime)
|
||||
TimeSinceLastAttack = long.Parse(lastAttackTime),
|
||||
GameName = attacker.CurrentServer.GameName
|
||||
};
|
||||
|
||||
if (hit.HitLoc == IW4Info.HitLocation.shield)
|
||||
@ -535,7 +540,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
if (Plugin.Config.Configuration().EnableAntiCheat && !attacker.IsBot && attacker.ClientId != victim.ClientId)
|
||||
if (Plugin.Config.Configuration().AnticheatConfiguration.Enable && !attacker.IsBot && attacker.ClientId != victim.ClientId)
|
||||
{
|
||||
clientDetection.TrackedHits.Add(hit);
|
||||
|
||||
@ -551,10 +556,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
if (oldestHit.IsAlive)
|
||||
{
|
||||
var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker.CurrentServer.EndPoint);
|
||||
#if !DEBUG
|
||||
await ApplyPenalty(result, attacker);
|
||||
#endif
|
||||
var result = DeterminePenaltyResult(clientDetection.ProcessHit(oldestHit), attacker);
|
||||
|
||||
if (!Utilities.IsDevelopment)
|
||||
{
|
||||
await ApplyPenalty(result, attacker);
|
||||
}
|
||||
|
||||
if (clientDetection.Tracker.HasChanges && result.ClientPenalty != EFPenalty.PenaltyType.Any)
|
||||
{
|
||||
@ -590,10 +597,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> results, long serverId)
|
||||
private DetectionPenaltyResult DeterminePenaltyResult(IEnumerable<DetectionPenaltyResult> results, EFClient client)
|
||||
{
|
||||
// allow disabling of certain detection types
|
||||
results = results.Where(_result => ShouldUseDetection(serverId, _result.Type));
|
||||
results = results.Where(_result => ShouldUseDetection(client.CurrentServer, _result.Type, client.ClientId));
|
||||
return results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Ban) ??
|
||||
results.FirstOrDefault(_result => _result.ClientPenalty == EFPenalty.PenaltyType.Flag) ??
|
||||
new DetectionPenaltyResult()
|
||||
@ -613,21 +620,31 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldUseDetection(long serverId, DetectionType detectionType)
|
||||
private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
|
||||
{
|
||||
var detectionTypes = Plugin.Config.Configuration().ServerDetectionTypes;
|
||||
var detectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes;
|
||||
var ignoredClients = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredClientIds;
|
||||
|
||||
if (detectionTypes == null)
|
||||
if (ignoredClients.Contains(clientId))
|
||||
{
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!detectionTypes.ContainsKey(serverId))
|
||||
|
||||
try
|
||||
{
|
||||
return true;
|
||||
if (detectionTypes[server.EndPoint].Contains(detectionType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return detectionTypes[serverId].Contains(detectionType);
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async Task ApplyPenalty(DetectionPenaltyResult penalty, EFClient attacker)
|
||||
@ -663,6 +680,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
|
||||
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}";
|
||||
|
||||
penaltyClient.AdministeredPenalties = new List<EFPenalty>()
|
||||
{
|
||||
new EFPenalty()
|
||||
{
|
||||
AutomatedOffense = flagReason
|
||||
}
|
||||
};
|
||||
|
||||
await attacker.Flag(flagReason, penaltyClient, new TimeSpan(168, 0, 0)).WaitAsync(Utilities.DefaultCommandTimeout, attacker.CurrentServer.Manager.CancellationToken);
|
||||
break;
|
||||
}
|
||||
@ -1127,10 +1152,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
public void ResetKillstreaks(Server sv)
|
||||
{
|
||||
foreach (var stat in sv.GetClientsAsList()
|
||||
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY)))
|
||||
foreach (var session in sv.GetClientsAsList()
|
||||
.Select(_client => new
|
||||
{
|
||||
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
|
||||
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
|
||||
}))
|
||||
{
|
||||
stat?.StartNewSession();
|
||||
session.stat?.StartNewSession();
|
||||
session.detection?.OnMapChange();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1220,6 +1250,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
return 886229536;
|
||||
}
|
||||
|
||||
// todo: this is not stable and will need to be migrated again...
|
||||
long id = HashCode.Combine(server.IP, server.Port);
|
||||
id = id < 0 ? Math.Abs(id) : id;
|
||||
long? serverId;
|
||||
|
@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Collections.Generic;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Models
|
||||
{
|
||||
@ -44,6 +45,8 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
||||
public float AdsPercent { get; set; }
|
||||
[NotMapped]
|
||||
public List<Vector3> AnglesList { get; set; }
|
||||
[NotMapped]
|
||||
public Game GameName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the attacker was alive after last captured angle
|
||||
|
@ -16,5 +16,6 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
||||
public string EndPoint { get; set; }
|
||||
public Game? GameName { get; set; }
|
||||
public string HostName { get; set; }
|
||||
public bool IsPasswordProtected { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,11 @@ using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Services;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using Stats.Dtos;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -33,13 +34,17 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
#endif
|
||||
private readonly IDatabaseContextFactory _databaseContextFactory;
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
private readonly IMetaService _metaService;
|
||||
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatQueryHelper;
|
||||
|
||||
public Plugin(IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory,
|
||||
ITranslationLookup translationLookup)
|
||||
ITranslationLookup translationLookup, IMetaService metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper)
|
||||
{
|
||||
Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
|
||||
_databaseContextFactory = databaseContextFactory;
|
||||
_translationLookup = translationLookup;
|
||||
_metaService = metaService;
|
||||
_chatQueryHelper = chatQueryHelper;
|
||||
}
|
||||
|
||||
public async Task OnEventAsync(GameEvent E, Server S)
|
||||
@ -177,8 +182,9 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
if (Config.Configuration() == null)
|
||||
{
|
||||
Config.Set((StatsConfiguration)new StatsConfiguration().Generate());
|
||||
await Config.Save();
|
||||
}
|
||||
Config.Configuration().ApplyMigration();
|
||||
await Config.Save();
|
||||
|
||||
// register the topstats page
|
||||
// todo:generate the URL/Location instead of hardcoding
|
||||
@ -188,17 +194,14 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
"/Stats/TopPlayersAsync");
|
||||
|
||||
// meta data info
|
||||
async Task<List<ProfileMeta>> getStats(int clientId, int offset, int count, DateTime? startAt)
|
||||
async Task<IEnumerable<InformationResponse>> getStats(ClientPaginationRequest request)
|
||||
{
|
||||
if (count > 1)
|
||||
{
|
||||
return new List<ProfileMeta>();
|
||||
}
|
||||
|
||||
IList<EFClientStatistics> clientStats;
|
||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||
int messageCount = 0;
|
||||
using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false))
|
||||
{
|
||||
clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == clientId).ToListAsync();
|
||||
clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == request.ClientId).ToListAsync();
|
||||
messageCount = await ctx.Set<EFClientMessage>().CountAsync(_message => _message.ClientId == request.ClientId);
|
||||
}
|
||||
|
||||
int kills = clientStats.Sum(c => c.Kills);
|
||||
@ -209,73 +212,76 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
double performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
|
||||
double spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Where(c => c.SPM > 0).Count(), 1);
|
||||
|
||||
return new List<ProfileMeta>()
|
||||
return new List<InformationResponse>()
|
||||
{
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
|
||||
Value = "#" + (await Manager.GetClientOverallRanking(clientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Value = "#" + (await Manager.GetClientOverallRanking(request.ClientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 0,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"],
|
||||
Value = kills.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 1,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_DEATHS"],
|
||||
Value = deaths.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 2,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"],
|
||||
Value = kdr.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 3,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_PERFORMANCE"],
|
||||
Value = performance.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 4,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_META_SPM"],
|
||||
Value = spm.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 0,
|
||||
Order = 5,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGES"],
|
||||
Value = messageCount.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 1,
|
||||
Order = 4,
|
||||
Type = MetaType.Information
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async Task<List<ProfileMeta>> getAnticheatInfo(int clientId, int offset, int count, DateTime? startAt)
|
||||
async Task<IEnumerable<InformationResponse>> getAnticheatInfo(ClientPaginationRequest request)
|
||||
{
|
||||
if (count > 1)
|
||||
{
|
||||
return new List<ProfileMeta>();
|
||||
}
|
||||
|
||||
IList<EFClientStatistics> clientStats;
|
||||
|
||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||
using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false))
|
||||
{
|
||||
clientStats = await ctx.Set<EFClientStatistics>()
|
||||
.Include(c => c.HitLocations)
|
||||
.Where(c => c.ClientId == clientId)
|
||||
.Where(c => c.ClientId == request.ClientId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@ -310,159 +316,103 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
averageSnapValue = clientStats.Any(_stats => _stats.AverageSnapValue > 0) ? clientStats.Where(_stats => _stats.AverageSnapValue > 0).Average(_stat => _stat.AverageSnapValue) : 0;
|
||||
}
|
||||
|
||||
return new List<ProfileMeta>()
|
||||
return new List<InformationResponse>()
|
||||
{
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 1",
|
||||
Value = chestRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 0,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM1"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM1"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 2",
|
||||
Value = abdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 1,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM2"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM2"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 3",
|
||||
Value = chestAbdomenRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 2,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM3"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM3"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 4",
|
||||
Value = headRatio.ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)) + '%',
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 3,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM4"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM4"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 5",
|
||||
// todo: make sure this is wrapped somewhere else
|
||||
Value = $"{Math.Round(((float)hitOffsetAverage), 4).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))}°",
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 4,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM5"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM5"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 6",
|
||||
Value = Math.Round(maxStrain, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 5,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM6"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM6"],
|
||||
IsSensitive = true
|
||||
},
|
||||
new ProfileMeta()
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = $"{Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_AC_METRIC"]} 7",
|
||||
Value = Math.Round(averageSnapValue, 3).ToString(new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Type = ProfileMeta.MetaType.Information,
|
||||
Type = MetaType.Information,
|
||||
Column = 2,
|
||||
Order = 6,
|
||||
Extra = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM7"],
|
||||
Sensitive = true
|
||||
ToolTipText = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_TITLE_ACM7"],
|
||||
IsSensitive = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async Task<List<ProfileMeta>> getMessages(int clientId, int offset, int count, DateTime? startAt)
|
||||
async Task<IEnumerable<MessageResponse>> getMessages(ClientPaginationRequest request)
|
||||
{
|
||||
if (count <= 1)
|
||||
var query = new ChatSearchQuery()
|
||||
{
|
||||
using (var ctx = new DatabaseContext(true))
|
||||
{
|
||||
return new List<ProfileMeta>
|
||||
{
|
||||
new ProfileMeta()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGES"],
|
||||
Value = (await ctx.Set<EFClientMessage>()
|
||||
.CountAsync(_message => _message.ClientId == clientId))
|
||||
.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 1,
|
||||
Order= 4,
|
||||
Type = ProfileMeta.MetaType.Information
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
ClientId = request.ClientId,
|
||||
Before = request.Before,
|
||||
SentBefore = request.Before ?? DateTime.UtcNow,
|
||||
Count = request.Count,
|
||||
IsProfileMeta = true
|
||||
};
|
||||
|
||||
List<ProfileMeta> messageMeta;
|
||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||
{
|
||||
var messages = ctx.Set<EFClientMessage>()
|
||||
.Where(m => m.ClientId == clientId)
|
||||
.Where(_message => _message.TimeSent < startAt)
|
||||
.OrderByDescending(_message => _message.TimeSent)
|
||||
.Skip(offset)
|
||||
.Take(count);
|
||||
|
||||
messageMeta = await messages.Select(m => new ProfileMeta()
|
||||
{
|
||||
Key = null,
|
||||
Value = new { m.Message, m.Server.GameName },
|
||||
When = m.TimeSent,
|
||||
Extra = m.ServerId.ToString(),
|
||||
Type = ProfileMeta.MetaType.ChatMessage
|
||||
}).ToListAsync();
|
||||
|
||||
foreach (var message in messageMeta)
|
||||
{
|
||||
if ((message.Value.Message as string).IsQuickMessage())
|
||||
{
|
||||
try
|
||||
{
|
||||
var quickMessages = ServerManager.GetApplicationSettings().Configuration()
|
||||
.QuickMessages
|
||||
.First(_qm => _qm.Game == message.Value.GameName);
|
||||
message.Value = quickMessages.Messages[(message.Value.Message as string).Substring(1)];
|
||||
message.Type = ProfileMeta.MetaType.QuickMessage;
|
||||
}
|
||||
catch
|
||||
{
|
||||
message.Value = message.Value.Message;
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
message.Value = message.Value.Message;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return messageMeta;
|
||||
return (await _chatQueryHelper.QueryResource(query)).Results;
|
||||
}
|
||||
|
||||
if (Config.Configuration().EnableAntiCheat)
|
||||
if (Config.Configuration().AnticheatConfiguration.Enable)
|
||||
{
|
||||
MetaService.AddRuntimeMeta(getAnticheatInfo);
|
||||
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, getAnticheatInfo);
|
||||
}
|
||||
|
||||
MetaService.AddRuntimeMeta(getStats);
|
||||
MetaService.AddRuntimeMeta(getMessages);
|
||||
_metaService.AddRuntimeMeta<ClientPaginationRequest, InformationResponse>(MetaType.Information, getStats);
|
||||
_metaService.AddRuntimeMeta<ClientPaginationRequest, MessageResponse>(MetaType.ChatMessage, getMessages);
|
||||
|
||||
async Task<string> totalKills(Server server)
|
||||
{
|
||||
@ -547,6 +497,6 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
/// </summary>
|
||||
/// <param name="s"></param>
|
||||
/// <returns></returns>
|
||||
private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().EnableAntiCheat && s.GameName == Server.Game.IW5;
|
||||
private bool ShouldOverrideAnticheatSetting(Server s) => Config.Configuration().AnticheatConfiguration.Enable && s.GameName == Server.Game.IW5;
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.3" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -1,9 +1,13 @@
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using StatsWeb.Dtos;
|
||||
using Stats.Dtos;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -12,31 +16,44 @@ namespace StatsWeb
|
||||
/// <summary>
|
||||
/// implementation of IResourceQueryHelper
|
||||
/// </summary>
|
||||
public class ChatResourceQueryHelper : IResourceQueryHelper<ChatSearchQuery, ChatSearchResult>
|
||||
public class ChatResourceQueryHelper : IResourceQueryHelper<ChatSearchQuery, MessageResponse>
|
||||
{
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
private List<EFServer> serverCache;
|
||||
|
||||
public ChatResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
|
||||
public ChatResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory, ApplicationConfiguration appConfig)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
_logger = logger;
|
||||
_appConfig = appConfig;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ResourceQueryHelperResult<ChatSearchResult>> QueryResource(ChatSearchQuery query)
|
||||
public async Task<ResourceQueryHelperResult<MessageResponse>> QueryResource(ChatSearchQuery query)
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
throw new ArgumentException("Query must be specified");
|
||||
}
|
||||
|
||||
var result = new ResourceQueryHelperResult<ChatSearchResult>();
|
||||
var result = new ResourceQueryHelperResult<MessageResponse>();
|
||||
using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
|
||||
if (serverCache == null)
|
||||
{
|
||||
serverCache = await context.Set<EFServer>().ToListAsync();
|
||||
}
|
||||
|
||||
if (int.TryParse(query.ServerId, out int serverId))
|
||||
{
|
||||
query.ServerId = serverCache.FirstOrDefault(_server => _server.ServerId == serverId)?.EndPoint ?? query.ServerId;
|
||||
}
|
||||
|
||||
var iqMessages = context.Set<EFClientMessage>()
|
||||
.Where(_message => _message.TimeSent >= query.SentAfter)
|
||||
.Where(_message => _message.TimeSent <= query.SentBefore);
|
||||
.Where(_message => _message.TimeSent < query.SentBefore);
|
||||
|
||||
if (query.ClientId != null)
|
||||
{
|
||||
@ -50,27 +67,29 @@ namespace StatsWeb
|
||||
|
||||
if (!string.IsNullOrEmpty(query.MessageContains))
|
||||
{
|
||||
iqMessages = iqMessages.Where(_message => EF.Functions.Like(_message.Message, $"%{query.MessageContains}%"));
|
||||
iqMessages = iqMessages.Where(_message => EF.Functions.Like(_message.Message.ToLower(), $"%{query.MessageContains.ToLower()}%"));
|
||||
}
|
||||
|
||||
var iqResponse = iqMessages
|
||||
.Select(_message => new ChatSearchResult
|
||||
.Select(_message => new MessageResponse
|
||||
{
|
||||
ClientId = _message.ClientId,
|
||||
ClientName = _message.Client.CurrentAlias.Name,
|
||||
Date = _message.TimeSent,
|
||||
ClientName = query.IsProfileMeta ? "" : _message.Client.CurrentAlias.Name,
|
||||
ServerId = _message.ServerId,
|
||||
When = _message.TimeSent,
|
||||
Message = _message.Message,
|
||||
ServerName = _message.Server.HostName
|
||||
ServerName = query.IsProfileMeta ? "" : _message.Server.HostName,
|
||||
GameName = _message.Server.GameName == null ? Server.Game.IW4 : _message.Server.GameName.Value
|
||||
});
|
||||
|
||||
if (query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending)
|
||||
{
|
||||
iqResponse = iqResponse.OrderByDescending(_message => _message.Date);
|
||||
iqResponse = iqResponse.OrderByDescending(_message => _message.When);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
iqResponse = iqResponse.OrderBy(_message => _message.Date);
|
||||
iqResponse = iqResponse.OrderBy(_message => _message.When);
|
||||
}
|
||||
|
||||
var resultList = await iqResponse
|
||||
@ -78,6 +97,27 @@ namespace StatsWeb
|
||||
.Take(query.Count)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var message in resultList)
|
||||
{
|
||||
message.IsHidden = serverCache.Any(server => server.ServerId == message.ServerId && server.IsPasswordProtected);
|
||||
|
||||
if (message.Message.IsQuickMessage())
|
||||
{
|
||||
try
|
||||
{
|
||||
var quickMessages = _appConfig
|
||||
.QuickMessages
|
||||
.First(_qm => _qm.Game == message.GameName);
|
||||
message.Message = quickMessages.Messages[message.Message.Substring(1)];
|
||||
message.IsQuickMessage = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
message.Message = message.Message.Substring(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.TotalResultCount = await iqResponse.CountAsync();
|
||||
result.Results = resultList;
|
||||
result.RetrievedResultCount = resultList.Count;
|
||||
|
@ -5,8 +5,9 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using StatsWeb.Dtos;
|
||||
using Stats.Dtos;
|
||||
using StatsWeb.Extensions;
|
||||
using System;
|
||||
using System.Linq;
|
||||
@ -18,10 +19,10 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IManager _manager;
|
||||
private readonly IResourceQueryHelper<ChatSearchQuery, ChatSearchResult> _chatResourceQueryHelper;
|
||||
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatResourceQueryHelper;
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
|
||||
public StatsController(ILogger logger, IManager manager, IResourceQueryHelper<ChatSearchQuery, ChatSearchResult> resourceQueryHelper,
|
||||
public StatsController(ILogger logger, IManager manager, IResourceQueryHelper<ChatSearchQuery, MessageResponse> resourceQueryHelper,
|
||||
ITranslationLookup translationLookup) : base(manager)
|
||||
{
|
||||
_logger = logger;
|
||||
@ -36,6 +37,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"];
|
||||
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"];
|
||||
ViewBag.Servers = _manager.GetServers().Select(_server => new ServerInfo() { Name = _server.Hostname, ID = _server.EndPoint });
|
||||
ViewBag.Localization = _translationLookup;
|
||||
|
||||
return View("Index");
|
||||
}
|
||||
@ -71,51 +73,24 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMessageAsync(int serverId, long when)
|
||||
public async Task<IActionResult> GetMessageAsync(string serverId, long when)
|
||||
{
|
||||
var whenTime = DateTime.FromFileTimeUtc(when);
|
||||
var whenUpper = whenTime.AddMinutes(5);
|
||||
var whenLower = whenTime.AddMinutes(-5);
|
||||
|
||||
using (var ctx = new SharedLibraryCore.Database.DatabaseContext(true))
|
||||
var messages = await _chatResourceQueryHelper.QueryResource(new ChatSearchQuery()
|
||||
{
|
||||
var iqMessages = from message in ctx.Set<Stats.Models.EFClientMessage>()
|
||||
where message.ServerId == serverId
|
||||
where message.TimeSent >= whenLower
|
||||
where message.TimeSent <= whenUpper
|
||||
select new ChatInfo()
|
||||
{
|
||||
ClientId = message.ClientId,
|
||||
Message = message.Message,
|
||||
Name = message.Client.CurrentAlias.Name,
|
||||
Time = message.TimeSent,
|
||||
ServerGame = message.Server.GameName ?? Server.Game.IW4
|
||||
};
|
||||
ServerId = serverId,
|
||||
SentBefore = whenUpper,
|
||||
SentAfter = whenLower
|
||||
});
|
||||
|
||||
var messages = await iqMessages.ToListAsync();
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (message.Message.IsQuickMessage())
|
||||
{
|
||||
try
|
||||
{
|
||||
var quickMessages = _manager.GetApplicationSettings().Configuration()
|
||||
.QuickMessages
|
||||
.First(_qm => _qm.Game == message.ServerGame);
|
||||
message.Message = quickMessages.Messages[message.Message.Substring(1)];
|
||||
message.IsQuickMessage = true;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
return View("_MessageContext", messages);
|
||||
}
|
||||
return View("_MessageContext", messages.Results);
|
||||
}
|
||||
|
||||
[HttpGet("Message/Find")]
|
||||
public async Task<IActionResult> FindMessage([FromQuery]string query)
|
||||
public async Task<IActionResult> FindMessage([FromQuery] string query)
|
||||
{
|
||||
ViewBag.Localization = _translationLookup;
|
||||
ViewBag.EnableColorCodes = _manager.GetApplicationSettings().Configuration().EnableColorCodes;
|
||||
@ -150,7 +125,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("Message/FindNext")]
|
||||
public async Task<IActionResult> FindNextMessages([FromQuery]string query, [FromQuery]int count, [FromQuery]int offset)
|
||||
public async Task<IActionResult> FindNextMessages([FromQuery] string query, [FromQuery] int count, [FromQuery] int offset)
|
||||
{
|
||||
ChatSearchQuery searchRequest;
|
||||
|
||||
|
@ -1,32 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace StatsWeb.Dtos
|
||||
{
|
||||
public class ChatSearchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// name of the client
|
||||
/// </summary>
|
||||
public string ClientName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// client id
|
||||
/// </summary>
|
||||
public int ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// hostname of the server
|
||||
/// </summary>
|
||||
public string ServerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// chat message
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// date the chat occured on
|
||||
/// </summary>
|
||||
public DateTime Date { get; set; }
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
using SharedLibraryCore.Dtos;
|
||||
using StatsWeb.Dtos;
|
||||
using Stats.Dtos;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
<RunPostBuildEvent>Always</RunPostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.3" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<ul class="nav nav-tabs border-top border-bottom nav-fill row" role="tablist" id="stats_top_players">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active top-players-link" href="#server_0" role="tab" data-toggle="tab" aria-selected="true" data-serverid="0">All Servers</a>
|
||||
<a class="nav-link active top-players-link" href="#server_0" role="tab" data-toggle="tab" aria-selected="true" data-serverid="0">@ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"]</a>
|
||||
</li>
|
||||
|
||||
@foreach (var server in ViewBag.Servers)
|
||||
|
@ -1,4 +1,5 @@
|
||||
@model SharedLibraryCore.Helpers.ResourceQueryHelperResult<StatsWeb.Dtos.ChatSearchResult>
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@model SharedLibraryCore.Helpers.ResourceQueryHelperResult<MessageResponse>
|
||||
|
||||
@if (ViewBag.Error != null)
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
@model IEnumerable<StatsWeb.Dtos.ChatSearchResult>
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@model IEnumerable<MessageResponse>
|
||||
|
||||
@foreach (var message in Model)
|
||||
{
|
||||
@ -10,13 +11,20 @@
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-light w-50 text-break">
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
@if (message.IsHidden && !ViewBag.Authorized)
|
||||
{
|
||||
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
</td>
|
||||
<td class="text-light">
|
||||
<color-code value="@(message.ServerName ?? "--")" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</td>
|
||||
<td class="text-right text-light">
|
||||
@message.Date
|
||||
@message.When
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -33,7 +41,14 @@
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
|
||||
<td class="text-light">
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
@if (message.IsHidden && !ViewBag.Authorized)
|
||||
{
|
||||
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -47,7 +62,7 @@
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary" style="border-bottom: 1px solid #222">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
|
||||
<td class="text-light mb-2 border-bottom">
|
||||
@message.Date
|
||||
@message.When
|
||||
</td>
|
||||
</tr>
|
||||
}
|
@ -1,20 +1,21 @@
|
||||
@model IEnumerable<SharedLibraryCore.Dtos.ChatInfo>
|
||||
@using SharedLibraryCore.Dtos.Meta.Responses
|
||||
@model IEnumerable<MessageResponse>
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<div class="client-message-context">
|
||||
<h5 class="bg-primary pt-2 pb-2 pl-3 mb-0 mt-2 text-white">@Model.First().Time.ToString()</h5>
|
||||
<h5 class="bg-primary pt-2 pb-2 pl-3 mb-0 mt-2 text-white">@Model.First().When.ToString()</h5>
|
||||
<div class="bg-dark p-3 mb-2 border-bottom">
|
||||
@foreach (var message in Model)
|
||||
{
|
||||
<span class="text-white">
|
||||
<color-code value="@message.Name" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
<color-code value="@message.ClientName" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
</span>
|
||||
<span>
|
||||
—
|
||||
<span class="@(message.IsQuickMessage ? "font-italic" : "")">
|
||||
<color-code value="@message.Message" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
<color-code value="@(message.IsHidden ? message.HiddenMessage : message.Message)" allow="ViewBag.EnableColorCodes"></color-code>
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.3" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.9" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
437
README.md
437
README.md
@ -1,26 +1,25 @@
|
||||
|
||||
# IW4MAdmin
|
||||
### Quick Start Guide
|
||||
### Version 2.4
|
||||
_______
|
||||
### About
|
||||
**IW4MAdmin** is an administration tool for [IW4x](https://iw4xcachep26muba.onion.link/), [Pluto T6](https://forum.plutonium.pw/category/6/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/14/plutonium-iw5), [CoD4x](https://cod4x.me/), [TeknoMW3](https://www.teknomw3.pw/), and most Call of Duty® dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze.
|
||||
# IW4MAdmin [](https://github.com/RaidMax/IW4M-Admin/blob/2.4-pr/LICENSE) [](https://github.com/RaidMax/IW4M-Admin/stargazers)
|
||||
[](https://ko-fi.com/J3J821KUJ)
|
||||
|
||||
## About
|
||||
**IW4MAdmin** is an administration tool for [IW4x](https://iw4x.org/), [Pluto T6](https://forum.plutonium.pw/category/6/plutonium-t6), [Pluto IW5](https://forum.plutonium.pw/category/14/plutonium-iw5), [CoD4x](https://cod4x.me/), [TeknoMW3](https://github.com/Musta1337/TeknoMW3), and most Call of Duty® dedicated servers. It allows complete control of your server; from changing maps, to banning players, **IW4MAdmin** monitors and records activity on your server(s). With plugin support, extending its functionality is a breeze.
|
||||
### Download
|
||||
Latest binary builds are always available at:
|
||||
- [GitHub](https://github.com/RaidMax/IW4M-Admin/releases)
|
||||
- [RaidMax](https://raidmax.org/IW4MAdmin)
|
||||
|
||||
---
|
||||
### Setup
|
||||
|
||||
## Setup
|
||||
**IW4MAdmin** requires minimal effort to get up and running.
|
||||
#### Prerequisites
|
||||
### Prerequisites
|
||||
* [.NET Core 3.1.x Runtime](https://www.microsoft.com/net/download) *or newer*
|
||||
* [Direct Download (Windows)](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-aspnetcore-3.1.4-windows-hosting-bundle-installer)
|
||||
* [Package Installation (Linux)](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1910)
|
||||
#### Installation
|
||||
### Installation
|
||||
1. Install .NET Core Runtime
|
||||
2. Extract `IW4MAdmin-<version>.zip`
|
||||
#### Launching
|
||||
### Launching
|
||||
Windows
|
||||
1. Run `StartIW4MAdmin.cmd`
|
||||
2. Configure **IW4MAdmin**
|
||||
@ -41,421 +40,9 @@ Linux
|
||||
|
||||
_Your configuration and database will be saved_
|
||||
|
||||
---
|
||||
### Help
|
||||
## Help
|
||||
Feel free to join the **IW4MAdmin** [Discord](https://discord.gg/ZZFK5p3)
|
||||
If you come across an issue, bug, or feature request please post an [issue](https://github.com/RaidMax/IW4M-Admin/issues)
|
||||
___
|
||||
|
||||
### Configuration
|
||||
#### Initial Configuration
|
||||
When **IW4MAdmin** is launched for the _first time_, you will be prompted to setup your configuration.
|
||||
|
||||
`Enable webfront`
|
||||
* Enables you to monitor and control your server(s) through a web interface
|
||||
* Default — `http://0.0.0.0:1624`
|
||||
|
||||
`Enable multiple owners`
|
||||
* Enables more than one client to be promoted to level of `Owner`
|
||||
* Default — `false`
|
||||
|
||||
`Enable stepped privilege hierarchy`
|
||||
* Allows privileged clients to promote other clients to the level below their current level
|
||||
* Default — `false`
|
||||
|
||||
`Enable custom say name`
|
||||
* Shows a prefix to every message send by **IW4MAdmin** -- `[Admin] message`
|
||||
* _This feature requires you specify a custom say name_
|
||||
* _This feature only works on games that support the `sv_sayName` dvar_
|
||||
* Default — `false`
|
||||
|
||||
`Enable social link`
|
||||
* Shows a link to your community's social media/website on the webfront
|
||||
* Default — `false`
|
||||
|
||||
`Use Custom Encoding Parser`
|
||||
* Allows alternative encodings to be used for parsing game information and events
|
||||
* **Russian users should use this and then specify** `windows-1251` **as the encoding string**
|
||||
* Default — `false`
|
||||
|
||||
#### Server Configuration
|
||||
After initial configuration is finished, you will be prompted to configure your servers for **IW4MAdmin**.
|
||||
|
||||
`Enter server IP Address`
|
||||
* For almost all scenarios `127.0.0.1` is sufficient
|
||||
* Default — `n/a`
|
||||
|
||||
`Enter server port`
|
||||
* The port that your server is listening on (can be obtained via `net_port`)
|
||||
* Default — `n/a`
|
||||
|
||||
`Enter server RCon password`
|
||||
* The *\(R\)emote (Con)sole* password set in your server configuration (can be obtained via `rcon_password`)
|
||||
* Default — `n/a`
|
||||
|
||||
`Enter number of reserved slots`
|
||||
* The number of client slots reserved for privileged players (unavailable for regular users to occupy)
|
||||
* For example, if you enter **2** reserved slots on an **18** slot server, you will have **16** publicly available slots
|
||||
* Default — `0`
|
||||
|
||||
#### Advanced Configuration
|
||||
If you wish to further customize your experience of **IW4MAdmin**, the following configuration file(s) will allow you to changes core options using any text-editor.
|
||||
|
||||
#### `IW4MAdminSettings.json`-- this file is created after initial setup
|
||||
* This file uses the [JSON](https://en.wikipedia.org/wiki/JSON#JSON_sample) specification, so please validate your configuration before running **IW4MAdmin**
|
||||
|
||||
`WebfrontBindUrl`
|
||||
* Specifies the address and port the webfront will listen on.
|
||||
* The value can be an [IP Address](https://en.wikipedia.org/wiki/IP_address):port or [Domain Name](https://en.wikipedia.org/wiki/Domain_name):port
|
||||
* Example http://gameserver.com:8080
|
||||
* Default — `http://0.0.0.0:1624` (indicates that it will listen on all IP Addresses available to the default interface)
|
||||
|
||||
`CustomLocale`
|
||||
* Specifies a [locale name](https://msdn.microsoft.com/en-us/library/39cwe7zf.aspx) to use instead of system default
|
||||
* Locale must be from the `Equivalent Locale Name` column
|
||||
* Default — `windows-1252`
|
||||
|
||||
`ConnectionString`
|
||||
* Specifies the [connection string](https://www.connectionstrings.com/mysql/) to a MySQL server that is used instead of SQLite
|
||||
* Default — `null`
|
||||
|
||||
`DatabaseProvider`
|
||||
* Specifies the database provider **IW4MAdmin** should use
|
||||
* Possible values — `sqlite`, `mysql`, `postgresql`
|
||||
* Default — `sqlite`
|
||||
|
||||
`Ignore Bots`
|
||||
* Disables bots from being registered and tracked by **IW4MAdmin**
|
||||
|
||||
`RConPollRate`
|
||||
* Specifies (in milliseconds) how often to poll each server for updates
|
||||
* Default — `5000`
|
||||
|
||||
`Servers`
|
||||
* Specifies the list of servers **IW4MAdmin** will monitor
|
||||
* Default — `[]`
|
||||
* `IPAddress`
|
||||
* Specifies the IP Address of the particular server
|
||||
* Default — `n/a`
|
||||
* `Port`
|
||||
* Specifies the port of the particular server
|
||||
* Default — `n/a`
|
||||
* `Password`
|
||||
* Specifies the `rcon_password` of the particular server
|
||||
* Default — `n/a`
|
||||
* `ManualLogPath`
|
||||
* Specifies the log path to be used instead of the automatically generated one
|
||||
* To use the `GameLogServer`, this should be set to the http address that the `GameLogServer` is listening on
|
||||
* Example — http://gamelogserver.com/
|
||||
* Default — `null`
|
||||
* `AutoMessages`
|
||||
* Specifies the list of messages that are broadcasted to the particular server
|
||||
* Default — `[]`
|
||||
* `Rules`
|
||||
* Specifies the list of rules that apply to the particular server
|
||||
* Default — `[]`
|
||||
* `ReservedSlotNumber`
|
||||
* Specifies the number of client slots to reserve for privileged users
|
||||
* Default — `0`
|
||||
* `GameLogServerUrl`
|
||||
* Specifies the HTTP Url for the Game Log Server
|
||||
* Default — `null`
|
||||
|
||||
`AutoMessagePeriod`
|
||||
* Specifies (in seconds) how often messages should be broadcasted to each server
|
||||
* Default — `60`
|
||||
|
||||
`AutoMessages`
|
||||
* Specifies the list of messages that are broadcasted to **all** servers
|
||||
* Specially formatted **tokens** can be used to broadcast dynamic information
|
||||
* `{{TOTALPLAYERS}}` — displays how many players have connected
|
||||
* `{{TOPSTATS}}` — displays the top 5 players on the server based on performance
|
||||
* `{{MOSTPLAYED}}` — displays the top 5 players based on number of kills
|
||||
* `{{TOTALPLAYTIME}}` — displays the cumulative play time (in man-hours) on all monitored servers
|
||||
* `{{VERSION}}` — displays the version of **IW4MAdmin**
|
||||
* `{{ADMINS}}` — displays the currently connected and *unmasked* privileged users online
|
||||
* `{{NEXTMAP}}` — displays the next map and gametype in rotation
|
||||
|
||||
`GlobalRules`
|
||||
* Specifies the list of rules that apply to **all** servers`
|
||||
|
||||
`Maps`
|
||||
* Specifies the list of maps for each supported game
|
||||
* `Name`
|
||||
* Specifies the name of the map as returned by the game (usually the file name sans the file extension)
|
||||
* `Alias`
|
||||
* Specifies the display name of the map (as seen while loading in)
|
||||
___
|
||||
|
||||
### Commands
|
||||
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|
||||
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ---------------|
|
||||
|prune|pa|demote any trusted clients that have not connected recently (defaults to 30 days)|False|!pa \<optional inactive days\>|Owner|
|
||||
|quit|q|quit IW4MAdmin|False|!q |Owner|
|
||||
|rcon|rcon|send rcon command to server|False|!rcon \<commands\>|Owner|
|
||||
|ban|b|permanently ban a client from the server|True|!b \<player\> \<reason\>|SeniorAdmin|
|
||||
|unban|ub|unban client by client id|True|!ub \<client id\> \<reason\>|SeniorAdmin|
|
||||
|find|f|find client in database|False|!f \<player\>|Administrator|
|
||||
|killserver|kill|kill the game server|False|!kill |Administrator|
|
||||
|map|m|change to specified map|False|!m \<map\>|Administrator|
|
||||
|maprotate|mr|cycle to the next map in rotation|False|!mr |Administrator|
|
||||
|plugins|p|view all loaded plugins|False|!p |Administrator|
|
||||
|tempban|tb|temporarily ban a client for specified time (defaults to 1 hour)|True|!tb \<player\> \<duration (m\|h\|d\|w\|y)\> \<reason\>|Administrator|
|
||||
|alias|known|get past aliases and ips of a client|True|!known \<player\>|Moderator|
|
||||
|baninfo|bi|get information about a ban for a client|True|!bi \<player\>|Moderator|
|
||||
|fastrestart|fr|fast restart current map|False|!fr |Moderator|
|
||||
|flag|fp|flag a suspicious client and announce to admins on join|True|!fp \<player\> \<reason\>|Moderator|
|
||||
|kick|k|kick a client by name|True|!k \<player\> \<reason\>|Moderator|
|
||||
|list|l|list active clients|False|!l |Moderator|
|
||||
|mask|hide|hide your presence as a privileged client|False|!hide |Moderator|
|
||||
|reports|reps|get or clear recent reports|False|!reps \<optional clear\>|Moderator|
|
||||
|say|s|broadcast message to all clients|False|!s \<message\>|Moderator|
|
||||
|setlevel|sl|set client to specified privilege level|True|!sl \<player\> \<level\>|Moderator|
|
||||
|setpassword|sp|set your authentication password|False|!sp \<password\>|Moderator|
|
||||
|unflag|uf|Remove flag for client|True|!uf \<player\>|Moderator|
|
||||
|uptime|up|get current application running time|False|!up |Moderator|
|
||||
|usage|us|get application memory usage|False|!us |Moderator|
|
||||
|warn|w|warn client for infringing rules|True|!w \<player\> \<reason\>|Trusted|
|
||||
|warnclear|wc|remove all warnings for a client|True|!wc \<player\>|Trusted|
|
||||
|admins|a|list currently connected privileged clients|False|!a |User|
|
||||
|getexternalip|ip|view your external IP address|False|!ip |User|
|
||||
|help|h|list all available commands|False|!h \<optional commands\>|User|
|
||||
|nextmap|nm|view next map in rotation|False|!nm |User|
|
||||
|owner|iamgod|claim ownership of the server|False|!iamgod |User|
|
||||
|ping|pi|get client's latency|False|!pi \<optional player\>|User|
|
||||
|privatemessage|pm|send message to other client|True|!pm \<player\> \<message\>|User|
|
||||
|report|rep|report a client for suspicious behavior|True|!rep \<player\> \<reason\>|User|
|
||||
|rules|r|list server rules|False|!r |User|
|
||||
|setgravatar|sg|set gravatar for webfront profile|False|!sg \<gravatar email\>|User|
|
||||
|whoami|who|give information about yourself|False|!who |User|
|
||||
|
||||
|
||||
_These commands include all shipped plugin commands._
|
||||
|
||||
---
|
||||
|
||||
#### Player Identification
|
||||
All players are identified 5 separate ways
|
||||
1. `npID/GUID/XUID` - The ID corresponding to the player's hardware or forum account
|
||||
2. `IP` - The player's IP Address
|
||||
3. `Client ID` - The internal reference to a player, generated by **IW4MAdmin**
|
||||
4. `Name` - The visible player name as it appears in game
|
||||
5. `Client Number` - The slot the client occupies on a server. (The number ranges between 0 and the max number of clients allowed on the server)
|
||||
|
||||
For most commands players are identified by their `Name`
|
||||
However, if they are currently offline, or their name contains un-typable characters, their `Client ID` must be used
|
||||
|
||||
The `Client ID` is specified by prefixing a player's reference number with `@`.
|
||||
For example, `@123` would reference the player with a `Client ID` of 123.
|
||||
|
||||
**All commands that require a `target` look at the `first argument` for a form of player identification**
|
||||
|
||||
---
|
||||
|
||||
#### Additional Command Examples
|
||||
`setlevel`
|
||||
- _shortcut_ - `sl`
|
||||
- _Parameter 1_ - Player to modify level of
|
||||
- _Parameter 2_ - Level to set the player to ```[ User, Trusted, Moderator, Administrator, SeniorAdmin, Owner ]```
|
||||
- _Example_ - `!setlevel Player1 SeniorAdmin`, `!sl @123 Moderator`
|
||||
- **NOTE** - An `owner` cannot set another player's level to `owner` unless the configuration option is enabled during setup
|
||||
|
||||
`ban`
|
||||
- _Shortcut_ - `b`
|
||||
- _Parameter 1_ - Player to ban
|
||||
- _Parameter 2_ - Reason for ban
|
||||
- _Example_ - `!ban Player1 caught cheating`, `!b @123 GUID Spoofing`
|
||||
|
||||
`tempban`
|
||||
- _Shortcut_ - `tb`
|
||||
- _Parameter 1_ - Player to ban
|
||||
- _Parameter 2_ - Ban length (minutes|hours|days|weeks|years)
|
||||
- _Parameter 3_ - Reason for ban
|
||||
- _Example_ - `!tempban Player1 3w racism`, `!tb @123 8h Abusive behaivor`
|
||||
|
||||
`reports`
|
||||
- _Shortcut_ - `reps`
|
||||
- _Optional Parameter 1_ - `clear` (erases reports for current server)
|
||||
|
||||
___
|
||||
### Plugins
|
||||
|
||||
#### Welcome
|
||||
- This plugin uses geo-location data to welcome a player based on their country of origin
|
||||
- All privileged users ( Trusted or higher ) receive a specialized welcome message as well
|
||||
- Welcome messages can be customized in `WelcomePluginSettings.json`
|
||||
|
||||
#### Stats
|
||||
- This plugin calculates basic player performance, skill approximation, and kill/death ratio
|
||||
- Skill is an number derived from an algorithmic processing of a player's Kill Death Ratio (KDR) and Score per Minute (SPM).
|
||||
- Elo Rating is based off of the number of encounters a player wins.
|
||||
- Performance is the average of Skill + Elo Rating
|
||||
|
||||
**Commands added by this plugin**
|
||||
|
||||
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|
||||
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|
|
||||
|resetstats|rs|reset your stats to factory-new|False|!rs |User|
|
||||
|stats|xlrstats|view your stats|False|!xlrstats \<optional player\>|User|
|
||||
|topstats|ts|view the top 5 players on this server|False|!ts |User|
|
||||
|mostplayed|mp|view the top 5 dedicated players on the server|False|!mp |User|
|
||||
|
||||
- To qualify for top stats, a client must have played for at least `3 hours` and connected within the past `15 days`.
|
||||
|
||||
#### Login
|
||||
- This plugin deters GUID spoofing by requiring privileged users to login with their password before executing commands
|
||||
- A password must be set using the `setpassword` command before logging in
|
||||
|
||||
**Commands added by this plugin**
|
||||
|
||||
|Name |Alias|Description |Requires Target|Syntax |Required Level|
|
||||
|--------------| -----| --------------------------------------------------------| -----------------| -------------| ----------------|
|
||||
|login|l|login using password|False|!l \<password\>|Trusted|
|
||||
|
||||
#### Profanity Determent
|
||||
- This plugin warns and kicks players for using profanity
|
||||
- Profane words and warning message can be specified in `ProfanityDetermentSettings.json`
|
||||
- If a client's name contains a word listed in the settings, they will immediately be kicked
|
||||
|
||||
#### IW4 Script Commands
|
||||
- This plugin provides additional integration to IW4x
|
||||
- In order to take advantage of it, copy the `userraw` folder into your IW4x server directory
|
||||
|
||||
#### VPN Detection [Script Plugin]
|
||||
- This plugin detects if a client is using a VPN and kicks them if they are
|
||||
- To disable this plugin, delete `Plugins\VPNDetection.js`
|
||||
- Adding **Client IDs** to the `vpnExceptionIds` array will prevent a client from being kicked.
|
||||
|
||||
#### Shared GUID Kicker [Script Plugin]
|
||||
- This plugin kicks users using a specific GUID
|
||||
- GUID `F4D2C30B712AC6E3` on IW4x was packed into a torrent version of the game.
|
||||
___
|
||||
### Webfront
|
||||
`Home`
|
||||
* Shows an overview of the monitored server(s)
|
||||
|
||||
`Penalties`
|
||||
* Shows a chronological ordered list of client penalties (scrolling down loads older penalties)
|
||||
|
||||
`Admins`
|
||||
* Shows a list of privileged clients
|
||||
|
||||
`Login`
|
||||
* Allows privileged users to login using their `Client ID` and password set via `setpassword`
|
||||
* `ClientID` is a number that can be found by using `!find <client name>` or find the client on the webfront and copy the ID following `ProfileAsync/`
|
||||
|
||||
`Profile`
|
||||
* Shows a client's information and history
|
||||
|
||||
`Web Console`
|
||||
* Allows logged in privileged users to execute commands as if they are in-
|
||||
|
||||
`Search`
|
||||
* Query clients and messages
|
||||
|
||||
Advanced filters can be constructed to search for resources using the following filter table.
|
||||
| Filter | Description | Format | Example |
|
||||
|-----------|--------------------------------------------------------|-----------------------|---------------------|
|
||||
| before | include items occurring on or before the provided date | YYYY-MM-DD hh:mm:ss (UTC inferred) | 2020-05-21 23:00:00 |
|
||||
| after | include items occurring on or after the provided date | YYYY-MM-DD hh:mm:ss (UTC inferred) | 2015-01-01 |
|
||||
| server | include items matching the server id | ip:port | 127.0.0.1:28960 |
|
||||
| client | include items matching the client id | integer | 8947 |
|
||||
| contains | include items containing this substring | string | hack |
|
||||
| sort | display results in this order | ascending\|descending | descending |
|
||||
|
||||
Any number of filters can be combined in any order.
|
||||
Example — `chat|before 2020-05-21|after 2020-05-01|server 127.0.0.1:28960|client 444|contains cheating|sort descending`
|
||||
|
||||
---
|
||||
### Game Log Server
|
||||
The game log server provides a way to remotely host your server's log over a http rest-ful api.
|
||||
This feature is useful if you plan on running IW4MAdmin on a different machine than the game server.
|
||||
#### Requirements
|
||||
- [Python 3.8.x](https://www.python.org/downloads/) or newer
|
||||
|
||||
#### Installation
|
||||
1. With Python 3.x installed, open up a terminal/command prompt window in the `GameLogServer` folder and execute:
|
||||
```console
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
If this fails, you can alternatively try installing with:
|
||||
```console
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
2. Allow TCP port 1625 through firewall
|
||||
* [Windows Instructions](https://www.tomshardware.com/news/how-to-open-firewall-ports-in-windows-10,36451.html)
|
||||
* [Linux Instructions (iptables)](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-basic-iptables-firewall-on-centos-6#open-up-ports-for-selected-services)
|
||||
#### Launching
|
||||
With Python 3 installed, open a terminal/command prompt window open in the `GameServerLog` folder and execute:
|
||||
```console
|
||||
python runserver.py
|
||||
```
|
||||
The Game Log Server window will need to remain running/open as long as **IW4MAdmin** is running
|
||||
|
||||
#### Configuring
|
||||
* Update your `IW4MAdminSettings.json` by changing the value of `GameLogServerUrl` to "http://<remote_server_ip>:1625"
|
||||
* Example — `"GameLogServerUrl": "http://192.168.1.123:1625",`
|
||||
---
|
||||
### Extending Plugins
|
||||
|
||||
#### NuGet Package
|
||||
The NuGet package for **IW4MAdmin's** "Shared Library" can be obtained from the [NuGet Gallery](https://www.nuget.org/packages/RaidMax.IW4MAdmin.SharedLibraryCore)
|
||||
Referencing this package will give you the ability to write plugins against **IW4MAdmin's** core library.
|
||||
#### Code
|
||||
**IW4MAdmin's** functionality can be extended by writing additional plugins in C#.
|
||||
Each class library must implement the `IPlugin` interface.
|
||||
See the existing [plugins](https://github.com/RaidMax/IW4M-Admin/tree/master/Plugins) for examples.
|
||||
#### JavaScript
|
||||
**IW4MAdmin** functionality can also be extended using JavaScript.
|
||||
The JavaScript parser supports [ECMA 5.1](https://ecma-international.org/ecma-262/5.1/) standards.
|
||||
#### Plugin Object Template
|
||||
In order to be properly parsed by the JavaScript engine, every plugin must conform to the following template.
|
||||
```js
|
||||
var plugin = {
|
||||
author: 'YourHandle',
|
||||
version: 1.0,
|
||||
name: 'Sample JavaScript Plugin',
|
||||
|
||||
onEventAsync: function (gameEvent, server) {
|
||||
},
|
||||
|
||||
onLoadAsync: function (manager) {
|
||||
},
|
||||
|
||||
onUnloadAsync: function () {
|
||||
},
|
||||
|
||||
onTickAsync: function (server) {
|
||||
}
|
||||
};
|
||||
```
|
||||
#### Required Properties
|
||||
- `author` — [string] Author of the plugin (usually your name or online name/alias)
|
||||
- `version` — [float] Version number of your plugin (useful if you release several different versions)
|
||||
- `name` — [string] Name of your plugin (be descriptive!)
|
||||
- `onEventAsync` — [function] Handler executed when an event occurs
|
||||
- `gameEvent` — [parameter object] Object containing event type, origin, target, and other info (see the GameEvent class declaration)
|
||||
- `server` — [parameter object] Object containing information and methods about the server the event occured on (see the Server class declaration)
|
||||
- `onLoadAsync` — [function] Handler executed when the plugin is loaded by code
|
||||
- `manager` — [parameter object] Object reference to the application manager (see the IManager interface definition)
|
||||
- `onUnloadAsync` — [function] Handler executed when the plugin is unloaded by code (see live reloading)
|
||||
- `onTickAsync` — [function] Handler executed approximately once per second by code *(unimplemented as of version 2.\*)*
|
||||
- `server` — [parameter object] Object containing information and methods about the server the event occured on (see the Server class declaration)
|
||||
### Live Reloading
|
||||
Thanks to JavaScript's flexibility and parsability, the plugin importer scans the plugins folder and reloads the JavaScript plugins on demand as they're modified. This allows faster development/testing/debugging.
|
||||
|
||||
---
|
||||
### Misc
|
||||
#### Anti-cheat
|
||||
This is an [IW4x](https://iw4xcachep26muba.onion.link/) only feature (wider game support planned), that uses analytics to detect aimbots and aim-assist tools.
|
||||
To utilize anti-cheat, enable it during setup **and** copy `_customcallbacks.gsc` from `userraw` into your `IW4x Server\userraw\scripts` folder.
|
||||
|
||||
The anti-cheat feature is a work in progress and as such will be constantly tweaked and may not be 100% accurate, however the goal is to deter as many cheaters as possible from IW4x.
|
||||
#### Database Storage
|
||||
By default, all **IW4MAdmin** information is stored in `Database.db`.
|
||||
|
||||
Should you need to reset your database, this file can simply be deleted.
|
||||
Additionally, this file should be preserved during updates to retain client information.
|
||||
|
||||
Setting the `ConnectionString` and `DatabaseProvider` properties in `IW4MAdminSettings.json`
|
||||
will allow **IW4MAdmin** to use alternate methods for database storage
|
||||
#### Explore the [wiki](https://github.com/RaidMax/IW4M-Admin/wiki) to find more information.
|
||||
|
@ -108,7 +108,7 @@ namespace SharedLibraryCore
|
||||
}
|
||||
|
||||
// give the local host full access
|
||||
else
|
||||
else if (!HttpContext.Request.Headers.ContainsKey("X-Forwarded-For"))
|
||||
{
|
||||
Client.ClientId = 1;
|
||||
Client.Level = EFClient.Permission.Console;
|
||||
|
@ -5,6 +5,7 @@ using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace SharedLibraryCore
|
||||
{
|
||||
@ -13,7 +14,7 @@ namespace SharedLibraryCore
|
||||
/// </summary>
|
||||
public abstract class Command : IManagerCommand
|
||||
{
|
||||
private readonly CommandConfiguration _config;
|
||||
protected readonly CommandConfiguration _config;
|
||||
protected readonly ITranslationLookup _translationLookup;
|
||||
protected ILogger logger;
|
||||
|
||||
@ -113,6 +114,25 @@ namespace SharedLibraryCore
|
||||
}
|
||||
private EFClient.Permission permission;
|
||||
|
||||
public Game[] SupportedGames
|
||||
{
|
||||
get => supportedGames;
|
||||
protected set
|
||||
{
|
||||
try
|
||||
{
|
||||
var savedGames = _config?.Commands[GetType().Name].SupportedGames;
|
||||
supportedGames = savedGames?.Length != 0 ? savedGames : value;
|
||||
}
|
||||
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
supportedGames = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
private Game[] supportedGames;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Argument list for the command
|
||||
|
@ -53,7 +53,6 @@ namespace SharedLibraryCore.Commands
|
||||
|
||||
public override Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
MetaService.Clear();
|
||||
E.Owner.Manager.Restart();
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_RESTART_SUCCESS"]);
|
||||
return Task.CompletedTask;
|
||||
@ -292,7 +291,7 @@ namespace SharedLibraryCore.Commands
|
||||
switch ((await E.Target.TempBan(tempbanReason, length, E.Origin).WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason)
|
||||
{
|
||||
case GameEvent.EventFailReason.None:
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_SUCCESS"].FormatExt(E.Target, length.TimeSpanText()));
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_TEMPBAN_SUCCESS"].FormatExt(E.Target, length.HumanizeForCurrentCulture()));
|
||||
break;
|
||||
case GameEvent.EventFailReason.Exception:
|
||||
E.Origin.Tell(_translationLookup["SERVER_ERROR_COMMAND_INGAME"]);
|
||||
@ -773,8 +772,6 @@ namespace SharedLibraryCore.Commands
|
||||
/// </summary>
|
||||
public class ListAdminsCommand : Command
|
||||
{
|
||||
private readonly CommandConfiguration _config;
|
||||
|
||||
public ListAdminsCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
|
||||
{
|
||||
Name = "admins";
|
||||
@ -782,8 +779,6 @@ namespace SharedLibraryCore.Commands
|
||||
Alias = "a";
|
||||
Permission = Permission.User;
|
||||
RequiresTarget = false;
|
||||
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public static string OnlineAdmins(Server S, ITranslationLookup lookup)
|
||||
@ -890,14 +885,9 @@ namespace SharedLibraryCore.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var P in db_players)
|
||||
foreach (var client in db_players)
|
||||
{
|
||||
// they're not going by another alias
|
||||
// /*P.AliasLink.Children.FirstOrDefault(a => a.Name.ToLower().Contains(E.Data.ToLower()))?.Name*/
|
||||
string msg = P.Name.ToLower().Contains(E.Data.ToLower()) ?
|
||||
$"[^3{P.Name}^7] [^3@{P.ClientId}^7] - [{ Utilities.ConvertLevelToColor((Permission)P.LevelInt, P.Level)}^7] - {P.IPAddress} | last seen {Utilities.GetTimePassed(P.LastConnection)}" :
|
||||
$"()->[^3{P.Name}^7] [^3@{P.ClientId}^7] - [{ Utilities.ConvertLevelToColor((Permission)P.LevelInt, P.Level)}^7] - {P.IPAddress} | last seen {Utilities.GetTimePassed(P.LastConnection)}";
|
||||
E.Origin.Tell(msg);
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_FIND_FORMAT"].FormatExt(client.Name, client.ClientId, Utilities.ConvertLevelToColor((Permission)client.LevelInt, client.Level), client.IPAddress, client.LastConnectionText));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -907,8 +897,6 @@ namespace SharedLibraryCore.Commands
|
||||
/// </summary>
|
||||
public class ListRulesCommands : Command
|
||||
{
|
||||
private readonly CommandConfiguration _config;
|
||||
|
||||
public ListRulesCommands(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
|
||||
{
|
||||
Name = "rules";
|
||||
@ -916,8 +904,6 @@ namespace SharedLibraryCore.Commands
|
||||
Alias = "r";
|
||||
Permission = Permission.User;
|
||||
RequiresTarget = false;
|
||||
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public override Task ExecuteAsync(GameEvent E)
|
||||
@ -1259,7 +1245,7 @@ namespace SharedLibraryCore.Commands
|
||||
|
||||
else
|
||||
{
|
||||
string remainingTime = (penalty.Expires.Value - DateTime.UtcNow).TimeSpanText();
|
||||
string remainingTime = (penalty.Expires.Value - DateTime.UtcNow).HumanizeForCurrentCulture();
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_BANINFO_TB_SUCCESS"].FormatExt(E.Target.Name, penalty.Offense, remainingTime));
|
||||
}
|
||||
}
|
||||
@ -1543,7 +1529,9 @@ namespace SharedLibraryCore.Commands
|
||||
/// </summary>
|
||||
public class SetGravatarCommand : Command
|
||||
{
|
||||
public SetGravatarCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config, translationLookup)
|
||||
private readonly IMetaService _metaService;
|
||||
|
||||
public SetGravatarCommand(CommandConfiguration config, ITranslationLookup translationLookup, IMetaService metaService) : base(config, translationLookup)
|
||||
{
|
||||
Name = "setgravatar";
|
||||
Description = _translationLookup["COMMANDS_GRAVATAR_DESC"];
|
||||
@ -1558,17 +1546,17 @@ namespace SharedLibraryCore.Commands
|
||||
Required = true
|
||||
}
|
||||
};
|
||||
|
||||
_metaService = metaService;
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
var metaSvc = new MetaService();
|
||||
|
||||
using (var md5 = MD5.Create())
|
||||
{
|
||||
string gravatarEmail = string.Concat(md5.ComputeHash(E.Data.ToLower().Select(d => Convert.ToByte(d)).ToArray())
|
||||
.Select(h => h.ToString("x2")));
|
||||
await metaSvc.AddPersistentMeta("GravatarEmail", gravatarEmail, E.Origin);
|
||||
await _metaService.AddPersistentMeta("GravatarEmail", gravatarEmail, E.Origin);
|
||||
}
|
||||
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_GRAVATAR_SUCCESS_NEW"]);
|
||||
|
37
SharedLibraryCore/Commands/PrivateMessageAdminsCommand.cs
Normal file
37
SharedLibraryCore/Commands/PrivateMessageAdminsCommand.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace SharedLibraryCore.Commands
|
||||
{
|
||||
public class PrivateMessageAdminsCommand : Command
|
||||
{
|
||||
public PrivateMessageAdminsCommand(CommandConfiguration config, ITranslationLookup lookup) : base(config, lookup)
|
||||
{
|
||||
Name = "privatemessageadmin";
|
||||
Description = lookup["COMMANDS_PMADMINS_DESC"];
|
||||
Alias = "pma";
|
||||
Permission = EFClient.Permission.Moderator;
|
||||
SupportedGames = new[] { Game.IW4, Game.IW5 };
|
||||
}
|
||||
|
||||
public override Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
bool isGameSupported = _config.Commands[nameof(PrivateMessageAdminsCommand)].SupportedGames.Length > 0 &&
|
||||
_config.Commands[nameof(PrivateMessageAdminsCommand)].SupportedGames.Contains(E.Owner.GameName);
|
||||
|
||||
if (!isGameSupported)
|
||||
{
|
||||
E.Origin.Tell(_translationLookup["COMMANDS_GAME_NOT_SUPPORTED"].FormatExt(nameof(PrivateMessageAdminsCommand)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
E.Owner.ToAdmins(E.Data);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System.Text.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using static SharedLibraryCore.Database.Models.EFClient;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace SharedLibraryCore.Configuration
|
||||
{
|
||||
@ -29,5 +30,11 @@ namespace SharedLibraryCore.Configuration
|
||||
/// Indicates if the command can be run by another user (impersonation)
|
||||
/// </summary>
|
||||
public bool AllowImpersonation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the games supporting the functionality of the command
|
||||
/// </summary>
|
||||
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
|
||||
public Game[] SupportedGames { get; set; } = new Game[0];
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ namespace SharedLibraryCore.Database
|
||||
|
||||
static string _ConnectionString;
|
||||
static string _provider;
|
||||
private static readonly string _migrationPluginDirectory = @"X:\IW4MAdmin\BUILD\Plugins";
|
||||
private static readonly ILoggerFactory _loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.AddConsole()
|
||||
@ -72,7 +71,7 @@ namespace SharedLibraryCore.Database
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
//optionsBuilder.UseLoggerFactory(_loggerFactory)
|
||||
// .EnableSensitiveDataLogging();
|
||||
// .EnableSensitiveDataLogging();
|
||||
|
||||
if (string.IsNullOrEmpty(_ConnectionString))
|
||||
{
|
||||
@ -198,11 +197,14 @@ namespace SharedLibraryCore.Database
|
||||
|
||||
// adapted from
|
||||
// https://aleemkhan.wordpress.com/2013/02/28/dynamically-adding-dbset-properties-in-dbcontext-for-entity-framework-code-first/
|
||||
#if DEBUG
|
||||
string pluginDir = _migrationPluginDirectory;
|
||||
#else
|
||||
|
||||
string pluginDir = Path.Join(Utilities.OperatingDirectory, "Plugins");
|
||||
#endif
|
||||
|
||||
if (Utilities.IsDevelopment)
|
||||
{
|
||||
pluginDir = Path.Join(Utilities.OperatingDirectory, "..", "..", "..", "..", "BUILD", "Plugins");
|
||||
}
|
||||
|
||||
IEnumerable<string> directoryFiles = Directory.GetFiles(pluginDir).Where(f => f.EndsWith(".dll"));
|
||||
|
||||
foreach (string dllPath in directoryFiles)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
@ -11,5 +12,7 @@ namespace SharedLibraryCore.Dtos
|
||||
public string Name { get; set; }
|
||||
public Game ServerGame { get; set; }
|
||||
public bool IsQuickMessage { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public string HiddenMessage => string.Concat(Enumerable.Repeat('●', Message.Length));
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
namespace SharedLibraryCore.Dtos
|
||||
{
|
||||
public class FindClientRequest : PaginationInfo
|
||||
public class FindClientRequest : PaginationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// name of client
|
||||
|
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Requests
|
||||
{
|
||||
public class BaseClientMetaRequest : PaginationRequest
|
||||
{
|
||||
public int ClientId { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Requests
|
||||
{
|
||||
public class ReceivedPenaltyRequest : BaseClientMetaRequest
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Responses
|
||||
{
|
||||
public class AdministeredPenaltyResponse : ReceivedPenaltyResponse
|
||||
{
|
||||
}
|
||||
}
|
19
SharedLibraryCore/Dtos/Meta/Responses/BaseMetaResponse.cs
Normal file
19
SharedLibraryCore/Dtos/Meta/Responses/BaseMetaResponse.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Responses
|
||||
{
|
||||
public class BaseMetaResponse : IClientMeta, IClientMetaResponse
|
||||
{
|
||||
public int MetaId { get; set; }
|
||||
public int ClientId { get; set; }
|
||||
public MetaType Type { get; set; }
|
||||
public DateTime When { get; set; }
|
||||
public bool IsSensitive { get; set; }
|
||||
public bool ShouldDisplay { get; set; }
|
||||
public int? Column { get; set; }
|
||||
public int? Order { get; set; }
|
||||
}
|
||||
}
|
14
SharedLibraryCore/Dtos/Meta/Responses/InformationResponse.cs
Normal file
14
SharedLibraryCore/Dtos/Meta/Responses/InformationResponse.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Responses
|
||||
{
|
||||
public class InformationResponse : BaseMetaResponse
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string ToolTipText { get; set; }
|
||||
}
|
||||
}
|
33
SharedLibraryCore/Dtos/Meta/Responses/MessageResponse.cs
Normal file
33
SharedLibraryCore/Dtos/Meta/Responses/MessageResponse.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.Linq;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Responses
|
||||
{
|
||||
public class MessageResponse : BaseMetaResponse
|
||||
{
|
||||
public long ServerId { get; set; }
|
||||
public string Message { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// name of the client
|
||||
/// </summary>
|
||||
public string ClientName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// hostname of the server
|
||||
/// </summary>
|
||||
public string ServerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// specifies the game the chat occured on
|
||||
/// </summary>
|
||||
public Server.Game GameName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// indicates if the chat message is a quick message phrase
|
||||
/// </summary>
|
||||
public bool IsQuickMessage { get; set; }
|
||||
|
||||
public string HiddenMessage => string.Concat(Enumerable.Repeat('●', Message.Length));
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using static SharedLibraryCore.Database.Models.EFPenalty;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Responses
|
||||
{
|
||||
public class ReceivedPenaltyResponse : BaseMetaResponse
|
||||
{
|
||||
public int PenaltyId { get; set; }
|
||||
public int OffenderClientId { get; set; }
|
||||
public string OffenderName { get; set; }
|
||||
public string PunisherName { get; set; }
|
||||
public int PunisherClientId { get; set; }
|
||||
public PenaltyType PenaltyType { get; set; }
|
||||
public string Offense { get; set; }
|
||||
public string AutomatedOffense { get; set; }
|
||||
public DateTime? ExpirationDate { get; set; }
|
||||
public string ExpiresInText => ExpirationDate.HasValue && ExpirationDate.Value > DateTime.UtcNow ? (ExpirationDate - DateTime.UtcNow).Value.HumanizeForCurrentCulture() : "";
|
||||
public string LengthText => ExpirationDate.HasValue ? (ExpirationDate.Value.AddMinutes(1) - When).HumanizeForCurrentCulture() : "";
|
||||
public bool IsLinked { get; set; }
|
||||
public int LinkedClientId { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
namespace SharedLibraryCore.Dtos.Meta.Responses
|
||||
{
|
||||
public class UpdatedAliasResponse : BaseMetaResponse
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string IPAddress { get; set; } = "--";
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is UpdatedAliasResponse resp)
|
||||
{
|
||||
return resp.Name.StripColors() == Name.StripColors() && resp.IPAddress == IPAddress;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => $"{Name.StripColors()}{IPAddress}".GetStableHashCode();
|
||||
}
|
||||
}
|
13
SharedLibraryCore/Dtos/Meta/WebfrontTranslationHelper.cs
Normal file
13
SharedLibraryCore/Dtos/Meta/WebfrontTranslationHelper.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta
|
||||
{
|
||||
public class WebfrontTranslationHelper
|
||||
{
|
||||
public bool IsInterpolation { get; set; }
|
||||
public string MatchValue { get; set; }
|
||||
public string TranslationValue { get; set; }
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
namespace SharedLibraryCore.Dtos
|
||||
using System;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
{
|
||||
/// <summary>
|
||||
/// pagination information holder class
|
||||
/// </summary>
|
||||
public class PaginationInfo
|
||||
public class PaginationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// how many items to skip
|
||||
@ -24,6 +26,8 @@
|
||||
/// direction of ordering
|
||||
/// </summary>
|
||||
public SortDirection Direction { get; set; } = SortDirection.Descending;
|
||||
|
||||
public DateTime? Before { get; set; }
|
||||
}
|
||||
|
||||
public enum SortDirection
|
@ -21,8 +21,8 @@ namespace SharedLibraryCore.Dtos
|
||||
public PenaltyType PenaltyType { get; set; }
|
||||
public string PenaltyTypeText => PenaltyType.ToString();
|
||||
public DateTime TimePunished { get; set; }
|
||||
public string TimePunishedString => Utilities.GetTimePassed(TimePunished, true);
|
||||
public string TimeRemaining => DateTime.UtcNow > Expires ? "" : $"{((Expires ?? DateTime.MaxValue).Year == DateTime.MaxValue.Year ? Utilities.GetTimePassed(TimePunished, true) : Utilities.TimeSpanText((Expires ?? DateTime.MaxValue) - DateTime.UtcNow))}";
|
||||
public string TimePunishedString => TimePunished.HumanizeForCurrentCulture();
|
||||
public string TimeRemaining => DateTime.UtcNow > Expires ? "" : $"{((Expires ?? DateTime.MaxValue).Year == DateTime.MaxValue.Year ? TimePunishedString : ((Expires ?? DateTime.MaxValue) - DateTime.UtcNow).HumanizeForCurrentCulture())}";
|
||||
public bool Expired => Expires.HasValue && Expires <= DateTime.UtcNow;
|
||||
public DateTime? Expires { get; set; }
|
||||
public override bool Sensitive => PenaltyType == PenaltyType.Flag || PenaltyType == PenaltyType.Unflag;
|
||||
|
@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
{
|
||||
@ -19,11 +19,13 @@ namespace SharedLibraryCore.Dtos
|
||||
public bool HasActivePenalty { get; set; }
|
||||
public string ActivePenaltyType { get; set; }
|
||||
public bool Authenticated { get; set; }
|
||||
public List<ProfileMeta> Meta { get; set; }
|
||||
public List<InformationResponse> Meta { get; set; }
|
||||
public EFPenalty ActivePenalty { get; set; }
|
||||
public bool Online { get; set; }
|
||||
public string TimeOnline { get; set; }
|
||||
public DateTime LastConnection { get; set; }
|
||||
public string LastConnectionText => Utilities.GetTimePassed(LastConnection, true);
|
||||
public string LastConnectionText => (DateTime.UtcNow - LastConnection).HumanizeForCurrentCulture();
|
||||
public IDictionary<int, long> LinkedAccounts { get; set; }
|
||||
public MetaType? MetaFilterType { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
{
|
||||
public class ProfileMeta : SharedInfo
|
||||
{
|
||||
public enum MetaType
|
||||
{
|
||||
Other,
|
||||
Information,
|
||||
AliasUpdate,
|
||||
ChatMessage,
|
||||
Penalized,
|
||||
ReceivedPenalty,
|
||||
QuickMessage
|
||||
}
|
||||
|
||||
public DateTime When { get; set; }
|
||||
public string WhenString => Utilities.GetTimePassed(When, false);
|
||||
public string Key { get; set; }
|
||||
public dynamic Value { get; set; }
|
||||
public string Extra { get; set; }
|
||||
public virtual string Class => Value.GetType().ToString();
|
||||
public MetaType Type { get; set; }
|
||||
public int? Column { get; set; }
|
||||
public int? Order { get; set; }
|
||||
}
|
||||
}
|
@ -21,5 +21,6 @@ namespace SharedLibraryCore.Dtos
|
||||
public bool Online { get; set; }
|
||||
public string ConnectProtocolUrl { get; set; }
|
||||
public string IPAddress { get; set; }
|
||||
public bool IsPasswordProtected { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
|
||||
namespace SharedLibraryCore.Helpers
|
||||
{
|
||||
|
@ -14,6 +14,6 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// </summary>
|
||||
/// <param name="paginationInfo">pagination info</param>
|
||||
/// <returns></returns>
|
||||
Task<IList<AuditInfo>> ListAuditInformation(PaginationInfo paginationInfo);
|
||||
Task<IList<AuditInfo>> ListAuditInformation(PaginationRequest paginationInfo);
|
||||
}
|
||||
}
|
||||
|
31
SharedLibraryCore/Interfaces/IClientMeta.cs
Normal file
31
SharedLibraryCore/Interfaces/IClientMeta.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// describes all the base attributes of a client meta object
|
||||
/// </summary>
|
||||
public interface IClientMeta
|
||||
{
|
||||
MetaType Type { get; }
|
||||
DateTime When { get; }
|
||||
|
||||
bool IsSensitive { get; }
|
||||
bool ShouldDisplay { get; }
|
||||
|
||||
// sorting purposes
|
||||
public int? Column { get; set; }
|
||||
public int? Order { get; set; }
|
||||
}
|
||||
|
||||
public enum MetaType
|
||||
{
|
||||
Other,
|
||||
Information,
|
||||
AliasUpdate,
|
||||
ChatMessage,
|
||||
Penalized,
|
||||
ReceivedPenalty,
|
||||
QuickMessage
|
||||
}
|
||||
}
|
12
SharedLibraryCore/Interfaces/IClientMetaResponse.cs
Normal file
12
SharedLibraryCore/Interfaces/IClientMetaResponse.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces
|
||||
{
|
||||
public interface IClientMetaResponse
|
||||
{
|
||||
int ClientId { get;}
|
||||
int MetaId { get; }
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using System.Threading.Tasks;
|
||||
using static SharedLibraryCore.Database.Models.EFClient;
|
||||
using static SharedLibraryCore.Server;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces
|
||||
{
|
||||
@ -35,6 +36,11 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// </summary>
|
||||
Permission Permission { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Games the command is supported on
|
||||
/// </summary>
|
||||
Game[] SupportedGames { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Syntax for using the command
|
||||
/// </summary>
|
||||
|
11
SharedLibraryCore/Interfaces/IMetaRegistration.cs
Normal file
11
SharedLibraryCore/Interfaces/IMetaRegistration.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces
|
||||
{
|
||||
public interface IMetaRegistration
|
||||
{
|
||||
void Register();
|
||||
}
|
||||
}
|
51
SharedLibraryCore/Interfaces/IMetaService.cs
Normal file
51
SharedLibraryCore/Interfaces/IMetaService.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces
|
||||
{
|
||||
public interface IMetaService
|
||||
{
|
||||
/// <summary>
|
||||
/// adds or updates meta key and value to the database
|
||||
/// </summary>
|
||||
/// <param name="metaKey">key of meta data</param>
|
||||
/// <param name="metaValue">value of the meta data</param>
|
||||
/// <param name="client">client to save the meta for</param>
|
||||
/// <returns></returns>
|
||||
Task AddPersistentMeta(string metaKey, string metaValue, EFClient client);
|
||||
|
||||
/// <summary>
|
||||
/// retrieves meta data for given client and key
|
||||
/// </summary>
|
||||
/// <param name="metaKey">key to retrieve value for</param>
|
||||
/// <param name="client">client to retrieve meta for</param>
|
||||
/// <returns></returns>
|
||||
Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client);
|
||||
|
||||
/// <summary>
|
||||
/// adds a meta task to the runtime meta list
|
||||
/// </summary>
|
||||
/// <param name="metaKey">type of meta</param>
|
||||
/// <param name="metaAction">action to perform</param>
|
||||
void AddRuntimeMeta<T,V>(MetaType metaKey, Func<T, Task<IEnumerable<V>>> metaAction) where V : IClientMeta where T: PaginationRequest;
|
||||
|
||||
/// <summary>
|
||||
/// retrieves all the runtime meta information for given client idea
|
||||
/// </summary>
|
||||
/// <param name="request">request information</param>
|
||||
/// <returns></returns>
|
||||
Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// retreives all the runtime of provided type
|
||||
/// </summary>
|
||||
/// <param name="request">>request information</param>
|
||||
/// <param name="metaType">type of meta to retreive</param>
|
||||
/// <returns></returns>
|
||||
Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType) where T : IClientMeta;
|
||||
}
|
||||
}
|
@ -15,9 +15,10 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// <param name="alias">alias of command</param>
|
||||
/// <param name="description">description of command</param>
|
||||
/// <param name="permission">minimum required permission</param>
|
||||
/// <param name="isTargetRequired">target required or not</param>
|
||||
/// <param name="args">command arguments (name, is required)</param>
|
||||
/// <param name="executeAction">action to peform when commmand is executed</param>
|
||||
/// <returns></returns>
|
||||
IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction);
|
||||
IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission, bool isTargetRequired, IEnumerable<(string, bool)> args, Action<GameEvent> executeAction);
|
||||
}
|
||||
}
|
||||
|
25
SharedLibraryCore/Interfaces/IScriptPluginServiceResolver.cs
Normal file
25
SharedLibraryCore/Interfaces/IScriptPluginServiceResolver.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// interface used to dynamically resolve services by string name
|
||||
/// </summary>
|
||||
public interface IScriptPluginServiceResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// resolves a service with the given name
|
||||
/// </summary>
|
||||
/// <param name="serviceName">class name of service</param>
|
||||
/// <returns></returns>
|
||||
object ResolveService(string serviceName);
|
||||
|
||||
/// <summary>
|
||||
/// resolves a service with the given name and generic params
|
||||
/// </summary>
|
||||
/// <param name="serviceName">class name of service</param>
|
||||
/// <param name="genericParams">generic class names</param>
|
||||
/// <returns></returns>
|
||||
object ResolveService(string serviceName, string[] genericParameters);
|
||||
}
|
||||
}
|
@ -1,12 +1,23 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace SharedLibraryCore.Localization
|
||||
{
|
||||
public class Layout
|
||||
{
|
||||
public string LocalizationName { get; set; }
|
||||
private string localizationName;
|
||||
public string LocalizationName
|
||||
{
|
||||
get => localizationName;
|
||||
set
|
||||
{
|
||||
localizationName = value;
|
||||
Culture = new CultureInfo(value);
|
||||
}
|
||||
}
|
||||
public TranslationLookup LocalizationIndex { get; set; }
|
||||
public CultureInfo Culture { get; private set; }
|
||||
|
||||
public Layout(Dictionary<string, string> set)
|
||||
{
|
||||
|
925
SharedLibraryCore/Migrations/20200819224119_AddIsPasswordProtectedColumn.Designer.cs
generated
Normal file
925
SharedLibraryCore/Migrations/20200819224119_AddIsPasswordProtectedColumn.Designer.cs
generated
Normal file
@ -0,0 +1,925 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using SharedLibraryCore.Database;
|
||||
|
||||
namespace SharedLibraryCore.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20200819224119_AddIsPasswordProtectedColumn")]
|
||||
partial class AddIsPasswordProtectedColumn
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.7");
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b =>
|
||||
{
|
||||
b.Property<int>("SnapshotId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CurrentSessionLength")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("CurrentStrain")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("CurrentViewAngleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Deaths")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("Distance")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("EloRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("HitDestinationId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("HitLocation")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("HitOriginId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("HitType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Hits")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Kills")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LastStrainAngleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("RecoilOffset")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("SessionAngleOffset")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("SessionAverageSnapValue")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("SessionSPM")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("SessionScore")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SessionSnapHits")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("StrainAngleBetween")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("TimeSinceLastEvent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("WeaponId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("When")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SnapshotId");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("CurrentViewAngleId");
|
||||
|
||||
b.HasIndex("HitDestinationId");
|
||||
|
||||
b.HasIndex("HitOriginId");
|
||||
|
||||
b.HasIndex("LastStrainAngleId");
|
||||
|
||||
b.ToTable("EFACSnapshot");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshotVector3", b =>
|
||||
{
|
||||
b.Property<int>("ACSnapshotVector3Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SnapshotId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Vector3Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ACSnapshotVector3Id");
|
||||
|
||||
b.HasIndex("SnapshotId");
|
||||
|
||||
b.HasIndex("Vector3Id");
|
||||
|
||||
b.ToTable("EFACSnapshotVector3");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
|
||||
{
|
||||
b.Property<long>("KillId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AttackerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Damage")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("DeathOriginVector3Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DeathType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("Fraction")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("HitLoc")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsKill")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("KillOriginVector3Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Map")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("ServerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VictimId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ViewAnglesVector3Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("VisibilityPercentage")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("Weapon")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("When")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("KillId");
|
||||
|
||||
b.HasIndex("AttackerId");
|
||||
|
||||
b.HasIndex("DeathOriginVector3Id");
|
||||
|
||||
b.HasIndex("KillOriginVector3Id");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.HasIndex("VictimId");
|
||||
|
||||
b.HasIndex("ViewAnglesVector3Id");
|
||||
|
||||
b.ToTable("EFClientKills");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
|
||||
{
|
||||
b.Property<long>("MessageId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("ServerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("TimeSent")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("MessageId");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.HasIndex("TimeSent");
|
||||
|
||||
b.ToTable("EFClientMessages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b =>
|
||||
{
|
||||
b.Property<int>("RatingHistoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("RatingHistoryId");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.ToTable("EFClientRatingHistory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
|
||||
{
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("ServerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("AverageRecoilOffset")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("AverageSnapValue")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("Deaths")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("EloRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("Kills")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("MaxStrain")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("RollingWeightedKDR")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("SPM")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("Skill")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("SnapHitCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TimePlayed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("VisionAverage")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("ClientId", "ServerId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.ToTable("EFClientStatistics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
|
||||
{
|
||||
b.Property<int>("HitLocationCountId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("EFClientStatisticsClientId")
|
||||
.HasColumnName("EFClientStatisticsClientId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("EFClientStatisticsServerId")
|
||||
.HasColumnName("EFClientStatisticsServerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("HitCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("HitOffsetAverage")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("Location")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("MaxAngleDistance")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("HitLocationCountId");
|
||||
|
||||
b.HasIndex("EFClientStatisticsServerId");
|
||||
|
||||
b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId");
|
||||
|
||||
b.ToTable("EFHitLocationCounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b =>
|
||||
{
|
||||
b.Property<int>("RatingId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ActivityAmount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Newest")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("Performance")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("Ranking")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RatingHistoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long?>("ServerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("When")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("RatingId");
|
||||
|
||||
b.HasIndex("RatingHistoryId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.HasIndex("Performance", "Ranking", "When");
|
||||
|
||||
b.ToTable("EFRating");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b =>
|
||||
{
|
||||
b.Property<long>("ServerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("EndPoint")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("GameName")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("HostName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsPasswordProtected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Port")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ServerId");
|
||||
|
||||
b.ToTable("EFServers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
|
||||
{
|
||||
b.Property<int>("StatisticId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("ServerId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TotalKills")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("TotalPlayTime")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("StatisticId");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
||||
b.ToTable("EFServerStatistics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
|
||||
{
|
||||
b.Property<int>("AliasId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("IPAddress")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LinkId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(24);
|
||||
|
||||
b.Property<string>("SearchableName")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(24);
|
||||
|
||||
b.HasKey("AliasId");
|
||||
|
||||
b.HasIndex("IPAddress");
|
||||
|
||||
b.HasIndex("LinkId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.HasIndex("SearchableName");
|
||||
|
||||
b.HasIndex("Name", "IPAddress")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("EFAlias");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b =>
|
||||
{
|
||||
b.Property<int>("AliasLinkId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("AliasLinkId");
|
||||
|
||||
b.ToTable("EFAliasLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b =>
|
||||
{
|
||||
b.Property<int>("ChangeHistoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(128);
|
||||
|
||||
b.Property<string>("CurrentValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ImpersonationEntityId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("OriginEntityId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PreviousValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TargetEntityId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("TimeChanged")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TypeOfChange")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ChangeHistoryId");
|
||||
|
||||
b.ToTable("EFChangeHistory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
|
||||
{
|
||||
b.Property<int>("ClientId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AliasLinkId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Connections")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CurrentAliasId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("FirstConnection")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastConnection")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Level")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Masked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("NetworkId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordSalt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TotalConnectionTime")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ClientId");
|
||||
|
||||
b.HasIndex("AliasLinkId");
|
||||
|
||||
b.HasIndex("CurrentAliasId");
|
||||
|
||||
b.HasIndex("NetworkId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("EFClients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b =>
|
||||
{
|
||||
b.Property<int>("MetaId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ClientId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Extra")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasMaxLength(32);
|
||||
|
||||
b.Property<DateTime>("Updated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("MetaId");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("Key");
|
||||
|
||||
b.ToTable("EFMeta");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
|
||||
{
|
||||
b.Property<int>("PenaltyId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AutomatedOffense")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expires")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsEvadedOffense")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LinkId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("OffenderId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Offense")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PunisherId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("When")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("PenaltyId");
|
||||
|
||||
b.HasIndex("LinkId");
|
||||
|
||||
b.HasIndex("OffenderId");
|
||||
|
||||
b.HasIndex("PunisherId");
|
||||
|
||||
b.ToTable("EFPenalties");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b =>
|
||||
{
|
||||
b.Property<int>("Vector3Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("X")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("Y")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("Z")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("Vector3Id");
|
||||
|
||||
b.ToTable("Vector3");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "CurrentViewAngle")
|
||||
.WithMany()
|
||||
.HasForeignKey("CurrentViewAngleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitDestination")
|
||||
.WithMany()
|
||||
.HasForeignKey("HitDestinationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitOrigin")
|
||||
.WithMany()
|
||||
.HasForeignKey("HitOriginId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "LastStrainAngle")
|
||||
.WithMany()
|
||||
.HasForeignKey("LastStrainAngleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshotVector3", b =>
|
||||
{
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", "Snapshot")
|
||||
.WithMany("PredictedViewAngles")
|
||||
.HasForeignKey("SnapshotId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "Vector")
|
||||
.WithMany()
|
||||
.HasForeignKey("Vector3Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker")
|
||||
.WithMany()
|
||||
.HasForeignKey("AttackerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeathOriginVector3Id");
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin")
|
||||
.WithMany()
|
||||
.HasForeignKey("KillOriginVector3Id");
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim")
|
||||
.WithMany()
|
||||
.HasForeignKey("VictimId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles")
|
||||
.WithMany()
|
||||
.HasForeignKey("ViewAnglesVector3Id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany()
|
||||
.HasForeignKey("EFClientStatisticsClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("EFClientStatisticsServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", null)
|
||||
.WithMany("HitLocations")
|
||||
.HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b =>
|
||||
{
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", "RatingHistory")
|
||||
.WithMany("Ratings")
|
||||
.HasForeignKey("RatingHistoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
|
||||
{
|
||||
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
|
||||
.WithMany()
|
||||
.HasForeignKey("ServerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("LinkId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("AliasLinkId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias")
|
||||
.WithMany()
|
||||
.HasForeignKey("CurrentAliasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
|
||||
.WithMany("Meta")
|
||||
.HasForeignKey("ClientId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
|
||||
{
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
|
||||
.WithMany("ReceivedPenalties")
|
||||
.HasForeignKey("LinkId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender")
|
||||
.WithMany("ReceivedPenalties")
|
||||
.HasForeignKey("OffenderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher")
|
||||
.WithMany("AdministeredPenalties")
|
||||
.HasForeignKey("PunisherId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace SharedLibraryCore.Migrations
|
||||
{
|
||||
public partial class AddIsPasswordProtectedColumn : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.ActiveProvider == "Npgsql.EntityFrameworkCore.PostgreSQL")
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsPasswordProtected",
|
||||
type: "bool",
|
||||
table: "EFServers",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsPasswordProtected",
|
||||
table: "EFServers",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsPasswordProtected",
|
||||
table: "EFServers");
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ namespace SharedLibraryCore.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.3");
|
||||
.HasAnnotation("ProductVersion", "3.1.7");
|
||||
|
||||
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b =>
|
||||
{
|
||||
@ -408,6 +408,9 @@ namespace SharedLibraryCore.Migrations
|
||||
b.Property<string>("HostName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsPasswordProtected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Port")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -618,7 +618,7 @@ namespace SharedLibraryCore.Database.Models
|
||||
if (tempbanPenalty != null)
|
||||
{
|
||||
CurrentServer.Logger.WriteDebug($"Kicking {this} because their GUID is temporarily banned");
|
||||
Kick($"{loc["SERVER_TB_REMAIN"]} ({(tempbanPenalty.Expires.Value - DateTime.UtcNow).TimeSpanText()} {loc["WEBFRONT_PENALTY_TEMPLATE_REMAINING"]})", autoKickClient);
|
||||
Kick($"{loc["SERVER_TB_REMAIN"]} ({(tempbanPenalty.Expires.Value - DateTime.UtcNow).HumanizeForCurrentCulture()} {loc["WEBFRONT_PENALTY_TEMPLATE_REMAINING"]})", autoKickClient);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -656,6 +656,12 @@ namespace SharedLibraryCore.Database.Models
|
||||
public int Score { get; set; }
|
||||
[NotMapped]
|
||||
public bool IsBot => NetworkId == Name.GenerateGuidFromString();
|
||||
[NotMapped]
|
||||
public bool IsZombieClient => IsBot && Name == "Zombie";
|
||||
[NotMapped]
|
||||
public string XuidString => (NetworkId + 0x110000100000000).ToString("x");
|
||||
[NotMapped]
|
||||
public string GuidString => NetworkId.ToString("x");
|
||||
|
||||
[NotMapped]
|
||||
public ClientState State { get; set; }
|
||||
|
12
SharedLibraryCore/QueryHelper/ClientPaginationRequest.cs
Normal file
12
SharedLibraryCore/QueryHelper/ClientPaginationRequest.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using SharedLibraryCore.Dtos;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedLibraryCore.QueryHelper
|
||||
{
|
||||
public class ClientPaginationRequest : PaginationRequest
|
||||
{
|
||||
public int ClientId { get; set; }
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ namespace SharedLibraryCore.Repositories
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IList<AuditInfo>> ListAuditInformation(PaginationInfo paginationInfo)
|
||||
public async Task<IList<AuditInfo>> ListAuditInformation(PaginationRequest paginationInfo)
|
||||
{
|
||||
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
|
||||
{
|
||||
|
@ -293,6 +293,7 @@ namespace SharedLibraryCore
|
||||
public string Hostname { get => ServerConfig.CustomHostname ?? hostname; protected set => hostname = value; }
|
||||
public string Website { get; protected set; }
|
||||
public string Gametype { get; set; }
|
||||
public string GamePassword { get; protected set; }
|
||||
public Map CurrentMap { get; set; }
|
||||
public int ClientNum
|
||||
{
|
||||
|
@ -1,169 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore.Database;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharedLibraryCore.Services
|
||||
{
|
||||
public class MetaService
|
||||
{
|
||||
private static List<Func<int, int, int, DateTime?, Task<List<ProfileMeta>>>> _metaActions = new List<Func<int, int, int, DateTime?, Task<List<ProfileMeta>>>>();
|
||||
|
||||
/// <summary>
|
||||
/// adds or updates meta key and value to the database
|
||||
/// </summary>
|
||||
/// <param name="metaKey">key of meta data</param>
|
||||
/// <param name="metaValue">value of the meta data</param>
|
||||
/// <param name="client">client to save the meta for</param>
|
||||
/// <returns></returns>
|
||||
public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client)
|
||||
{
|
||||
// this seems to happen if the client disconnects before they've had time to authenticate and be added
|
||||
if (client.ClientId < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (var ctx = new DatabaseContext())
|
||||
{
|
||||
var existingMeta = await ctx.EFMeta
|
||||
.Where(_meta => _meta.Key == metaKey)
|
||||
.Where(_meta => _meta.ClientId == client.ClientId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existingMeta != null)
|
||||
{
|
||||
existingMeta.Value = metaValue;
|
||||
existingMeta.Updated = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
ctx.EFMeta.Add(new EFMeta()
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Created = DateTime.UtcNow,
|
||||
Key = metaKey,
|
||||
Value = metaValue
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Clear()
|
||||
{
|
||||
_metaActions.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// retrieves meta data for given client and key
|
||||
/// </summary>
|
||||
/// <param name="metaKey">key to retrieve value for</param>
|
||||
/// <param name="client">client to retrieve meta for</param>
|
||||
/// <returns></returns>
|
||||
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
|
||||
{
|
||||
using (var ctx = new DatabaseContext(disableTracking: true))
|
||||
{
|
||||
return await ctx.EFMeta
|
||||
.Where(_meta => _meta.Key == metaKey)
|
||||
.Where(_meta => _meta.ClientId == client.ClientId)
|
||||
.Select(_meta => new EFMeta()
|
||||
{
|
||||
MetaId = _meta.MetaId,
|
||||
Key = _meta.Key,
|
||||
ClientId = _meta.ClientId,
|
||||
Value = _meta.Value
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// aads a meta task to the runtime meta list
|
||||
/// </summary>
|
||||
/// <param name="metaAction"></param>
|
||||
public static void AddRuntimeMeta(Func<int, int, int, DateTime?, Task<List<ProfileMeta>>> metaAction)
|
||||
{
|
||||
_metaActions.Add(metaAction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// retrieves all the runtime meta information for given client idea
|
||||
/// </summary>
|
||||
/// <param name="clientId">id of the client</param>
|
||||
/// <param name="count">number of meta items to retrieve</param>
|
||||
/// <param name="offset">offset from the first item</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<List<ProfileMeta>> GetRuntimeMeta(int clientId, int offset = 0, int count = int.MaxValue, DateTime? startAt = null)
|
||||
{
|
||||
var meta = new List<ProfileMeta>();
|
||||
|
||||
foreach (var action in _metaActions)
|
||||
{
|
||||
var metaItems = await action(clientId, offset, count, startAt);
|
||||
meta.AddRange(metaItems);
|
||||
}
|
||||
|
||||
if (count == 1)
|
||||
{
|
||||
var table = new List<List<ProfileMeta>>();
|
||||
var metaWithColumn = meta
|
||||
.Where(_meta => _meta.Column != null);
|
||||
|
||||
var columnGrouping = metaWithColumn
|
||||
.GroupBy(_meta => _meta.Column);
|
||||
|
||||
var metaToSort = meta.Except(metaWithColumn).ToList();
|
||||
|
||||
foreach (var metaItem in columnGrouping)
|
||||
{
|
||||
table.Add(new List<ProfileMeta>(metaItem));
|
||||
}
|
||||
|
||||
while (metaToSort.Count > 0)
|
||||
{
|
||||
var sortingMeta = metaToSort.First();
|
||||
|
||||
int indexOfSmallestColumn()
|
||||
{
|
||||
int index = 0;
|
||||
int smallestColumnSize = int.MaxValue;
|
||||
for (int i = 0; i < table.Count; i++)
|
||||
{
|
||||
if (table[i].Count < smallestColumnSize)
|
||||
{
|
||||
smallestColumnSize = table[i].Count;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
int columnIndex = indexOfSmallestColumn();
|
||||
|
||||
sortingMeta.Column = columnIndex;
|
||||
sortingMeta.Order = columnGrouping
|
||||
.First(_group => _group.Key == columnIndex)
|
||||
.Count();
|
||||
|
||||
table[columnIndex].Add(sortingMeta);
|
||||
|
||||
metaToSort.Remove(sortingMeta);
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
return meta.OrderByDescending(_meta => _meta.When)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
@ -6,7 +6,7 @@
|
||||
<ApplicationIcon />
|
||||
<StartupObject />
|
||||
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
|
||||
<Version>2.4.3</Version>
|
||||
<Version>2.4.10</Version>
|
||||
<Authors>RaidMax</Authors>
|
||||
<Company>Forever None</Company>
|
||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||
@ -18,10 +18,11 @@
|
||||
<Copyright>2020</Copyright>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Description>Shared Library for IW4MAdmin</Description>
|
||||
<AssemblyVersion>2.4.3.0</AssemblyVersion>
|
||||
<FileVersion>2.4.3.0</FileVersion>
|
||||
<AssemblyVersion>2.4.10.0</AssemblyVersion>
|
||||
<FileVersion>2.4.10.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
|
||||
@ -30,32 +31,36 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="8.6.2" />
|
||||
<PackageReference Include="FluentValidation" Version="9.1.3" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.8.26" />
|
||||
<PackageReference Include="Humanizer.Core.ru" Version="2.8.26" />
|
||||
<PackageReference Include="Humanizer.Core.de" Version="2.8.26" />
|
||||
<PackageReference Include="Humanizer.Core.es" Version="2.8.26" />
|
||||
<PackageReference Include="Humanizer.Core.pt" Version="2.8.26" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.7" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="Npgsql" Version="4.1.3.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.3" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
|
||||
<PackageReference Include="Npgsql" Version="4.1.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.2" />
|
||||
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.7" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||
<Exec Command="if not exist "$(ProjectDir)..\BUILD" (
if $(ConfigurationName) == Debug (
md "$(ProjectDir)..\BUILD"
)
)
if not exist "$(ProjectDir)..\BUILD\Plugins" (
if $(ConfigurationName) == Debug (
md "$(ProjectDir)..\BUILD\Plugins"
)
)" />
|
||||
</Target>
|
||||
|
@ -1,5 +1,8 @@
|
||||
|
||||
using Humanizer;
|
||||
using Humanizer.Localisation;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos.Meta;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
@ -366,7 +369,27 @@ namespace SharedLibraryCore
|
||||
/// </summary>
|
||||
/// <param name="value">value string</param>
|
||||
/// <returns></returns>
|
||||
public static long GenerateGuidFromString(this string value) => string.IsNullOrEmpty(value) ? -1 : HashCode.Combine(value.StripColors());
|
||||
public static long GenerateGuidFromString(this string value) => string.IsNullOrEmpty(value) ? -1 : GetStableHashCode(value.StripColors());
|
||||
|
||||
/// https://stackoverflow.com/questions/36845430/persistent-hashcode-for-strings
|
||||
public static int GetStableHashCode(this string str)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash1 = 5381;
|
||||
int hash2 = hash1;
|
||||
|
||||
for (int i = 0; i < str.Length && str[i] != '\0'; i += 2)
|
||||
{
|
||||
hash1 = ((hash1 << 5) + hash1) ^ str[i];
|
||||
if (i == str.Length - 1 || str[i + 1] == '\0')
|
||||
break;
|
||||
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
|
||||
}
|
||||
|
||||
return hash1 + (hash2 * 1566083941);
|
||||
}
|
||||
}
|
||||
|
||||
public static int? ConvertToIP(this string str)
|
||||
{
|
||||
@ -381,57 +404,6 @@ namespace SharedLibraryCore
|
||||
return !ip.HasValue ? "" : new IPAddress(BitConverter.GetBytes(ip.Value)).ToString();
|
||||
}
|
||||
|
||||
public static string GetTimePassed(DateTime start)
|
||||
{
|
||||
return GetTimePassed(start, true);
|
||||
}
|
||||
|
||||
public static string GetTimePassed(DateTime start, bool includeAgo)
|
||||
{
|
||||
TimeSpan Elapsed = DateTime.UtcNow - start;
|
||||
string ago = includeAgo ? $" {CurrentLocalization.LocalizationIndex["WEBFRONT_PENALTY_TEMPLATE_AGO"]}" : "";
|
||||
|
||||
if (Elapsed.TotalSeconds < 30)
|
||||
{
|
||||
return CurrentLocalization.LocalizationIndex["GLOBAL_TIME_JUSTNOW"] + ago;
|
||||
}
|
||||
if (Elapsed.TotalMinutes < 120)
|
||||
{
|
||||
if (Elapsed.TotalMinutes < 1.5)
|
||||
{
|
||||
return $"1 {CurrentLocalization.LocalizationIndex["GLOBAL_TIME_MINUTES"]}{ago}";
|
||||
}
|
||||
|
||||
return Math.Round(Elapsed.TotalMinutes, 0) + $" {CurrentLocalization.LocalizationIndex["GLOBAL_TIME_MINUTES"]}{ago}";
|
||||
}
|
||||
if (Elapsed.TotalHours <= 24)
|
||||
{
|
||||
if (Elapsed.TotalHours < 1.5)
|
||||
{
|
||||
return $"1 {CurrentLocalization.LocalizationIndex["GLOBAL_TIME_HOURS"]}{ago}";
|
||||
}
|
||||
|
||||
return Math.Round(Elapsed.TotalHours, 0) + $" { CurrentLocalization.LocalizationIndex["GLOBAL_TIME_HOURS"]}{ago}";
|
||||
}
|
||||
if (Elapsed.TotalDays <= 90)
|
||||
{
|
||||
if (Elapsed.TotalDays < 1.5)
|
||||
{
|
||||
return $"1 {CurrentLocalization.LocalizationIndex["GLOBAL_TIME_DAYS"]}{ago}";
|
||||
}
|
||||
|
||||
return Math.Round(Elapsed.TotalDays, 0) + $" {CurrentLocalization.LocalizationIndex["GLOBAL_TIME_DAYS"]}{ago}";
|
||||
}
|
||||
if (Elapsed.TotalDays <= 365)
|
||||
{
|
||||
return $"{Math.Round(Elapsed.TotalDays / 7)} {CurrentLocalization.LocalizationIndex["GLOBAL_TIME_WEEKS"]}{ago}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{Math.Round(Elapsed.TotalDays / 30, 0)} {CurrentLocalization.LocalizationIndex["GLOBAL_TIME_MONTHS"]}{ago}";
|
||||
}
|
||||
}
|
||||
|
||||
public static Game GetGame(string gameName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(gameName))
|
||||
@ -519,42 +491,6 @@ namespace SharedLibraryCore
|
||||
return new TimeSpan(1, 0, 0);
|
||||
}
|
||||
|
||||
public static string TimeSpanText(this TimeSpan span)
|
||||
{
|
||||
var loc = CurrentLocalization.LocalizationIndex;
|
||||
|
||||
if (span.TotalMinutes < 60)
|
||||
{
|
||||
return $"{span.Minutes} {loc["GLOBAL_TIME_MINUTES"]}";
|
||||
}
|
||||
else if (span.Hours >= 1 && span.TotalHours < 24)
|
||||
{
|
||||
return $"{span.Hours} {loc["GLOBAL_TIME_HOURS"]}";
|
||||
}
|
||||
else if (span.TotalDays >= 1 && span.TotalDays < 7)
|
||||
{
|
||||
return $"{span.Days} {loc["GLOBAL_TIME_DAYS"]}";
|
||||
}
|
||||
else if (span.TotalDays >= 7 && span.TotalDays < 90)
|
||||
{
|
||||
return $"{Math.Round(span.Days / 7.0, 0)} {loc["GLOBAL_TIME_WEEKS"]}";
|
||||
}
|
||||
else if (span.TotalDays >= 90 && span.TotalDays < 365)
|
||||
{
|
||||
return $"{Math.Round(span.Days / 30.0, 0)} {loc["GLOBAL_TIME_MONTHS"]}";
|
||||
}
|
||||
else if (span.TotalDays >= 365 && span.TotalDays < 36500)
|
||||
{
|
||||
return $"{Math.Round(span.Days / 365.0, 0)} {loc["GLOBAL_TIME_YEARS"]}";
|
||||
}
|
||||
else if (span.TotalDays >= 36500)
|
||||
{
|
||||
return loc["GLOBAL_TIME_FOREVER"];
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns a list of penalty types that should be shown across all profiles
|
||||
/// </summary>
|
||||
@ -756,11 +692,11 @@ namespace SharedLibraryCore
|
||||
return server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue);
|
||||
}
|
||||
|
||||
public static async Task<Dvar<T>> GetMappedDvarValueOrDefaultAsync<T>(this Server server, string dvarName, string infoResponseName = null, IDictionary<string, string> infoResponse = null)
|
||||
public static async Task<Dvar<T>> GetMappedDvarValueOrDefaultAsync<T>(this Server server, string dvarName, string infoResponseName = null, IDictionary<string, string> infoResponse = null, T overrideDefault = default)
|
||||
{
|
||||
// todo: unit test this
|
||||
string mappedKey = server.RconParser.GetOverrideDvarName(dvarName);
|
||||
var defaultValue = server.RconParser.GetDefaultDvarValue<T>(mappedKey);
|
||||
var defaultValue = server.RconParser.GetDefaultDvarValue<T>(mappedKey) ?? overrideDefault;
|
||||
|
||||
string foundKey = infoResponse?.Keys.Where(_key => new[] { mappedKey, dvarName, infoResponseName ?? dvarName }.Contains(_key)).FirstOrDefault();
|
||||
|
||||
@ -857,6 +793,8 @@ namespace SharedLibraryCore
|
||||
byte[] bytes = toTest.GetAddressBytes();
|
||||
switch (bytes[0])
|
||||
{
|
||||
case 0:
|
||||
return bytes[1] == 0 && bytes[2] == 0 && bytes[3] == 0;
|
||||
case 10:
|
||||
return true;
|
||||
case 172:
|
||||
@ -895,7 +833,7 @@ namespace SharedLibraryCore
|
||||
/// <returns>true if the </returns>
|
||||
public static bool IsQuickMessage(this string message)
|
||||
{
|
||||
return Regex.IsMatch(message, @"^\u0014(?:[A-Z]|_)+$");
|
||||
return Regex.IsMatch(message, @"^\u0014(?:\w|_|!|\s)+$");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -932,7 +870,7 @@ namespace SharedLibraryCore
|
||||
/// <param name="penalty"></param>
|
||||
/// <param name="penaltyService"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <returns>true of the creat succeeds, false otherwise</returns>
|
||||
/// <returns>true of the create succeeds, false otherwise</returns>
|
||||
public static async Task<bool> TryCreatePenalty(this EFPenalty penalty, IEntityService<EFPenalty> penaltyService, ILogger logger)
|
||||
{
|
||||
try
|
||||
@ -969,7 +907,50 @@ namespace SharedLibraryCore
|
||||
}
|
||||
|
||||
public static bool ShouldHideLevel(this Permission perm) => perm == Permission.Flagged;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// parses translation string into tokens that are able to be formatted by the webfront
|
||||
/// </summary>
|
||||
/// <param name="translationKey">key for translation lookup</param>
|
||||
/// <returns></returns>
|
||||
public static WebfrontTranslationHelper[] SplitTranslationTokens(string translationKey)
|
||||
{
|
||||
string translationString = CurrentLocalization.LocalizationIndex[translationKey];
|
||||
var builder = new StringBuilder();
|
||||
var results = new List<WebfrontTranslationHelper>();
|
||||
|
||||
foreach (string word in translationString.Split(' '))
|
||||
{
|
||||
string finalWord = word;
|
||||
|
||||
if ((word.StartsWith("{{") && !word.EndsWith("}}")) ||
|
||||
(builder.Length > 0 && !word.EndsWith("}}")))
|
||||
{
|
||||
builder.Append($"{word} ");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (builder.Length > 0)
|
||||
{
|
||||
builder.Append(word);
|
||||
finalWord = builder.ToString();
|
||||
builder.Clear();
|
||||
}
|
||||
|
||||
var match = Regex.Match(finalWord, @"{{([^}|^-]+)(?:->)([^}]+)}}|{{([^}]+)}}");
|
||||
bool isInterpolation = match.Success;
|
||||
|
||||
results.Add(new WebfrontTranslationHelper
|
||||
{
|
||||
IsInterpolation = isInterpolation,
|
||||
MatchValue = isInterpolation ? match.Groups[3].Length > 0 ? match.Groups[3].ToString() : match.Groups[1].ToString() : finalWord,
|
||||
TranslationValue = isInterpolation && match.Groups[2].Length > 0 ? match.Groups[2].ToString() : ""
|
||||
});
|
||||
}
|
||||
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// indicates if running in development mode
|
||||
/// </summary>
|
||||
@ -991,5 +972,27 @@ namespace SharedLibraryCore
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// wrapper method for humanizee that uses current current culture
|
||||
/// </summary>
|
||||
public static string HumanizeForCurrentCulture(this TimeSpan timeSpan, int precision = 1, TimeUnit maxUnit = TimeUnit.Week,
|
||||
TimeUnit minUnit = TimeUnit.Millisecond, string collectionSeparator = ", ", bool toWords = false)
|
||||
{
|
||||
return timeSpan.Humanize(precision, CurrentLocalization.Culture, maxUnit, minUnit, collectionSeparator, toWords);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// wrapper method for humanizee that uses current current culture
|
||||
/// </summary>
|
||||
public static string HumanizeForCurrentCulture(this DateTime input, bool utcDate = true, DateTime? dateToCompareAgainst = null, CultureInfo culture = null)
|
||||
{
|
||||
return input.Humanize(utcDate, dateToCompareAgainst, CurrentLocalization.Culture);
|
||||
}
|
||||
|
||||
public static string ToTranslatedName(this MetaType metaType)
|
||||
{
|
||||
return CurrentLocalization.LocalizationIndex[$"META_TYPE_{metaType.ToString().ToUpper()}_NAME"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using Stats.Dtos;
|
||||
using StatsWeb.Dtos;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
@ -6,10 +6,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FakeItEasy" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="FakeItEasy" Version="6.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user