Compare commits

...

24 Commits

Author SHA1 Message Date
ecc2b5bf54 increase width of side context menu for longer server names 2022-06-09 13:59:00 -05:00
2ac9cc4379 fix bug with loading top stats for individual servers 2022-06-09 13:50:58 -05:00
215037095f remove extra parenthesis oops.. 2022-06-09 10:15:43 -05:00
5433d7d1d2 add total ranked client number for stats pages 2022-06-09 09:56:41 -05:00
0446fe1ec5 revert time out for status preventing server from entering unreachable state 2022-06-08 09:10:31 -05:00
cf2a00e5b3 add game to player profile and admins page 2022-06-07 21:58:32 -05:00
ab494a22cb add mwr to game list (h1) 2022-06-07 12:10:39 -05:00
b690579154 fix issue with meta event context after 1st page load 2022-06-05 16:35:39 -05:00
acc967e50a add ban management page 2022-06-05 16:27:56 -05:00
c493fbe13d add game badge to server overview 2022-06-04 09:58:30 -05:00
ee56a5db1f fix map/gametype alignment on server overview and add back ip display on connect click 2022-06-04 09:21:08 -05:00
f235d0fafd update for pluto t5 rcon issue 2022-06-03 17:01:58 -05:00
7ecf516278 add plutonium T5 parser. Must use ManualLogPath 2022-06-03 16:26:58 -05:00
210f1ca336 fix incorrect wildcard colorcode 2022-06-02 19:59:09 -05:00
a38789adb9 add default anticheat detection types 2022-06-02 18:30:22 -05:00
e459b2fcde Add per game anticheat configuration option for issue #203 2022-06-02 18:24:13 -05:00
26853a0005 fix issue with player name spacing on server overview at certain resolutions 2022-06-02 18:16:54 -05:00
ee14306db9 fix displaying correct server name on top players 2022-06-02 17:53:14 -05:00
169105e849 fix loader on mobile audit log view 2022-06-02 16:54:26 -05:00
7c10e0e3de add baninfo api 2022-06-02 16:48:47 -05:00
2f7eb07e39 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2022-06-02 15:51:59 -05:00
1f13f9122c fix intermittent issue with game interface during connection loss with servers 2022-06-01 11:25:11 -05:00
dd8c4f438f reduce logging for failed anticheat log parsing 2022-05-22 18:04:38 -05:00
2230036d45 fix issue with VPN banlist evaluation 2022-05-22 18:04:23 -05:00
77 changed files with 6122 additions and 249 deletions

View File

@ -24,7 +24,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2037" />
<PackageReference Include="Jint" Version="3.0.0-beta-2038" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>

View File

@ -790,10 +790,16 @@ namespace IW4MAdmin
/// array index 2 = updated clients
/// </summary>
/// <returns></returns>
async Task<List<EFClient>[]> PollPlayersAsync()
async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token)
{
var currentClients = GetClientsAsList();
var statusResponse = await this.GetStatusAsync(Manager.CancellationToken);
var statusResponse = await this.GetStatusAsync(token);
if (statusResponse is null)
{
return null;
}
var polledClients = statusResponse.Clients.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
@ -910,11 +916,11 @@ namespace IW4MAdmin
private DateTime _lastMessageSent = DateTime.Now;
private DateTime _lastPlayerCount = DateTime.Now;
public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
public override async Task<bool> ProcessUpdatesAsync(CancellationToken token)
{
try
{
if (cts.IsCancellationRequested)
if (token.IsCancellationRequested)
{
await ShutdownInternal();
return true;
@ -928,13 +934,18 @@ namespace IW4MAdmin
return true;
}
var polledClients = await PollPlayersAsync();
var polledClients = await PollPlayersAsync(token);
if (polledClients is null)
{
return true;
}
foreach (var disconnectingClient in polledClients[1]
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
{
disconnectingClient.CurrentServer = this;
var e = new GameEvent()
var e = new GameEvent
{
Type = GameEvent.EventType.PreDisconnect,
Origin = disconnectingClient,

View File

@ -0,0 +1,12 @@
using System;
using System.Threading;
namespace IW4MAdmin.Application.Misc;
public class AsyncResult : IAsyncResult
{
public object AsyncState { get; set; }
public WaitHandle AsyncWaitHandle { get; set; }
public bool CompletedSynchronously { get; set; }
public bool IsCompleted { get; set; }
}

View File

@ -276,8 +276,8 @@ namespace IW4MAdmin.Application.Misc
{
_logger.LogDebug("OnLoad executing for {Name}", Name);
_scriptEngine.SetValue("_manager", manager);
_scriptEngine.SetValue("getDvar", GetDvarAsync);
_scriptEngine.SetValue("setDvar", SetDvarAsync);
_scriptEngine.SetValue("getDvar", BeginGetDvar);
_scriptEngine.SetValue("setDvar", BeginSetDvar);
_scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
return Task.CompletedTask;
@ -343,7 +343,8 @@ namespace IW4MAdmin.Application.Misc
/// <param name="commands">commands value from jint parser</param>
/// <param name="scriptCommandFactory">factory to create the command from</param>
/// <returns></returns>
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory)
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands,
IScriptCommandFactory scriptCommandFactory)
{
var commandList = new List<IManagerCommand>();
@ -454,65 +455,54 @@ namespace IW4MAdmin.Application.Misc
return commandList;
}
private void GetDvarAsync(Server server, string dvarName, Delegate onCompleted)
{
Task.Run(() =>
private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
string result = null;
var success = true;
try
{
result = server.GetDvarAsync<string>(dvarName, token: tokenSource.Token).GetAwaiter().GetResult().Value;
}
catch
{
success = false;
}
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
_onProcessing.Wait();
server.BeginGetDvar(dvarName, result =>
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, result),
JsValue.FromObject(_scriptEngine, success),
JsValue.FromObject(_scriptEngine, value),
JsValue.FromObject(_scriptEngine, success)
});
}
finally
{
if (_onProcessing.CurrentCount == 0)
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release();
}
}
});
}, tokenSource.Token);
}
private void SetDvarAsync(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{
Task.Run(() =>
private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
var success = true;
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
server.BeginSetDvar(dvarName, dvarValue, result =>
{
var shouldRelease = false;
try
{
server.SetDvarAsync(dvarName, dvarValue, tokenSource.Token).GetAwaiter().GetResult();
}
catch
{
success = false;
}
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
var success = (bool)result.AsyncState;
_onProcessing.Wait();
try
{
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
@ -522,15 +512,14 @@ namespace IW4MAdmin.Application.Misc
JsValue.FromObject(_scriptEngine, success)
});
}
finally
{
if (_onProcessing.CurrentCount == 0)
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release();
}
}
});
}, tokenSource.Token);
}
}

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Server;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -22,18 +23,20 @@ namespace IW4MAdmin.Application.Misc
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache,
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache)
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
{
_logger = logger;
_snapshotCache = snapshotCache;
_serverStatsCache = serverStatsCache;
_clientHistoryCache = clientHistoryCache;
_rankedClientsCache = rankedClientsCache;
}
public async Task<(int?, DateTime?)>
@ -160,5 +163,30 @@ namespace IW4MAdmin.Application.Misc
return Enumerable.Empty<ClientHistoryInfo>();
}
}
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
{
_rankedClientsCache.SetCacheItem(async (set, cancellationToken) =>
{
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
return await set
.Where(rating => rating.Newest)
.Where(rating => rating.ServerId == serverId)
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken);
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan, true);
try
{
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(RankedClientsCountAsync));
return 0;
}
}
}
}

View File

