Compare commits

..

10 Commits

21 changed files with 380 additions and 119 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

@ -792,8 +792,16 @@ namespace IW4MAdmin
/// <returns></returns>
async Task<List<EFClient>[]> PollPlayersAsync()
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
var currentClients = GetClientsAsList();
var statusResponse = await this.GetStatusAsync(Manager.CancellationToken);
var statusResponse = await this.GetStatusAsync(tokenSource.Token);
if (statusResponse is null)
{
return null;
}
var polledClients = statusResponse.Clients.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
@ -930,6 +938,11 @@ namespace IW4MAdmin
var polledClients = await PollPlayersAsync();
if (polledClients is null)
{
return true;
}
foreach (var disconnectingClient in polledClients[1]
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
{

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>();
@ -410,7 +411,7 @@ namespace IW4MAdmin.Application.Misc
_scriptEngine.SetValue("_event", gameEvent);
var jsEventObject = _scriptEngine.Evaluate("_event");
dynamicCommand.execute.Target.Invoke(_scriptEngine, jsEventObject);
}
@ -424,7 +425,7 @@ namespace IW4MAdmin.Application.Misc
throw new PluginException("A runtime error occured while executing action for script plugin");
}
catch (Exception ex)
{
using (LogContext.PushProperty("Server", gameEvent.Owner?.ToString()))
@ -454,83 +455,71 @@ namespace IW4MAdmin.Application.Misc
return commandList;
}
private void GetDvarAsync(Server server, string dvarName, Delegate onCompleted)
private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
{
Task.Run(() =>
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
server.BeginGetDvar(dvarName, result =>
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
string result = null;
var success = true;
var shouldRelease = false;
try
{
result = server.GetDvarAsync<string>(dvarName, token: tokenSource.Token).GetAwaiter().GetResult().Value;
}
catch
{
success = false;
}
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
_onProcessing.Wait();
try
{
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, result),
JsValue.FromObject(_scriptEngine, success),
});
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release();
}
}
});
}
private void SetDvarAsync(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{
Task.Run(() =>
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
var success = true;
try
{
server.SetDvarAsync(dvarName, dvarValue, tokenSource.Token).GetAwaiter().GetResult();
}
catch
{
success = false;
}
_onProcessing.Wait();
try
{
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, dvarValue),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, value),
JsValue.FromObject(_scriptEngine, success)
});
}
finally
{
if (_onProcessing.CurrentCount == 0)
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release();
}
}
});
}, tokenSource.Token);
}
private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
server.BeginSetDvar(dvarName, dvarValue, result =>
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
var success = (bool)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, dvarValue),
JsValue.FromObject(_scriptEngine, success)
});
}
finally
{
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release();
}
}
}, tokenSource.Token);
}
}

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

@ -53,6 +53,7 @@ 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
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"

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

@ -633,32 +633,44 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return;
}
var hit = new EFClientKill()
EFClientKill hit;
try
{
Active = true,
AttackerId = attacker.ClientId,
VictimId = victim.ClientId,
ServerId = serverId,
DeathOrigin = vDeathOrigin,
KillOrigin = vKillOrigin,
DeathType = (int) ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
Damage = int.Parse(damage),
HitLoc = (int) ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
WeaponReference = weapon,
ViewAngles = vViewAngles,
TimeOffset = long.Parse(offset),
When = time,
IsKillstreakKill = isKillstreakKill[0] != '0',
AdsPercent = float.Parse(Ads, System.Globalization.CultureInfo.InvariantCulture),
Fraction = double.Parse(fraction, System.Globalization.CultureInfo.InvariantCulture),
VisibilityPercentage = double.Parse(visibilityPercentage,
System.Globalization.CultureInfo.InvariantCulture),
IsKill = !isDamage,
AnglesList = snapshotAngles,
IsAlive = isAlive == "1",
TimeSinceLastAttack = long.Parse(lastAttackTime),
GameName = (int) attacker.CurrentServer.GameName
};
hit = new EFClientKill
{
Active = true,
AttackerId = attacker.ClientId,
VictimId = victim.ClientId,
ServerId = serverId,
DeathOrigin = vDeathOrigin,
KillOrigin = vKillOrigin,
DeathType = (int) ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
Damage = int.Parse(damage),
HitLoc = (int) ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
WeaponReference = weapon,
ViewAngles = vViewAngles,
TimeOffset = long.Parse(offset),
When = time,
IsKillstreakKill = isKillstreakKill[0] != '0',
AdsPercent = float.Parse(Ads, System.Globalization.CultureInfo.InvariantCulture),
Fraction = double.Parse(fraction, System.Globalization.CultureInfo.InvariantCulture),
VisibilityPercentage = double.Parse(visibilityPercentage,
System.Globalization.CultureInfo.InvariantCulture),
IsKill = !isDamage,
AnglesList = snapshotAngles,
IsAlive = isAlive == "1",
TimeSinceLastAttack = long.Parse(lastAttackTime),
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);
@ -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;
}
@ -826,6 +838,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (KeyNotFoundException)
{
}
try
{
if (!gameDetectionTypes[server.GameName].Contains(detectionType))
{
return false;
}
}
catch
{
// ignored
}
return true;
}

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;
@ -35,4 +35,4 @@ namespace SharedLibraryCore.Dtos
Ascending,
Descending
}
}
}