@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -141,6 +142,30 @@ namespace IW4MAdmin.Application.RConParsers
};
}
public void BeginGetDvar(IRConConnection connection, string dvarName, AsyncCallback callback, CancellationToken token = default)
{
GetDvarAsync<string>(connection, dvarName, token: token).ContinueWith(action =>
{
if (action.Exception is null)
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = (true, action.Result.Value)
});
}
else
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = (false, (string)null)
});
}
}, token);
}
public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default)
{
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token);
@ -196,6 +221,31 @@ namespace IW4MAdmin.Application.RConParsers
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0;
}
public void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback,
CancellationToken token = default)
{
SetDvarAsync(connection, dvarName, dvarValue, token).ContinueWith(action =>
{
if (action.Exception is null)
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = true
});
}
else
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = false
});
}
}, token);
}
private List<EFClient> ClientsFromStatus(string[] Status)
{
List<EFClient> StatusPlayers = new List<EFClient>();

View File

@ -46,7 +46,7 @@ namespace IW4MAdmin.Application.RConParsers
{ColorCodes.White.ToString(), "^7"},
{ColorCodes.Map.ToString(), "^8"},
{ColorCodes.Grey.ToString(), "^9"},
{ColorCodes.Wildcard.ToString(), ":^"},
{ColorCodes.Wildcard.ToString(), "^:"}
};
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
@ -9,6 +10,11 @@ namespace Data.Abstractions
{
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
TimeSpan? expirationTime = null, bool autoRefresh = false);
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false);
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
Task<TReturnType> GetCacheItem(string keyName, object id = null, CancellationToken token = default);
}
}

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
@ -15,8 +17,7 @@ namespace Data.Helpers
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ConcurrentDictionary<string, CacheState<TReturnType>> _cacheStates =
new ConcurrentDictionary<string, CacheState<TReturnType>>();
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
private bool _autoRefresh;
private const int DefaultExpireMinutes = 15;
@ -51,10 +52,24 @@ namespace Data.Helpers
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
TimeSpan? expirationTime = null, bool autoRefresh = false)
{
if (_cacheStates.ContainsKey(key))
SetCacheItem(getter, key, null, expirationTime, autoRefresh);
}
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
{
_logger.LogDebug("Cache key {Key} is already added", key);
return;
ids ??= new[] { new object() };
if (!_cacheStates.ContainsKey(key))
{
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
}
foreach (var id in ids)
{
if (_cacheStates[key].ContainsKey(id))
{
continue;
}
var state = new CacheState<TReturnType>
@ -64,9 +79,10 @@ namespace Data.Helpers
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_cacheStates[key].Add(id, state);
_autoRefresh = autoRefresh;
_cacheStates.TryAdd(key, state);
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
@ -77,15 +93,20 @@ namespace Data.Helpers
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
}
}
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default)
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
await GetCacheItem(keyName, null, cancellationToken);
public async Task<TReturnType> GetCacheItem(string keyName, object id = null,
CancellationToken cancellationToken = default)
{
if (!_cacheStates.ContainsKey(keyName))
{
throw new ArgumentException("No cache found for key {key}", keyName);
}
var state = _cacheStates[keyName];
var state = id is null ? _cacheStates[keyName].Values.First() : _cacheStates[keyName][id];
// when auto refresh is off we want to check the expiration and value
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

View File

@ -456,6 +456,8 @@ namespace Data.Migrations.MySql
b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking");
b.HasIndex("ServerId");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

View File

@ -475,6 +475,8 @@ namespace Data.Migrations.Postgresql
b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking");
b.HasIndex("ServerId");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

View File

@ -454,6 +454,8 @@ namespace Data.Migrations.Sqlite
b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking");
b.HasIndex("ServerId");

View File

@ -86,6 +86,7 @@ namespace Data.Models.Configuration
entity.HasIndex(ranking => ranking.Ranking);
entity.HasIndex(ranking => ranking.ZScore);
entity.HasIndex(ranking => ranking.UpdatedDateTime);
entity.HasIndex(ranking => ranking.CreatedDateTime);
});
}
}

View File

@ -15,7 +15,8 @@
T6 = 7,
T7 = 8,
SHG1 = 9,
CSGO = 10
CSGO = 10,
H1 = 11
}
public enum ConnectionType

View File

@ -53,6 +53,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\GameInterface.js = Plugins\ScriptPlugins\GameInterface.js
Plugins\ScriptPlugins\SubnetBan.js = Plugins\ScriptPlugins\SubnetBan.js
Plugins\ScriptPlugins\BanBroadcasting.js = Plugins\ScriptPlugins\BanBroadcasting.js
Plugins\ScriptPlugins\ParserH1MOD.js = Plugins\ScriptPlugins\ParserH1MOD.js
Plugins\ScriptPlugins\ParserPlutoniumT5.js = Plugins\ScriptPlugins\ParserPlutoniumT5.js
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"

View File

@ -368,7 +368,9 @@ namespace Integrations.Cod
throw new RConException("Unexpected response header from server");
}
var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.StartsWith("^7") ? line[2..] : line).ToArray();
return splitResponse;
}

View File