View File

@ -55,6 +55,8 @@ namespace SharedLibraryCore.Interfaces
/// <param name="token"></param>
/// <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
@ -65,6 +67,8 @@ namespace SharedLibraryCore.Interfaces
/// <param name="token"></param>
/// <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

View File

@ -773,6 +773,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)
@ -808,6 +813,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)
{
@ -824,9 +835,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

@ -0,0 +1,96 @@
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
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 > 30)
{
query.Count = 30;
}
await using var context = _contextFactory.CreateContext(false);
var matchingClients = await context.Clients.Where(client =>
EF.Functions.ILike(client.CurrentAlias.SearchableName ?? client.CurrentAlias.Name, $"%{query.ClientName.Trim()}%"))
.Where(client => client.Level == EFClient.Permission.Banned)
.OrderByDescending(client => client.LastConnection)
.Skip(query.Offset)
.Take(query.Count)
.Select(client => new
{
client.CurrentAlias.Name,
client.NetworkId,
client.AliasLinkId,
client.ClientId
}).ToListAsync();
var usedIps = await context.Aliases
.Where(alias => matchingClients.Select(client => client.AliasLinkId).Contains(alias.LinkId))
.Where(alias => alias.IPAddress != null)
.Select(alias => new { alias.IPAddress, alias.LinkId })
.ToListAsync();
var usedIpsGrouped = usedIps
.GroupBy(alias => alias.LinkId)
.ToDictionary(key => key.Key, value => value.Select(alias => alias.IPAddress).Distinct());
var searchingNetworkIds = matchingClients.Select(client => client.NetworkId);
var searchingIps = usedIpsGrouped.SelectMany(group => group.Value);
var matchedPenalties = await context.PenaltyIdentifiers.Where(identifier =>
searchingNetworkIds.Contains(identifier.NetworkId) ||
searchingIps.Contains(identifier.IPv4Address))
.Select(penalty => new
{
penalty.CreatedDateTime,
PunisherName = penalty.Penalty.Punisher.CurrentAlias.Name,
Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense) ? penalty.Penalty.Offense : "Anticheat Detection",
LinkId = penalty.Penalty.Offender.AliasLinkId,
penalty.Penalty.PunisherId
})
.ToListAsync();
var groupedPenalties = matchedPenalties.GroupBy(penalty => penalty.LinkId)
.ToDictionary(key => key.Key, value => value.FirstOrDefault());
var results = matchingClients.Select(client =>
{
var matchedPenalty =
groupedPenalties.ContainsKey(client.AliasLinkId) ? groupedPenalties[client.AliasLinkId] : null;
return new BanInfo
{
DateTime = matchedPenalty?.CreatedDateTime,
OffenderName = client.Name.StripColors(),
OffenderId = client.ClientId,
PunisherName = matchedPenalty?.PunisherName.StripColors(),
PunisherId = matchedPenalty?.PunisherId,
Offense = matchedPenalty?.Offense
};
}).ToList();
return new ResourceQueryHelperResult<BanInfo>
{
RetrievedResultCount = results.Count,
TotalResultCount = results.Count,
Results = results
};
}
}

View File

@ -0,0 +1,12 @@
using System;
public class BanInfo
{
public string OffenderName { get; set; }
public int OffenderId { get; set; }
public string PunisherName { get; set; }
public int? PunisherId { 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;
}

View File

@ -0,0 +1,8 @@
using SharedLibraryCore.Dtos;
namespace WebfrontCore.QueryHelpers.Models;
public class BanInfoRequest : PaginationRequest
{
public string ClientName { 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

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

@ -5,7 +5,7 @@
<div class="col-12 col-lg-9 col-xl-10 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>
</span>
<div id="topPlayersContainer">

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,36 +46,34 @@
</span>
}
</div>
}
<hr class="d-block d-md-none"/>
<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>
}
</div>
}
@if (groupedClients.Count > 0)
{
<br/>
}
@if (groupedClients.Count > 0)
{
<br/>
}
</div>
</div>

View File

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