@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,7 +19,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -0,0 +1,39 @@
var rconParser;
var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.1,
name: 'Plutonium T5 Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t5';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.Dvar.Pattern = '^(?:\\^7)?\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$';
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.DefaultRConPort = 3074;
rconParser.Configuration.CanGenerateLogPath = false;
rconParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
rconParser.GameName = 6; // T5
eventParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
eventParser.GameName = 6; // T5
eventParser.Configuration.GuidNumberStyle = 7; // Integer
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -29,7 +29,7 @@ const plugin = {
let exempt = false;
// prevent players that are exempt from being kicked
vpnExceptionIds.forEach(function (id) {
if (id === origin.ClientId) {
if (id == origin.ClientId) { // when loaded from the config the "id" type is not the same as the ClientId type
exempt = true;
return false;
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
@ -88,8 +89,8 @@ namespace Stats.Client
return zScore ?? 0;
}, MaxZScoreCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30));
await _distributionCache.GetCacheItem(DistributionCacheKey);
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey);
await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
/*foreach (var serverId in _serverIds)
{
@ -132,7 +133,7 @@ namespace Stats.Client
public async Task<double> GetZScoreForServer(long serverId, double value)
{
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey);
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
if (!serverParams.ContainsKey(serverId))
{
return 0.0;
@ -150,7 +151,7 @@ namespace Stats.Client
public async Task<double?> GetRatingForZScore(double? value)
{
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey);
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore);
}
}

View File

@ -79,7 +79,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
}
else
{
gameEvent.Owner.Broadcast(topStats);
await gameEvent.Owner.BroadcastAsync(topStats);
}
}
}

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
using static SharedLibraryCore.Server;
@ -7,7 +9,15 @@ namespace Stats.Config
public class AnticheatConfiguration
{
public bool Enable { get; set; }
[Obsolete]
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; } = new Dictionary<long, DetectionType[]>();
public IDictionary<Game, DetectionType[]> GameDetectionTypes { get; set; } =
new Dictionary<Game, DetectionType[]>()
{
{ Game.IW4, Enum.GetValues(typeof(DetectionType)).Cast<DetectionType>().ToArray() },
{ Game.T6, new[] { DetectionType.Offset, DetectionType.Snap, DetectionType.Strain } }
};
public IList<long> IgnoredClientIds { get; set; } = new List<long>();
public IDictionary<Game, IDictionary<DetectionType, string[]>> IgnoredDetectionSpecification{ get; set; } = new Dictionary<Game, IDictionary<DetectionType, string[]>>
{

View File

@ -14,6 +14,7 @@ namespace Stats.Dtos
public EFClient.Permission Level { get; set; }
public double? Performance { get; set; }
public int? Ranking { get; set; }
public int TotalRankedClients { get; set; }
public double? ZScore { get; set; }
public double? Rating { get; set; }
public List<ServerInfo> Servers { get; set; }

View File

@ -633,7 +633,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return;
}
var hit = new EFClientKill()
EFClientKill hit;
try
{
hit = new EFClientKill
{
Active = true,
AttackerId = attacker.ClientId,
@ -660,6 +663,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
GameName = (int) attacker.CurrentServer.GameName
};
}
catch (Exception ex)
{
_log.LogError(ex, "Could not parse script hit data. Damage={Damage}, TimeOffset={Offset}, TimeSinceLastAttack={LastAttackTime}",
damage, offset, lastAttackTime);
return;
}
hit.SetAdditionalProperty("HitLocationReference", hitLoc);
if (hit.HitLoc == (int) IW4Info.HitLocation.shield)
@ -769,7 +781,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
catch (Exception ex)
{
_log.LogError(ex, "Could not save hit or anti-cheat info {@attacker} {@victim} {server}", attacker,
_log.LogError(ex, "Could not save hit or anti-cheat info {Attacker} {Victim} {Server}", attacker,
victim, serverId);
}
@ -806,7 +818,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
{
var detectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes;
var serverDetectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes;
var gameDetectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.GameDetectionTypes;
var ignoredClients = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredClientIds;
if (ignoredClients.Contains(clientId))
@ -814,10 +827,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return false;
}
try
{
if (!detectionTypes[server.EndPoint].Contains(detectionType))
if (!serverDetectionTypes[server.EndPoint].Contains(detectionType))
{
return false;
}
@ -827,6 +839,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
}
try
{
if (!gameDetectionTypes[server.GameName].Contains(detectionType))
{
return false;
}
}
catch
{
// ignored
}
return true;
}

View File

@ -42,10 +42,11 @@ namespace IW4MAdmin.Plugins.Stats
private readonly ILogger<Plugin> _logger;
private readonly List<IClientStatisticCalculator> _statCalculators;
private readonly IServerDistributionCalculator _serverDistributionCalculator;
private readonly IServerDataViewer _serverDataViewer;
public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory,
ITranslationLookup translationLookup, IMetaServiceV2 metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper, ILogger<StatManager> managerLogger,
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator)
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator, IServerDataViewer serverDataViewer)
{
Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
_databaseContextFactory = databaseContextFactory;
@ -56,6 +57,7 @@ namespace IW4MAdmin.Plugins.Stats
_logger = logger;
_statCalculators = statCalculators.ToList();
_serverDistributionCalculator = serverDistributionCalculator;
_serverDataViewer = serverDataViewer;
}
public async Task OnEventAsync(GameEvent gameEvent, Server server)
@ -201,13 +203,17 @@ namespace IW4MAdmin.Plugins.Stats
var performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed);
var performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
var spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Count(c => c.SPM > 0), 1);
var overallRanking = await Manager.GetClientOverallRanking(request.ClientId);
return new List<InformationResponse>
{
new InformationResponse
{
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
Value = "#" + (await Manager.GetClientOverallRanking(request.ClientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Value = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING_FORMAT"].FormatExt((overallRanking == 0 ? "--" :
overallRanking.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))),
(await _serverDataViewer.RankedClientsCountAsync(token: token)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))
),
Column = 0,
Order = 0,
Type = MetaType.Information

View File

@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -20,7 +20,7 @@
</Target>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,5 @@
using System;
using Data.Models;
using Data.Models.Client;
namespace SharedLibraryCore.Dtos
@ -10,6 +11,7 @@ namespace SharedLibraryCore.Dtos
public int LinkId { get; set; }
public EFClient.Permission Level { get; set; }
public DateTime LastConnection { get; set; }
public Reference.Game Game { get; set; }
public bool IsMasked { get; set; }
}
}

View File

@ -13,7 +13,7 @@ namespace SharedLibraryCore.Dtos
public int Offset { get; set; }
/// <summary>
/// how many itesm to take
/// how many items to take
/// </summary>
public int Count { get; set; } = 100;

View File

@ -9,6 +9,7 @@ namespace SharedLibraryCore.Dtos
public class PlayerInfo
{
public string Name { get; set; }
public Reference.Game Game { get; set; }
public int ClientId { get; set; }
public string Level { get; set; }
public string Tag { get; set; }

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Data.Models;
using SharedLibraryCore.Helpers;
namespace SharedLibraryCore.Dtos
@ -40,5 +41,6 @@ namespace SharedLibraryCore.Dtos
return Math.Round(valid.Select(player => player.ZScore.Value).Average(), 2);
}
}
public Reference.Game Game { get; set; }
}
}

View File

@ -56,6 +56,8 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns>
Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default);
void BeginGetDvar(IRConConnection connection, string dvarName, AsyncCallback callback, CancellationToken token = default);
/// <summary>
/// set value of DVAR by name
/// </summary>
@ -66,6 +68,8 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns>
Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default);
void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback, CancellationToken token = default);
/// <summary>
/// executes a console command on the server
/// </summary>

View File

@ -37,5 +37,13 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns>
Task<IEnumerable<ClientHistoryInfo>> ClientHistoryAsync(TimeSpan? overPeriod = null,
CancellationToken token = default);
/// <summary>
/// Retrieves the number of ranked clients for given server id
/// </summary>
/// <param name="serverId">ServerId to query on</param>
/// <param name="token">CancellationToken</param>
/// <returns></returns>
Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default);
}
}

View File

@ -33,7 +33,8 @@ namespace SharedLibraryCore
T6 = 7,
T7 = 8,
SHG1 = 9,
CSGO = 10
CSGO = 10,
H1 = 11
}
// only here for performance
@ -200,7 +201,7 @@ namespace SharedLibraryCore
.ToList();
}
public virtual Task<bool> ProcessUpdatesAsync(CancellationToken cts)
public virtual Task<bool> ProcessUpdatesAsync(CancellationToken token)
{
return (Task<bool>)Task.CompletedTask;
}

View File

@ -178,6 +178,7 @@ namespace SharedLibraryCore.Services
.Select(_client => new EFClient
{
ClientId = _client.ClientId,
GameName = _client.GameName,
AliasLinkId = _client.AliasLinkId,
Level = _client.Level,
Connections = _client.Connections,
@ -789,7 +790,8 @@ namespace SharedLibraryCore.Services
PasswordSalt = client.PasswordSalt,
NetworkId = client.NetworkId,
LastConnection = client.LastConnection,
Masked = client.Masked
Masked = client.Masked,
GameName = client.GameName
};
return await iqClients.ToListAsync();

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2022.3.23.1</Version>
<Version>2022.6.9.1</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2022.3.23.1</PackageVersion>
<PackageVersion>2022.6.9.1</PackageVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

View File

@ -47,6 +47,9 @@ namespace SharedLibraryCore
public static char[] DirectorySeparatorChars = { '\\', '/' };
public static char CommandPrefix { get; set; } = '!';
public static string ToStandardFormat(this DateTime? time) => time?.ToString("yyyy-MM-dd H:mm:ss UTC");
public static string ToStandardFormat(this DateTime time) => time.ToString("yyyy-MM-dd H:mm:ss UTC");
public static EFClient IW4MAdminClient(Server server = null)
{
return new EFClient
@ -774,6 +777,11 @@ namespace SharedLibraryCore
return await server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue, token);
}
public static void BeginGetDvar(this Server server, string dvarName, AsyncCallback callback, CancellationToken token = default)
{
server.RconParser.BeginGetDvar(server.RemoteConnection, dvarName, callback, token);
}
public static async Task<Dvar<T>> GetDvarAsync<T>(this Server server, string dvarName,
T fallbackValue = default)
{
@ -809,6 +817,12 @@ namespace SharedLibraryCore
await server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue, token);
}
public static void BeginSetDvar(this Server server, string dvarName, object dvarValue,
AsyncCallback callback, CancellationToken token = default)
{
server.RconParser.BeginSetDvar(server.RemoteConnection, dvarName, dvarValue, callback, token);
}
public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue)
{
await SetDvarAsync(server, dvarName, dvarValue, default);
@ -824,9 +838,17 @@ namespace SharedLibraryCore
return await ExecuteCommandAsync(server, commandName, default);
}
public static Task<IStatusResponse> GetStatusAsync(this Server server, CancellationToken token)
public static async Task<IStatusResponse> GetStatusAsync(this Server server, CancellationToken token)
{
return server.RconParser.GetStatusAsync(server.RemoteConnection, token);
try
{
return await server.RconParser.GetStatusAsync(server.RemoteConnection, token);
}
catch (TaskCanceledException)
{
return null;
}
}
/// <summary>

View File

@ -0,0 +1,25 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using WebfrontCore.QueryHelpers.Models;
namespace WebfrontCore.Controllers.API;
[Route("api/[controller]")]
public class PenaltyController : BaseController
{
private readonly IResourceQueryHelper<BanInfoRequest, BanInfo> _banInfoQueryHelper;
public PenaltyController(IManager manager, IResourceQueryHelper<BanInfoRequest, BanInfo> banInfoQueryHelper) : base(manager)
{
_banInfoQueryHelper = banInfoQueryHelper;
}
[HttpGet("BanInfo/{clientName}")]
public async Task<IActionResult> BanInfo(BanInfoRequest request)
{
var result = await _banInfoQueryHelper.QueryResource(request);
return Json(result);
}
}

View File

@ -142,7 +142,7 @@ namespace WebfrontCore.Controllers
}));
}
public IActionResult UnbanForm()
public IActionResult UnbanForm(long? id)
{
var info = new ActionInfo
{
@ -159,6 +159,15 @@ namespace WebfrontCore.Controllers
Action = "UnbanAsync",
ShouldRefresh = true
};
if (id is not null)
{
info.Inputs.Add(new()
{
Name = "targetId",
Value = id.ToString(),
Type = "hidden"
});
}
return View("_ActionForm", info);
}

View File

@ -4,6 +4,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Threading.Tasks;
using WebfrontCore.QueryHelpers.Models;
namespace WebfrontCore.Controllers
{
@ -11,12 +12,16 @@ namespace WebfrontCore.Controllers
{
private readonly IAuditInformationRepository _auditInformationRepository;
private readonly ITranslationLookup _translationLookup;
private readonly IResourceQueryHelper<BanInfoRequest, BanInfo> _banInfoQueryHelper;
private static readonly int DEFAULT_COUNT = 25;
public AdminController(IManager manager, IAuditInformationRepository auditInformationRepository, ITranslationLookup translationLookup) : base(manager)
public AdminController(IManager manager, IAuditInformationRepository auditInformationRepository,
ITranslationLookup translationLookup,
IResourceQueryHelper<BanInfoRequest, BanInfo> banInfoQueryHelper) : base(manager)
{
_auditInformationRepository = auditInformationRepository;
_translationLookup = translationLookup;
_banInfoQueryHelper = banInfoQueryHelper;
}
[Authorize]
@ -27,7 +32,7 @@ namespace WebfrontCore.Controllers
ViewBag.Title = _translationLookup["WEBFRONT_NAV_AUDIT_LOG"];
ViewBag.InitialOffset = DEFAULT_COUNT;
var auditItems = await _auditInformationRepository.ListAuditInformation(new PaginationRequest()
var auditItems = await _auditInformationRepository.ListAuditInformation(new PaginationRequest
{
Count = DEFAULT_COUNT
});
@ -41,5 +46,25 @@ namespace WebfrontCore.Controllers
var auditItems = await _auditInformationRepository.ListAuditInformation(paginationInfo);
return PartialView("_ListAuditLog", auditItems);
}
public async Task<IActionResult> BanManagement([FromQuery] BanInfoRequest request)
{
var results = await _banInfoQueryHelper.QueryResource(request);
ViewBag.ClientName = request.ClientName;
ViewBag.ClientId = request.ClientId;
ViewBag.ClientIP = request.ClientIP;
ViewBag.ClientGuid = request.ClientGuid;
ViewBag.Title = "Ban Management";
return View(results.Results);
}
public async Task<IActionResult> BanManagementList([FromQuery] BanInfoRequest request)
{
var results = await _banInfoQueryHelper.QueryResource(request);
return PartialView("_BanEntries", results.Results);
}
}
}

View File

@ -88,6 +88,7 @@ namespace WebfrontCore.Controllers
var clientDto = new PlayerInfo
{
Name = client.Name,
Game = client.GameName ?? Reference.Game.UKN,
Level = displayLevel,
LevelInt = displayLevelInt,
ClientId = client.ClientId,
@ -181,7 +182,8 @@ namespace WebfrontCore.Controllers
Name = admin.Name,
ClientId = admin.ClientId,
LastConnection = admin.LastConnection,
IsMasked = admin.Masked
IsMasked = admin.Masked,
Game = admin.GameName ?? Reference.Game.UKN
});
}

View File

@ -1,5 +1,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using IW4MAdmin.Plugins.Stats.Helpers;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
@ -13,26 +15,38 @@ namespace WebfrontCore.Controllers
{
private IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> _queryHelper;
private readonly DefaultSettings _defaultConfig;
private readonly IServerDataViewer _serverDataViewer;
public ClientStatisticsController(IManager manager,
IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> queryHelper,
DefaultSettings defaultConfig) : base(manager)
DefaultSettings defaultConfig, IServerDataViewer serverDataViewer) : base(manager)
{
_queryHelper = queryHelper;
_defaultConfig = defaultConfig;
_serverDataViewer = serverDataViewer;
}
[HttpGet("{id:int}/advanced")]
public async Task<IActionResult> Advanced(int id, [FromQuery] string serverId)
public async Task<IActionResult> Advanced(int id, [FromQuery] string serverId, CancellationToken token = default)
{
ViewBag.Config = _defaultConfig.GameStrings;
var hitInfo = await _queryHelper.QueryResource(new StatsInfoRequest
var hitInfo = (await _queryHelper.QueryResource(new StatsInfoRequest
{
ClientId = id,
ServerEndpoint = serverId
});
})).Results.First();
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo.Results.First());
var server = Manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
long? matchedServerId = null;
if (server != null)
{
matchedServerId = StatManager.GetIdForServer(server);
}
hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo);
}
}
}

View File

@ -11,6 +11,7 @@ using Stats.Dtos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -28,10 +29,11 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
private readonly ITranslationLookup _translationLookup;
private readonly IDatabaseContextFactory _contextFactory;
private readonly StatsConfiguration _config;
private readonly IServerDataViewer _serverDataViewer;
public StatsController(ILogger<StatsController> logger, IManager manager, IResourceQueryHelper<ChatSearchQuery,
MessageResponse> resourceQueryHelper, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory, StatsConfiguration config) : base(manager)
IDatabaseContextFactory contextFactory, StatsConfiguration config, IServerDataViewer serverDataViewer) : base(manager)
{
_logger = logger;
_manager = manager;
@ -39,16 +41,28 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
_translationLookup = translationLookup;
_contextFactory = contextFactory;
_config = config;
_serverDataViewer = serverDataViewer;
}
[HttpGet]
public IActionResult TopPlayers(string serverId = null)
public async Task<IActionResult> TopPlayers(string serverId = null, CancellationToken token = default)
{
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"];
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"];
ViewBag.Localization = _translationLookup;
ViewBag.SelectedServerId = serverId;
var server = _manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
long? matchedServerId = null;
if (server != null)
{
matchedServerId = StatManager.GetIdForServer(server);
}
ViewBag.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
ViewBag.ServerId = matchedServerId;
return View("~/Views/Client/Statistics/Index.cshtml", _manager.GetServers()
.Select(server => new ServerInfo
{

View File

@ -5,6 +5,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Linq;
using Data.Models;
using Data.Models.Client.Stats;
using IW4MAdmin.Plugins.Stats.Helpers;
using WebfrontCore.ViewModels;
@ -34,6 +35,7 @@ namespace WebfrontCore.Controllers
ID = s.EndPoint,
Port = s.Port,
Map = s.CurrentMap.Alias,
Game = (Reference.Game)s.GameName,
ClientCount = s.Clients.Count(client => client != null),
MaxClients = s.MaxClients,
GameType = s.GametypeName,

View File

@ -0,0 +1,232 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Data.Models.Client;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using WebfrontCore.QueryHelpers.Models;
namespace WebfrontCore.QueryHelpers;
public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, BanInfo>
{
private readonly IDatabaseContextFactory _contextFactory;
public BanInfoResourceQueryHelper(IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<ResourceQueryHelperResult<BanInfo>> QueryResource(BanInfoRequest query)
{
if (query.Count > 10)
{
query.Count = 10;
}
await using var context = _contextFactory.CreateContext(false);
var iqMatchingClients = context.Clients.Where(client => client.Level == EFClient.Permission.Banned);
iqMatchingClients = SetupSearchArgs(query, iqMatchingClients);
if (string.IsNullOrEmpty(query.ClientName) && string.IsNullOrEmpty(query.ClientGuid) &&
query.ClientId is null && string.IsNullOrEmpty(query.ClientIP))
{
return new ResourceQueryHelperResult<BanInfo>
{
Results = Enumerable.Empty<BanInfo>()
};
}
var matchingClients = await iqMatchingClients
.OrderByDescending(client => client.LastConnection)
.Skip(query.Offset)
.Take(query.Count)
.Select(client => new
{
client.CurrentAlias.Name,
client.NetworkId,
client.AliasLinkId,
client.ClientId,
client.CurrentAlias.IPAddress
}).ToListAsync();
var results = new List<BanInfo>();
var matchedClientIds = new List<int?>();
var lateDateTime = DateTime.Now.AddYears(100);
// would prefer not to loop this, but unfortunately due to the data design
// we can't properly group on ip and alias link
foreach (var matchingClient in matchingClients)
{
var usedIps = await context.Aliases
.Where(alias => matchingClient.AliasLinkId == alias.LinkId)
.Where(alias => alias.IPAddress != null)
.Select(alias => new { alias.IPAddress, alias.LinkId })
.ToListAsync();
var searchingNetworkId = matchingClient.NetworkId;
var searchingIps = usedIps.Select(ip => ip.IPAddress).Distinct();
var matchedPenalties = await context.PenaltyIdentifiers.Where(identifier =>
identifier.NetworkId == searchingNetworkId ||
searchingIps.Contains(identifier.IPv4Address))
.Where(identifier => identifier.Penalty.Expires == null || identifier.Penalty.Expires > lateDateTime)
.Select(penalty => new
{
penalty.CreatedDateTime,
PunisherName = penalty.Penalty.Punisher.CurrentAlias.Name,
OffenderName = penalty.Penalty.Offender.CurrentAlias.Name,
Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense)
? penalty.Penalty.Offense
: "Anticheat Detection",
LinkId = penalty.Penalty.Offender.AliasLinkId,
penalty.Penalty.OffenderId,
penalty.Penalty.PunisherId,
penalty.IPv4Address,
penalty.Penalty.Offender.NetworkId
})
.ToListAsync();
if (!matchedPenalties.Any())
{
var linkIds = (await context.Aliases
.Where(alias => alias.IPAddress != null && searchingIps.Contains(alias.IPAddress))
.Select(alias => alias.LinkId)
.ToListAsync()).Distinct();
matchedPenalties = await context.Penalties.Where(penalty => penalty.Type == EFPenalty.PenaltyType.Ban)
.Where(penalty => penalty.Expires == null || penalty.Expires > lateDateTime)
.Where(penalty => penalty.LinkId != null && linkIds.Contains(penalty.LinkId.Value))
.OrderByDescending(penalty => penalty.When)
.Select(penalty => new
{
CreatedDateTime = penalty.When,
PunisherName = penalty.Punisher.CurrentAlias.Name,
OffenderName = penalty.Offender.CurrentAlias.Name,
Offense = string.IsNullOrEmpty(penalty.AutomatedOffense)
? penalty.Offense
: "Anticheat Detection",
LinkId = penalty.Offender.AliasLinkId,
penalty.OffenderId,
penalty.PunisherId,
IPv4Address = penalty.Offender.CurrentAlias.IPAddress,
penalty.Offender.NetworkId
}).ToListAsync();
}
var allPenalties = matchedPenalties.Select(penalty => new PenaltyInfo
{
DateTime = penalty.CreatedDateTime,
Offense = penalty.Offense,
PunisherInfo = new RelatedClientInfo
{
ClientName = penalty.PunisherName.StripColors(),
ClientId = penalty.PunisherId,
},
OffenderInfo = new RelatedClientInfo
{
ClientName = penalty.OffenderName.StripColors(),
ClientId = penalty.OffenderId,
IPAddress = penalty.IPv4Address,
NetworkId = penalty.NetworkId
}
}).ToList();
if (matchedClientIds.Contains(matchingClient.ClientId))
{
continue;
}
matchedClientIds.Add(matchingClient.ClientId);
var relatedEntities =
allPenalties.Where(penalty => penalty.OffenderInfo.ClientId != matchingClient.ClientId);
matchedClientIds.AddRange(relatedEntities.Select(client => client.OffenderInfo.ClientId));
results.Add(new BanInfo
{
ClientName = matchingClient.Name.StripColors(),
ClientId = matchingClient.ClientId,
NetworkId = matchingClient.NetworkId,
IPAddress = matchingClient.IPAddress,
AssociatedPenalties = relatedEntities,
AttachedPenalty = allPenalties.FirstOrDefault(penalty =>
penalty.OffenderInfo.ClientId == matchingClient.ClientId)
});
}
return new ResourceQueryHelperResult<BanInfo>
{
RetrievedResultCount = results.Count,
TotalResultCount = results.Count,
Results = results
};
}
private IQueryable<EFClient> SetupSearchArgs(BanInfoRequest query, IQueryable<EFClient> source)
{
if (!string.IsNullOrEmpty(query.ClientName))
{
var nameToSearch = query.ClientName.Trim().ToLower();
source = source.Where(client =>
EF.Functions.Like(client.CurrentAlias.SearchableName ?? client.CurrentAlias.Name.ToLower(),
$"%{nameToSearch}%"));
}
if (!string.IsNullOrEmpty(query.ClientGuid))
{
long? parsedGuid = null;
if (!long.TryParse(query.ClientGuid, NumberStyles.HexNumber, null, out var guid))
{
if (!long.TryParse(query.ClientGuid, out var guid2))
{
}
else
{
parsedGuid = guid2;
}
}
else
{
parsedGuid = guid;
}
if (parsedGuid is not null)
{
source = source.Where(client => client.NetworkId == parsedGuid);
}
}
if (query.ClientId is not null)
{
source = source.Where(client => client.ClientId == query.ClientId);
}
if (string.IsNullOrEmpty(query.ClientIP))
{
return source;
}
var parsedIp = query.ClientIP.ConvertToIP();
if (parsedIp is not null)
{
source = source.Where(client => client.CurrentAlias.IPAddress == parsedIp);
}
else
{
query.ClientIP = null;
}
return source;
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
namespace WebfrontCore.QueryHelpers.Models;
public class BanInfo
{
public string ClientName { get; set; }
public int ClientId { get; set; }
public int? IPAddress { get; set; }
public long NetworkId { get; set; }
public PenaltyInfo AttachedPenalty { get; set; }
public IEnumerable<PenaltyInfo> AssociatedPenalties { get; set; }
}
public class PenaltyInfo
{
public RelatedClientInfo OffenderInfo { get; set; }
public RelatedClientInfo PunisherInfo { get; set; }
public string Offense { get; set; }
public DateTime? DateTime { get; set; }
public long? TimeStamp =>
DateTime.HasValue ? new DateTimeOffset(DateTime.Value, TimeSpan.Zero).ToUnixTimeSeconds() : null;
}
public class RelatedClientInfo
{
public string ClientName { get; set; }
public int? ClientId { get; set; }
public int? IPAddress { get; set; }
public long? NetworkId { get; set; }
}

View File

@ -0,0 +1,11 @@
using SharedLibraryCore.Dtos;
namespace WebfrontCore.QueryHelpers.Models;
public class BanInfoRequest : PaginationRequest
{
public string ClientName { get; set; }
public string ClientGuid { get; set; }
public int? ClientId { get; set; }
public string ClientIP { get; set; }
}

View File

@ -24,11 +24,12 @@ using System.Reflection;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Helpers;
using IW4MAdmin.Plugins.Stats.Config;
using Stats.Client.Abstractions;
using Stats.Config;
using WebfrontCore.Controllers.API.Validation;
using WebfrontCore.Middleware;
using WebfrontCore.QueryHelpers;
using WebfrontCore.QueryHelpers.Models;
namespace WebfrontCore
{
@ -127,6 +128,7 @@ namespace WebfrontCore
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IMetaServiceV2>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ApplicationConfiguration>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ClientService>());
services.AddSingleton<IResourceQueryHelper<BanInfoRequest, BanInfo>, BanInfoResourceQueryHelper>();
services.AddSingleton(
Program.ApplicationServiceProvider.GetRequiredService<IServerDistributionCalculator>());
services.AddSingleton(Program.ApplicationServiceProvider

View File

@ -4,11 +4,9 @@ using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client.Stats;
using Microsoft.AspNetCore.Hosting.Server;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using static SharedLibraryCore.Server;
@ -72,6 +70,7 @@ namespace WebfrontCore.ViewComponents
ID = server.EndPoint,
Port = server.Port,
Map = server.CurrentMap.Alias,
Game = (Reference.Game)server.GameName,
ClientCount = server.Clients.Count(client => client != null),
MaxClients = server.MaxClients,
GameType = server.GametypeName,

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Html;
namespace WebfrontCore.ViewModels;
@ -19,7 +20,7 @@ public class TableInfo
public class RowDefinition
{
public List<string> Datum { get; } = new();
public List<ColumnTypeDefinition> Datum { get; } = new();
}
public class ColumnDefinition
@ -28,6 +29,23 @@ public class ColumnDefinition
public string ColumnSpan { get; set; }
}
public enum ColumnType
{
Text,
Link,
Icon,
Button
}
public class ColumnTypeDefinition
{
public ColumnType Type { get; set; }
public string Value { get; set; }
public string Data { get; set; }
public IHtmlContent Template { get; set; }
public int Id { get; set; }
}
public static class TableInfoExtensions
{
public static TableInfo WithColumns(this TableInfo info, IEnumerable<string> columns)
@ -42,6 +60,16 @@ public static class TableInfoExtensions
public static TableInfo WithRows<T>(this TableInfo info, IEnumerable<T> source,
Func<T, IEnumerable<string>> selector)
{
return WithRows(info, source, (outer) => selector(outer).Select(item => new ColumnTypeDefinition
{
Value = item,
Type = ColumnType.Text
}));
}
public static TableInfo WithRows<T>(this TableInfo info, IEnumerable<T> source,
Func<T, IEnumerable<ColumnTypeDefinition>> selector)
{
info.Rows.AddRange(source.Select(row =>
{

View File

@ -19,7 +19,7 @@
<partial name="_ListAuditLog"/>
</tbody>
</table>
<i id="loaderLoad" class="loader-load-more oi oi-chevron-bottom text-center text-primary d-none d-lg-block mt-10"></i>
<i id="loaderLoad" class="loader-load-more oi oi-chevron-bottom text-center w-full text-primary mt-10"></i>
</div>
@section scripts {

View File

@ -0,0 +1,66 @@
@model IEnumerable<WebfrontCore.QueryHelpers.Models.BanInfo>
<div class="content mt-0">
<h2 class="content-title mt-20 mb-10">@ViewBag.Title</h2>
@if (!Model.Any())
{
<div class="text-muted mb-10">Search for records...</div>
}
<form method="get" class="mt-10">
<div class="d-flex flex-column flex-md-row">
<div class="input-group">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientNameInput" name="clientName" value="@ViewBag.ClientName" placeholder="Client Name">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
</button>
</div>
</div>
<div class="input-group mr-md-5 ml-md-10 mt-10 mb-5 mt-md-0 mb-md-0">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientGuidInput" name="clientGuid" value="@ViewBag.ClientGuid" placeholder="Client GUID">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
</button>
</div>
</div>
<div class="input-group mr-md-10 ml-md-5 mb-10 mt-5 mt-md-0 mb-md-0">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIPInput" name="clientIP" value="@ViewBag.ClientIP" placeholder="Client IP">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
</button>
</div>
</div>
<div class="input-group">
<input type="number" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIdInput" name="clientId" value="@ViewBag.ClientId" placeholder="Client Id">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
</button>
</div>
</div>
</div>
</form>
<div id="ban_entry_list">
<partial name="_BanEntries" for="@Model"/>
</div>
@if (Model.Any())
{
<div class="w-full text-center">
<i id="loaderLoad" class="oi oi-chevron-bottom mt-10 loader-load-more text-primary m-auto" aria-hidden="true"></i>
</div>
}
</div>
@section scripts {
<script>
initLoader('/Admin/BanManagementList', '#ban_entry_list', 10, 10, [{ 'name': 'clientIP', 'value': () => $('#clientIPInput').val() },
{ 'name': 'clientGuid', 'value': () => $('#clientGuidInput').val() },
{ 'name': 'clientName', 'value': () => $('#clientNameInput').val() },
{ 'name': 'clientId', 'value': () => $('#clientIdInput').val() }]);
</script>
}

View File

@ -0,0 +1,63 @@
@model IEnumerable<WebfrontCore.QueryHelpers.Models.BanInfo>
@foreach (var ban in Model)
{
if (ban.AttachedPenalty is null && !ban.AssociatedPenalties.Any())
{
continue;
}
<div class="card p-10 m-0 mt-15 mb-15">
<div class="d-flex flex-row flex-wrap">
<div class="d-flex p-15 mr-md-10 w-full w-md-200 bg-very-dark-dm bg-light-ex-lm rounded">
<div class="align-self-center ">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@ban.ClientId" class="font-size-18 no-decoration">@ban.ClientName</a>
<has-permission entity="ClientGuid" required-permission="Read">
<div class="text-muted">@ban.NetworkId.ToString("X")</div>
</has-permission>
<has-permission entity="ClientIPAddress" required-permission="Read">
<div class="text-muted">@ban.IPAddress.ConvertIPtoString()</div>
</has-permission>
@if (ban.AttachedPenalty is not null)
{
<br/>
<div class="text-muted font-weight-light">@ban.AttachedPenalty.Offense.CapClientName(30)</div>
<div class="text-danger font-weight-light">@ban.AttachedPenalty.DateTime.ToStandardFormat()</div>
<div class="btn profile-action mt-10 w-100" data-action="unban" data-action-id="@ban.ClientId">Unban</div>
}
else
{
<br/>
<div class="align-self-end text-muted font-weight-light">
<span class="oi oi-warning font-size-12"></span>
<span>Link-Only Ban</span>
</div>
}
</div>
</div>
@foreach (var associatedEntity in ban.AssociatedPenalties)
{
<div class="d-flex flex-wrap flex-column w-full w-md-200 p-10">
<div data-toggle="tooltip" data-title="Linked via shared IP" class="d-flex">
<i class="oi oi-link-intact align-self-center"></i>
<div class="text-truncate ml-5 mr-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@associatedEntity.OffenderInfo.ClientId" class="font-size-18 no-decoration">@associatedEntity.OffenderInfo.ClientName</a>
</div>
</div>
<has-permission entity="ClientGuid" required-permission="Read">
<div class="text-muted">@associatedEntity.OffenderInfo.NetworkId?.ToString("X")</div>
</has-permission>
<has-permission entity="ClientIPAddress" required-permission="Read">
<div class="text-muted">@associatedEntity.OffenderInfo.IPAddress.ConvertIPtoString()</div>
</has-permission>
<br/>
<div class="text-muted font-weight-light">@associatedEntity.Offense.CapClientName(30)</div>
<div class="text-danger font-weight-light">@associatedEntity.DateTime.ToStandardFormat()</div>
<div class="btn profile-action mt-10 w-100" data-action="unban" data-action-id="@associatedEntity.OffenderInfo.ClientId">Unban</div>
</div>
}
</div>
</div>
}

View File

@ -5,10 +5,11 @@
@foreach (var key in Model.Keys)
{
<table class="table mb-20">
<table class="table mb-20" style="table-layout:fixed;">
<thead>
<tr class="level-bgcolor-@((int)key)">
<th class="text-light">@key.ToLocalizedLevelName()</th>
<th>Game</th>
<th class="text-right font-weight-bold">Last Connected</th>
</tr>
</thead>
@ -33,6 +34,9 @@
<color-code value="@client.Name"></color-code>
</a>
</td>
<td>
<div class="badge">@ViewBag.Localization[$"GAME_{client.Game}"]</div>
</td>
<td class="text-right">
@client.LastConnection.HumanizeForCurrentCulture()
</td>

View File

@ -32,7 +32,7 @@
}
<div class="content row mt-20">
<div class="col-12 col-lg-9 col-xl-10">
<div class="col-12 col-lg-9">
@if (Model.ActivePenalty != null)
{
<has-permission entity="ClientLevel" required-permission="Read">
@ -58,7 +58,8 @@
</has-permission>
}
<h2 class="content-title mb-10">Player Profile</h2>
<h2 class="content-title mb-0">Player Profile</h2>
<div class="font-size-12 text-muted">@ViewBag.Localization[$"GAME_{Model.Game}"]</div>
<div id="profile_wrapper" class="mb-10 mt-10">

View File

@ -232,7 +232,7 @@
<div class="content row mt-20">
<!-- main content -->
<div class="col-12 col-lg-9 col-xl-10 mt-0">
<div class="col-12 col-lg-9 mt-0">
<h2 class="content-title mb-0">Player Stats</h2>
<span class="text-muted">
<color-code value="@(Model.Servers.FirstOrDefault(server => server.Endpoint == Model.ServerEndpoint)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
@ -256,7 +256,7 @@
{
if (Model.Ranking > 0)
{
<div class="h5 mb-0">@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_RANKED"] as string).FormatExt(Model.Ranking))</div>
<div class="h5 mb-0">@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_RANKED_V2"] as string).FormatExt(Model.Ranking?.ToString("#,##0"), Model.TotalRankedClients.ToString("#,##0")))</div>
}
else

View File

@ -2,10 +2,11 @@
@using WebfrontCore.ViewModels
<div class="content mt-20 row">
<div class="col-12 col-lg-9 col-xl-10 mt-0">
<div class="col-12 col-lg-9 mt-0">
<h2 class="content-title mb-0">Top Players</h2>
<span class="text-muted">
<color-code value="@(ViewBag.SelectedServerName ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
<color-code value="@(Model.FirstOrDefault(m => m.Endpoint == ViewBag.SelectedServerId)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
&mdash; <span class="text-primary">@ViewBag.TotalRankedClients.ToString("#,##0")</span> Ranked Players
</span>
<div id="topPlayersContainer">
@ -42,6 +43,7 @@
{
<environment include="Development">
<script type="text/javascript" src="~/js/stats.js"></script>
<script type="text/javascript" src="~/lib/canvas.js/canvasjs.js"></script>
</environment>
<script>initLoader('/Stats/GetTopPlayersAsync', '#topPlayersContainer', 25);</script>
<script>initLoader('/Stats/GetTopPlayersAsync', '#topPlayersContainer', 25, 25, [{ 'name': 'serverId', 'value' : () => @(ViewBag.ServerId ?? 0) }]);</script>
}

View File

@ -11,7 +11,7 @@
}
}
<div class="content mt-20 row">
<div class="col-12 col-lg-9 col-xl-10">
<div class="col-12 col-lg-9">
<h2 class="content-title mb-0">Server Overview</h2>
@if (Model.Game.HasValue)
{

View File

@ -13,7 +13,7 @@
</style>
<div class="content mt-20 row">
<div class="col-12 col-lg-9 col-xl-10">
<div class="col-12 col-lg-9">
<h2 class="content-title mb-0">Live Radar</h2>
<div class="text-muted mb-15">
<color-code value="@((Model.FirstOrDefault(server => server.Endpoint == ViewBag.SelectedServerId) ?? Model.First()).Name)"></color-code>

View File

@ -5,7 +5,7 @@
}
<div class="content mt-20 row">
<div class="col-12 col-lg-9 col-xl-10">
<div class="col-12 col-lg-9">
@if (Model is not null)
{
<div class=" scoreboard-container" data-server-id="@ViewBag.SelectedServerId">

View File

@ -22,10 +22,10 @@
}
}
<div class="pt-15 pl-15 pr-15 d-flex flex-wrap flex-column flex-md-row justify-content-between w-full w-auto-lg">
<div class="pt-15 pl-15 pr-15 d-flex flex-wrap flex-column flex-md-row w-full w-auto-lg">
@if (groupedClients.Count > 0)
{
<div class="flex-fill flex-lg-grow-0 w-full w-md-half pr-md-10 pb-md-10">
<div class="flex-fill flex-lg-grow-0 w-full w-md-half">
@foreach (var chat in Model.ChatHistory)
{
var message = chat.IsHidden && !ViewBag.Authorized ? chat.HiddenMessage : chat.Message;
@ -33,7 +33,6 @@
<div class="text-truncate">
<i class="oi @stateIcon"></i>
<span>
<color-code value="@chat.Name"></color-code>
</span>
@ -47,27 +46,25 @@
</span>
}
</div>
}
<hr class="d-block d-md-none"/>
</div>
}
<div class="d-flex flex-row w-full w-md-half pl-md-10 pb-md-10">
<div class="d-flex flex-row w-full w-md-half">
@foreach (var clientIndex in groupedClients)
{
<div class="@(clientIndex.index == 1 ? "pl-md-10" : "pr-md-10") w-half w-xl-full">
<div class="w-half">
@foreach (var client in clientIndex.group)
{
var levelColorClass = !ViewBag.Authorized || client.client.LevelInt == 0 ? "text-light-dm text-dark-lm" : $"level-color-{client.client.LevelInt}";
<div class="d-flex @(clientIndex.index == 1 ? "justify-content-start ml-auto flex-row-reverse" : "ml-auto") w-full w-xl-200">
<div class="d-flex @(clientIndex.index == 1 ? "justify-content-start flex-row-reverse" : "")">
<has-permission entity="AdminMenu" required-permission="Update">
<a href="#actionModal" class="profile-action" data-action="kick" data-action-id="@client.client.ClientId" aria-hidden="true">
<i class="oi oi-circle-x font-size-12 @levelColorClass @(clientIndex.index == 1 ? "ml-5" : "mr-5")"></i>
<i class="oi oi-circle-x font-size-12 @levelColorClass"></i>
</a>
</has-permission>
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.client.ClientId" class="@levelColorClass no-decoration text-truncate">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.client.ClientId" class="@levelColorClass no-decoration text-truncate ml-5 mr-5">
<color-code value="@client.client.Name"></color-code>
</a>
</div>

View File

@ -15,20 +15,23 @@
foreach (var snapshot in Model.ClientHistory.ClientCounts)
{
snapshot.MapAlias = GetMapName(snapshot.Map);
}
};
string MakeAbbreviation(string gameName) => string.Join("", gameName.Split(' ').Select(word => char.ToUpper(word.First())).ToArray());
}
<div class="card mt-20 mb-20 ml-0 mr-0 p-0">
<div class="p-5 pl-10 pr-10 bg-primary rounded-top d-flex flex-column flex-md-row flex-wrap justify-content-between text-light" id="server_header_@Model.ID">
<div class="d-flex align-self-center flex-column-reverse flex-md-row">
<div class="ml-5 mr-5 text-center">
<div class="p-5 pl-10 pr-10 bg-primary rounded-top d-flex flex-column flex-lg-row flex-wrap justify-content-between text-light" id="server_header_@Model.ID">
<!-- first column -->
<div class="d-flex align-self-center flex-column-reverse flex-lg-row col-12 col-lg-6">
<div class="ml-5 mr-5 text-center text-lg-left">
<color-code value="@Model.Name"></color-code>
<div class="server-header-ip-address font-weight-light" style="display:none">@(Model.ExternalIPAddress):@(Model.Port)</div>
</div>
<div class="d-flex justify-content-center">
<!-- connect button -->
<a href="@Model.ConnectProtocolUrl" class="text-light align-self-center" title="@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_HOME_JOIN_DESC"]">
<a href="@Model.ConnectProtocolUrl" class="text-light align-self-center server-join-button" title="@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_HOME_JOIN_DESC"]">
<i class="oi oi-play-circle ml-5 mr-5"></i>
<span class="server-header-ip-address" style="display:none;">@(Model.ExternalIPAddress):@(Model.Port)</span>
</a>
<has-permission entity="AdminMenu" required-permission="Update">
<!-- send message button -->
@ -41,17 +44,21 @@
class="text-light align-self-center">
<i class="oi oi-spreadsheet ml-5 mr-5"></i>
</a>
<span class="ml-5 mr-5 text-light badge font-weight-light" data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{Model.Game}"]">@MakeAbbreviation(ViewBag.Localization[$"GAME_{Model.Game}"])</span>
</div>
</div>
<div class="align-self-center">
<!-- second column -->
<div class="col-12 align-self-center text-center text-lg-left col-lg-4">
<span>@Model.Map</span>
@if (!string.IsNullOrEmpty(Model.GameType) && Model.GameType.Length > 1)
{
<span>&ndash;</span>
<span>@Model.GameType</span>
}
</div>
<div class="align-self-center d-flex flex-column flex-md-row">
<!-- third column -->
<div class="align-self-center d-flex flex-column flex-lg-row col-12 col-lg-2 justify-content-end">
@if (Model.LobbyZScore != null)
{
<div data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_HOME_RATING_DESC"]" class="cursor-help d-flex flex-row-reverse flex-md-row justify-content-center">

View File

@ -55,10 +55,10 @@
}
start++;
<div class="profile-meta-entry loader-data-time" data-time="@meta.When.ToFileTimeUtc()" onclick="$('#metaContextDateToggle@(start)').show()">
<div class="profile-meta-entry loader-data-time" data-time="@meta.When.ToFileTimeUtc()" onclick="$('#metaContextDateToggle@(meta.When.ToFileTimeUtc())').show()">
<partial name="~/Views/Client/Profile/Meta/_@(meta.GetType().Name).cshtml" model="meta"/>
<div style="display:none" id="metaContextDateToggle@(start)">
Event occured at <span class="text-light">@meta.When.ToString()</span>
<div style="display:none" id="metaContextDateToggle@(meta.When.ToFileTimeUtc())">
Event occured at <span class="text-light">@meta.When.ToStandardFormat()</span>
</div>
</div>
}

View File

@ -1,4 +1,5 @@
@model WebfrontCore.ViewModels.TableInfo
@using WebfrontCore.ViewModels
@model WebfrontCore.ViewModels.TableInfo
@{
Layout = null;
}
@ -38,7 +39,32 @@
<tr class="bg-dark-dm bg-light-lm @(Model.InitialRowCount > 0 && start >= Model.InitialRowCount ? "d-none hidden-row-lg" : "d-none d-lg-table-row")">
@for (var i = 0; i < Model.Columns.Count; i++)
{
<td>@row.Datum[i]</td>
var data = row.Datum[i];
<td>
@if (data.Template is null)
{
if (data.Type == ColumnType.Text)
{
<span>@data.Value</span>
}
if (data.Type == ColumnType.Link)
{
<a href="@data.Data" class="no-decoration">@data.Value</a>
}
if (data.Type == ColumnType.Icon)
{
<span class="oi @data.Value profile-action" data-action="@data.Data" data-action-id="@data.Id"></span>
}
if (data.Type == ColumnType.Button)
{
<div class="btn profile-action" data-action="@data.Data" data-action-id="@data.Id">@data.Value</div>
}
}
else
{
@data.Template
}
</td>
}
</tr>
@ -53,7 +79,21 @@
<td class="bg-dark-dm bg-light-lm flex-fill w-200">
@for (var i = 0; i < Model.Columns.Count; i++)
{
<div class="mt-5 mb-5 text-truncate" style="min-width:0">@row.Datum[i]</div>
var data = row.Datum[i];
<div class="mt-5 mb-5 text-truncate" style="min-width:0">
@if (data.Type == ColumnType.Text)
{
<span>@data.Value</span>
}
@if (data.Type == ColumnType.Link)
{
<a href="@data.Data">@data.Value</a>
}
@if (data.Type == ColumnType.Icon)
{
<span class="oi @data.Value profile-action" data-action="@data.Data" data-action-id="@data.Id"></span>
}
</div>
}
</td>
</tr>

View File

@ -115,6 +115,12 @@
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_CONSOLE"]</span>
</a>
</has-permission>
<has-permission entity="Penalty" required-permission="Read"></has-permission>
<a asp-controller="Admin" asp-action="BanManagement" class="sidebar-link">
<i class="oi oi-ban mr-5"></i>
<span class="name">Ban Management</span>
</a>
</has-permission>
@if (ViewBag.User.Level >= EFClient.Permission.Owner)
{
<a asp-controller="Configuration" asp-action="Edit" class="sidebar-link">
@ -138,7 +144,6 @@
<i class="oi oi-key mr-5"></i>
<span class="name">@ViewBag.Localization["WEBFRONT_ACTION_TOKEN"]</span>
</a>
</has-permission>
@if (ViewBag.Authorized)
{
<a asp-controller="Account" asp-action="Logout" class="sidebar-link">

View File

@ -1,7 +1,7 @@
@model WebfrontCore.ViewModels.SideContextMenuItems
@{ Layout = null; }
<div class="d-none d-lg-flex col-3 col-xl-2">
<div class="d-none d-lg-flex col-3">
<div class="content mt-0">
<div class="on-this-page-nav pt-0">
<div class="title">@Model.MenuTitle</div>

View File

@ -56,7 +56,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="ref" />
<Folder Include="wwwroot\lib\canvas.js\" />
</ItemGroup>

View File

@ -150,7 +150,7 @@ function refreshClientActivity(serverId) {
$(document).ready(function () {
$('.server-join-button').click(function (e) {
$(this).children('.server-header-ip-address').show();
$(this).parent().parent().find('.server-header-ip-address').show();
});
$('.server-history-row').each(function (index, element) {