Compare commits
15 Commits
2022.07.13
...
2022.07.23
Author | SHA1 | Date | |
---|---|---|---|
444c06e65e | |||
561909158f | |||
cd12c3f26e | |||
c817f9a810 | |||
b27ae1517e | |||
507688a175 | |||
d2cfd50e39 | |||
51e8b31e42 | |||
fa1567d3f5 | |||
f97e266c24 | |||
506b17dbb3 | |||
bef8c08d90 | |||
b78c467539 | |||
c3e042521a | |||
cb5f490d3b |
52
Application/Commands/AddClientNoteCommand.cs
Normal file
52
Application/Commands/AddClientNoteCommand.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models.Client;
|
||||
using IW4MAdmin.Application.Meta;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Application.Commands;
|
||||
|
||||
public class AddClientNoteCommand : Command
|
||||
{
|
||||
private readonly IMetaServiceV2 _metaService;
|
||||
|
||||
public AddClientNoteCommand(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) : base(config, layout)
|
||||
{
|
||||
Name = "addnote";
|
||||
Description = _translationLookup["COMMANDS_ADD_CLIENT_NOTE_DESCRIPTION"];
|
||||
Alias = "an";
|
||||
Permission = EFClient.Permission.Moderator;
|
||||
RequiresTarget = true;
|
||||
Arguments = new[]
|
||||
{
|
||||
new CommandArgument
|
||||
{
|
||||
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
|
||||
Required = true
|
||||
},
|
||||
new CommandArgument
|
||||
{
|
||||
Name = _translationLookup["COMMANDS_ARGS_NOTE"],
|
||||
Required = false
|
||||
}
|
||||
};
|
||||
|
||||
_metaService = metaService;
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||
{
|
||||
var note = new ClientNoteMetaResponse
|
||||
{
|
||||
Note = gameEvent.Data?.Trim(),
|
||||
OriginEntityId = gameEvent.Origin.ClientId,
|
||||
ModifiedDate = DateTime.UtcNow
|
||||
};
|
||||
await _metaService.SetPersistentMetaValue("ClientNotes", note, gameEvent.Target.ClientId);
|
||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_ADD_CLIENT_NOTE_SUCCESS"]);
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ namespace IW4MAdmin.Application.Commands
|
||||
Name = "readmessage";
|
||||
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
|
||||
Alias = "rm";
|
||||
Permission = EFClient.Permission.Flagged;
|
||||
Permission = EFClient.Permission.User;
|
||||
|
||||
_contextFactory = contextFactory;
|
||||
_logger = logger;
|
||||
@ -76,4 +76,4 @@ namespace IW4MAdmin.Application.Commands
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@ -771,17 +772,28 @@ namespace IW4MAdmin
|
||||
|
||||
else if (E.Type == GameEvent.EventType.MetaUpdated)
|
||||
{
|
||||
if (E.Extra is "PersistentStatClientId" && int.TryParse(E.Data, out var persistentClientId))
|
||||
if (E.Extra is "PersistentClientGuid")
|
||||
{
|
||||
var penalties = await Manager.GetPenaltyService().GetActivePenaltiesByClientId(persistentClientId);
|
||||
var banPenalty = penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
|
||||
var parts = E.Data.Split(",");
|
||||
|
||||
if (banPenalty is not null && E.Origin.Level != Permission.Banned)
|
||||
if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
|
||||
int.TryParse(parts[1], out var low))
|
||||
{
|
||||
ServerLogger.LogInformation(
|
||||
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
|
||||
E.Origin.ToString(), persistentClientId);
|
||||
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(persistentClientId), Utilities.IW4MAdminClient(this), true);
|
||||
var guid = long.Parse(high.ToString("X") + low.ToString("X"), NumberStyles.HexNumber);
|
||||
|
||||
var penalties = await Manager.GetPenaltyService()
|
||||
.GetActivePenaltiesByIdentifier(null, guid, (Reference.Game)GameName);
|
||||
var banPenalty =
|
||||
penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
|
||||
|
||||
if (banPenalty is not null && E.Origin.Level != Permission.Banned)
|
||||
{
|
||||
ServerLogger.LogInformation(
|
||||
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
|
||||
E.Origin.ToString(), guid);
|
||||
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(guid),
|
||||
Utilities.IW4MAdminClient(this), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1289,28 +1301,17 @@ namespace IW4MAdmin
|
||||
this.GamePassword = gamePassword.Value;
|
||||
UpdateMap(mapname);
|
||||
|
||||
if (RconParser.CanGenerateLogPath)
|
||||
if (RconParser.CanGenerateLogPath && string.IsNullOrEmpty(ServerConfig.ManualLogPath))
|
||||
{
|
||||
bool needsRestart = false;
|
||||
|
||||
if (logsync.Value == 0)
|
||||
{
|
||||
await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4
|
||||
needsRestart = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(logfile.Value))
|
||||
{
|
||||
logfile.Value = "games_mp.log";
|
||||
await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken);
|
||||
needsRestart = true;
|
||||
}
|
||||
|
||||
if (needsRestart)
|
||||
{
|
||||
// disabling this for the time being
|
||||
/*Logger.WriteWarning("Game log file not properly initialized, restarting map...");
|
||||
await this.ExecuteCommandAsync("map_restart");*/
|
||||
}
|
||||
|
||||
// this DVAR isn't set until the a map is loaded
|
||||
|
@ -33,7 +33,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
builder.Append(header);
|
||||
builder.Append(config.NoticeLineSeparator);
|
||||
// build the reason
|
||||
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense);
|
||||
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense.FormatMessageForEngine(config));
|
||||
|
||||
if (isNewLineSeparator)
|
||||
{
|
||||
@ -117,4 +117,4 @@ namespace IW4MAdmin.Application.Misc
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,6 @@ namespace Data.Models.Client.Stats
|
||||
{
|
||||
public class EFClientRankingHistory: AuditFields
|
||||
{
|
||||
public const int MaxRankingCount = 1728;
|
||||
|
||||
[Key]
|
||||
public long ClientRankingHistoryId { get; set; }
|
||||
|
||||
|
@ -29,8 +29,9 @@ onPlayerConnect( player )
|
||||
for( ;; )
|
||||
{
|
||||
level waittill( "connected", player );
|
||||
player setClientDvar("cl_autorecord", 1);
|
||||
player setClientDvar("cl_demosKeep", 200);
|
||||
player setClientDvars( "cl_autorecord", 1,
|
||||
"cl_demosKeep", 200 );
|
||||
|
||||
player thread waitForFrameThread();
|
||||
player thread waitForAttack();
|
||||
}
|
||||
@ -60,7 +61,7 @@ getHttpString( url )
|
||||
|
||||
runRadarUpdates()
|
||||
{
|
||||
interval = int(getDvar("sv_printradar_updateinterval"));
|
||||
interval = getDvarInt( "sv_printradar_updateinterval", 500 );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -191,7 +192,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
|
||||
i++;
|
||||
}
|
||||
|
||||
lastAttack = int(getTime()) - int(self.lastAttackTime);
|
||||
lastAttack = getTime() - self.lastAttackTime;
|
||||
isAlive = isAlive(self);
|
||||
|
||||
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
||||
|
@ -53,7 +53,7 @@ waitForAttack()
|
||||
|
||||
runRadarUpdates()
|
||||
{
|
||||
interval = int(getDvar("sv_printradar_updateinterval"));
|
||||
interval = getDvarInt( "sv_printradar_updateinterval", 500 );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -183,7 +183,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
|
||||
i++;
|
||||
}
|
||||
|
||||
lastAttack = int(getTime()) - int(self.lastAttackTime);
|
||||
lastAttack = getTime() - self.lastAttackTime;
|
||||
isAlive = isAlive(self);
|
||||
|
||||
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
||||
|
@ -60,7 +60,7 @@ waitForAttack()
|
||||
|
||||
runRadarUpdates()
|
||||
{
|
||||
interval = int(getDvar("sv_printradar_updateinterval"));
|
||||
interval = getDvarInt( "sv_printradar_updateinterval" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -190,7 +190,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
|
||||
i++;
|
||||
}
|
||||
|
||||
lastAttack = int(getTime()) - int(self.lastAttackTime);
|
||||
lastAttack = getTime() - self.lastAttackTime;
|
||||
isAlive = isAlive(self);
|
||||
|
||||
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
||||
|
@ -59,7 +59,7 @@ init()
|
||||
|
||||
OnPlayerConnect()
|
||||
{
|
||||
level endon ( "disconnect" );
|
||||
level endon ( "game_ended" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -67,7 +67,7 @@ OnPlayerConnect()
|
||||
|
||||
level.iw4adminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" );
|
||||
|
||||
if ( isDefined(player.pers["isBot"]) && player.pers["isBot"] )
|
||||
if ( isDefined( player.pers["isBot"] ) && player.pers["isBot"] )
|
||||
{
|
||||
// we don't want to track bots
|
||||
continue;
|
||||
@ -104,7 +104,7 @@ OnPlayerSpawned()
|
||||
|
||||
OnPlayerDisconnect()
|
||||
{
|
||||
level endon ( "disconnect" );
|
||||
self endon ( "disconnect" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -139,8 +139,6 @@ OnPlayerJoinedSpectators()
|
||||
|
||||
OnGameEnded()
|
||||
{
|
||||
level endon ( "disconnect" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
level waittill( "game_ended" );
|
||||
@ -167,24 +165,29 @@ DisplayWelcomeData()
|
||||
|
||||
SetPersistentData()
|
||||
{
|
||||
storedClientId = self GetPlayerData( "bests", "none" );
|
||||
guidHigh = self GetPlayerData( "bests", "none" );
|
||||
guidLow = self GetPlayerData( "awards", "none" );
|
||||
persistentGuid = guidHigh + "," + guidLow;
|
||||
|
||||
if ( storedClientId != 0 )
|
||||
if ( guidHigh != 0 && guidLow != 0)
|
||||
{
|
||||
if ( level.iw4adminIntegrationDebug == 1 )
|
||||
{
|
||||
IPrintLn( "Uploading persistent client id " + storedClientId );
|
||||
IPrintLn( "Uploading persistent guid " + persistentGuid );
|
||||
}
|
||||
|
||||
SetClientMeta( "PersistentStatClientId", storedClientId );
|
||||
SetClientMeta( "PersistentClientGuid", persistentGuid );
|
||||
}
|
||||
|
||||
if ( level.iw4adminIntegrationDebug == 1 )
|
||||
{
|
||||
IPrintLn( "Persisting client id " + self.persistentClientId );
|
||||
IPrintLn( "Persisting client guid " + persistentGuid );
|
||||
}
|
||||
|
||||
self SetPlayerData( "bests", "none", int( self.persistentClientId ) );
|
||||
guid = self SplitGuid();
|
||||
|
||||
self SetPlayerData( "bests", "none", guid["high"] );
|
||||
self SetPlayerData( "awards", "none", guid["low"] );
|
||||
}
|
||||
|
||||
PlayerConnectEvents()
|
||||
@ -228,8 +231,7 @@ PlayerTrackingOnInterval()
|
||||
|
||||
MonitorClientEvents()
|
||||
{
|
||||
level endon( "disconnect" );
|
||||
self endon( "disconnect" );
|
||||
level endon( "game_ended" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -324,6 +326,107 @@ DecrementClientMeta( metaKey, decrementValue, clientId )
|
||||
SetClientMeta( metaKey, decrementValue, clientId, "decrement" );
|
||||
}
|
||||
|
||||
SplitGuid()
|
||||
{
|
||||
guid = self GetGuid();
|
||||
|
||||
if ( isDefined( self.guid ) )
|
||||
{
|
||||
guid = self.guid;
|
||||
}
|
||||
|
||||
firstPart = 0;
|
||||
secondPart = 0;
|
||||
stringLength = 17;
|
||||
firstPartExp = 0;
|
||||
secondPartExp = 0;
|
||||
|
||||
for ( i = stringLength - 1; i > 0; i-- )
|
||||
{
|
||||
char = GetSubStr( guid, i - 1, i );
|
||||
if ( char == "" )
|
||||
{
|
||||
char = "0";
|
||||
}
|
||||
|
||||
if ( i > stringLength / 2 )
|
||||
{
|
||||
value = GetIntForHexChar( char );
|
||||
power = Pow( 16, secondPartExp );
|
||||
secondPart = secondPart + ( value * power );
|
||||
secondPartExp++;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = GetIntForHexChar( char );
|
||||
power = Pow( 16, firstPartExp );
|
||||
firstPart = firstPart + ( value * power );
|
||||
firstPartExp++;
|
||||
}
|
||||
}
|
||||
|
||||
split = [];
|
||||
split["low"] = int( secondPart );
|
||||
split["high"] = int( firstPart );
|
||||
|
||||
return split;
|
||||
}
|
||||
|
||||
Pow( num, exponent )
|
||||
{
|
||||
result = 1;
|
||||
while( exponent != 0 )
|
||||
{
|
||||
result = result * num;
|
||||
exponent--;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
GetIntForHexChar( char )
|
||||
{
|
||||
char = ToLower( char );
|
||||
// generated by co-pilot because I can't be bothered to make it more "elegant"
|
||||
switch( char )
|
||||
{
|
||||
case "0":
|
||||
return 0;
|
||||
case "1":
|
||||
return 1;
|
||||
case "2":
|
||||
return 2;
|
||||
case "3":
|
||||
return 3;
|
||||
case "4":
|
||||
return 4;
|
||||
case "5":
|
||||
return 5;
|
||||
case "6":
|
||||
return 6;
|
||||
case "7":
|
||||
return 7;
|
||||
case "8":
|
||||
return 8;
|
||||
case "9":
|
||||
return 9;
|
||||
case "a":
|
||||
return 10;
|
||||
case "b":
|
||||
return 11;
|
||||
case "c":
|
||||
return 12;
|
||||
case "d":
|
||||
return 13;
|
||||
case "e":
|
||||
return 14;
|
||||
case "f":
|
||||
return 15;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
GenerateJoinTeamString( isSpectator )
|
||||
{
|
||||
team = self.team;
|
||||
@ -476,7 +579,7 @@ MonitorBus()
|
||||
|
||||
QueueEvent( request, eventType, notifyEntity )
|
||||
{
|
||||
level endon( "disconnect" );
|
||||
level endon( "game_ended" );
|
||||
|
||||
start = GetTime();
|
||||
maxWait = level.eventBus.timeout * 1000; // 30 seconds
|
||||
@ -510,6 +613,8 @@ QueueEvent( request, eventType, notifyEntity )
|
||||
{
|
||||
notifyEntity NotifyClientEventTimeout( eventType );
|
||||
}
|
||||
|
||||
SetDvar( level.eventBus.inVar, "" );
|
||||
|
||||
return;
|
||||
}
|
||||
@ -923,7 +1028,7 @@ GotoCoordImpl( data )
|
||||
return;
|
||||
}
|
||||
|
||||
position = ( int(data["x"]), int(data["y"]), int(data["z"]) );
|
||||
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
|
||||
self SetOrigin( position );
|
||||
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
|
||||
}
|
||||
|
@ -518,8 +518,8 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
|
||||
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}, Direction=${event.data['direction']} ${token}`);
|
||||
if (event.data['direction'] != null) {
|
||||
event.data['direction'] = 'up'
|
||||
? metaService.IncrementPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult()
|
||||
: metaService.DecrementPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult();
|
||||
? metaService.IncrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult()
|
||||
: metaService.DecrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult();
|
||||
} else {
|
||||
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult();
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ namespace Stats.Helpers
|
||||
.Where(r => r.ClientId == clientInfo.ClientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
.Where(r => r.Ranking != null)
|
||||
.OrderByDescending(r => r.UpdatedDateTime)
|
||||
.OrderByDescending(r => r.CreatedDateTime)
|
||||
.Take(250)
|
||||
.ToListAsync();
|
||||
|
||||
|
@ -86,7 +86,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
public async Task<int> GetClientOverallRanking(int clientId, long? serverId = null)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
|
||||
if (_config.EnableAdvancedMetrics)
|
||||
{
|
||||
var clientRanking = await context.Set<EFClientRankingHistory>()
|
||||
@ -117,7 +117,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null, long? serverId = null)
|
||||
public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null,
|
||||
long? serverId = null)
|
||||
{
|
||||
return (ranking) => ranking.ServerId == serverId
|
||||
&& ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned
|
||||
@ -138,6 +139,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
public class RankingSnapshot
|
||||
{
|
||||
public int ClientId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public DateTime LastConnection { get; set; }
|
||||
public double? PerformanceMetric { get; set; }
|
||||
public double? ZScore { get; set; }
|
||||
public int? Ranking { get; set; }
|
||||
public DateTime CreatedDateTime { get; set; }
|
||||
}
|
||||
|
||||
public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
@ -150,24 +162,38 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.Take(count)
|
||||
.ToListAsync();
|
||||
|
||||
var rankings = await context.Set<EFClientRankingHistory>()
|
||||
.Where(ranking => clientIdsList.Contains(ranking.ClientId))
|
||||
.Where(ranking => ranking.ServerId == serverId)
|
||||
.Select(ranking => new
|
||||
{
|
||||
ranking.ClientId,
|
||||
ranking.Client.CurrentAlias.Name,
|
||||
ranking.Client.LastConnection,
|
||||
ranking.PerformanceMetric,
|
||||
ranking.ZScore,
|
||||
ranking.Ranking,
|
||||
ranking.CreatedDateTime
|
||||
})
|
||||
.ToListAsync();
|
||||
var rankingsDict = new Dictionary<int, List<RankingSnapshot>>();
|
||||
|
||||
foreach (var clientId in clientIdsList)
|
||||
{
|
||||
var eachRank = await context.Set<EFClientRankingHistory>()
|
||||
.Where(ranking => ranking.ClientId == clientId)
|
||||
.Where(ranking => ranking.ServerId == serverId)
|
||||
.OrderByDescending(ranking => ranking.CreatedDateTime)
|
||||
.Select(ranking => new RankingSnapshot
|
||||
{
|
||||
ClientId = ranking.ClientId,
|
||||
Name = ranking.Client.CurrentAlias.Name,
|
||||
LastConnection = ranking.Client.LastConnection,
|
||||
PerformanceMetric = ranking.PerformanceMetric,
|
||||
ZScore = ranking.ZScore,
|
||||
Ranking = ranking.Ranking,
|
||||
CreatedDateTime = ranking.CreatedDateTime
|
||||
})
|
||||
.Take(60)
|
||||
.ToListAsync();
|
||||
|
||||
if (rankingsDict.ContainsKey(clientId))
|
||||
{
|
||||
rankingsDict[clientId] = rankingsDict[clientId].Concat(eachRank).Distinct()
|
||||
.OrderByDescending(ranking => ranking.CreatedDateTime).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
rankingsDict.Add(clientId, eachRank);
|
||||
}
|
||||
}
|
||||
|
||||
var rankingsDict = rankings.GroupBy(rank => rank.ClientId)
|
||||
.ToDictionary(rank => rank.Key, rank => rank.OrderBy(r => r.CreatedDateTime).ToList());
|
||||
|
||||
var statsInfo = await context.Set<EFClientStatistics>()
|
||||
.Where(stat => clientIdsList.Contains(stat.ClientId))
|
||||
.Where(stat => stat.TimePlayed > 0)
|
||||
@ -179,7 +205,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ClientId = s.Key,
|
||||
Kills = s.Sum(c => c.Kills),
|
||||
Deaths = s.Sum(c => c.Deaths),
|
||||
KDR = s.Sum(c => (c.Kills / (double) (c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
|
||||
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
|
||||
s.Sum(c => c.TimePlayed),
|
||||
TotalTimePlayed = s.Sum(c => c.TimePlayed),
|
||||
UpdatedAt = s.Max(c => c.UpdatedAt)
|
||||
@ -187,7 +213,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.ToListAsync();
|
||||
|
||||
var finished = statsInfo
|
||||
.OrderByDescending(stat => rankingsDict[stat.ClientId].Last().PerformanceMetric)
|
||||
.OrderByDescending(stat => rankingsDict[stat.ClientId].First().PerformanceMetric)
|
||||
.Select((s, index) => new TopStatsInfo
|
||||
{
|
||||
ClientId = s.ClientId,
|
||||
@ -195,24 +221,23 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
Deaths = s.Deaths,
|
||||
Kills = s.Kills,
|
||||
KDR = Math.Round(s.KDR, 2),
|
||||
LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection))
|
||||
LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].First().LastConnection))
|
||||
.HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false),
|
||||
LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection),
|
||||
LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].First().LastConnection),
|
||||
Name = rankingsDict[s.ClientId].First().Name,
|
||||
Performance = Math.Round(rankingsDict[s.ClientId].Last().PerformanceMetric ?? 0, 2),
|
||||
RatingChange = (rankingsDict[s.ClientId].First().Ranking -
|
||||
rankingsDict[s.ClientId].Last().Ranking) ?? 0,
|
||||
Performance = Math.Round(rankingsDict[s.ClientId].First().PerformanceMetric ?? 0, 2),
|
||||
RatingChange = (rankingsDict[s.ClientId].Last().Ranking -
|
||||
rankingsDict[s.ClientId].First().Ranking) ?? 0,
|
||||
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => new PerformanceHistory
|
||||
{ Performance = ranking.PerformanceMetric ?? 0, OccurredAt = ranking.CreatedDateTime })
|
||||
.ToList(),
|
||||
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
|
||||
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed),
|
||||
Ranking = index + start + 1,
|
||||
ZScore = rankingsDict[s.ClientId].Last().ZScore,
|
||||
ZScore = rankingsDict[s.ClientId].First().ZScore,
|
||||
ServerId = serverId
|
||||
})
|
||||
.OrderBy(r => r.Ranking)
|
||||
.Take(60)
|
||||
.ToList();
|
||||
|
||||
return finished;
|
||||
@ -224,7 +249,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
return await GetNewTopStats(start, count, serverId);
|
||||
}
|
||||
|
||||
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
// setup the query for the clients within the given rating range
|
||||
var iqClientRatings = (from rating in context.Set<EFRating>()
|
||||
@ -267,7 +292,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.Select(grp => new
|
||||
{
|
||||
grp.Key,
|
||||
Ratings = grp.Select(r => new {r.Performance, r.Ranking, r.When})
|
||||
Ratings = grp.Select(r => new { r.Performance, r.Ranking, r.When })
|
||||
});
|
||||
|
||||
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
|
||||
@ -281,7 +306,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ClientId = s.Key,
|
||||
Kills = s.Sum(c => c.Kills),
|
||||
Deaths = s.Sum(c => c.Deaths),
|
||||
KDR = s.Sum(c => (c.Kills / (double) (c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
|
||||
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
|
||||
s.Sum(c => c.TimePlayed),
|
||||
TotalTimePlayed = s.Sum(c => c.TimePlayed),
|
||||
});
|
||||
@ -379,7 +404,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
Port = sv.Port,
|
||||
EndPoint = sv.ToString(),
|
||||
ServerId = serverId,
|
||||
GameName = (Reference.Game?) sv.GameName,
|
||||
GameName = (Reference.Game?)sv.GameName,
|
||||
HostName = sv.Hostname
|
||||
};
|
||||
|
||||
@ -389,9 +414,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
// we want to set the gamename up if it's never been set, or it changed
|
||||
else if (!server.GameName.HasValue || server.GameName.Value != (Reference.Game) sv.GameName)
|
||||
else if (!server.GameName.HasValue || server.GameName.Value != (Reference.Game)sv.GameName)
|
||||
{
|
||||
server.GameName = (Reference.Game) sv.GameName;
|
||||
server.GameName = (Reference.Game)sv.GameName;
|
||||
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
@ -482,7 +507,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
Active = true,
|
||||
HitCount = 0,
|
||||
Location = (int) hl
|
||||
Location = (int)hl
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
@ -502,7 +527,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
Active = true,
|
||||
HitCount = 0,
|
||||
Location = (int) hl
|
||||
Location = (int)hl
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@ -534,9 +559,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
catch (DbUpdateException updateException) when (
|
||||
updateException.InnerException is PostgresException {SqlState: "23503"}
|
||||
|| updateException.InnerException is SqliteException {SqliteErrorCode: 787}
|
||||
|| updateException.InnerException is MySqlException {SqlState: "23503"})
|
||||
updateException.InnerException is PostgresException { SqlState: "23503" }
|
||||
|| updateException.InnerException is SqliteException { SqliteErrorCode: 787 }
|
||||
|| updateException.InnerException is MySqlException { SqlState: "23503" })
|
||||
{
|
||||
_log.LogWarning("Trying to add {Client} to stats before they have been added to the database",
|
||||
pl.ToString());
|
||||
@ -657,9 +682,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ServerId = serverId,
|
||||
DeathOrigin = vDeathOrigin,
|
||||
KillOrigin = vKillOrigin,
|
||||
DeathType = (int) ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
|
||||
DeathType = (int)ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
|
||||
Damage = int.Parse(damage),
|
||||
HitLoc = (int) ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
|
||||
HitLoc = (int)ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
|
||||
WeaponReference = weapon,
|
||||
ViewAngles = vViewAngles,
|
||||
TimeOffset = long.Parse(offset),
|
||||
@ -673,21 +698,21 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
AnglesList = snapshotAngles,
|
||||
IsAlive = isAlive == "1",
|
||||
TimeSinceLastAttack = long.Parse(lastAttackTime),
|
||||
GameName = (int) attacker.CurrentServer.GameName
|
||||
GameName = (int)attacker.CurrentServer.GameName
|
||||
};
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Could not parse script hit data. Damage={Damage}, TimeOffset={Offset}, TimeSinceLastAttack={LastAttackTime}",
|
||||
_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)
|
||||
if (hit.HitLoc == (int)IW4Info.HitLocation.shield)
|
||||
{
|
||||
// we don't care about shield hits
|
||||
return;
|
||||
@ -706,9 +731,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
await waiter.WaitAsync(Utilities.DefaultCommandTimeout, Plugin.ServerManager.CancellationToken);
|
||||
|
||||
// increment their hit count
|
||||
if (hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
|
||||
hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
|
||||
hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
|
||||
if (hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
|
||||
hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
|
||||
hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
|
||||
{
|
||||
clientStats.HitLocations.First(hl => hl.Location == hit.HitLoc).HitCount += 1;
|
||||
}
|
||||
@ -851,7 +876,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
if (!gameDetectionTypes[server.GameName].Contains(detectionType))
|
||||
@ -883,7 +908,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
new EFPenalty()
|
||||
{
|
||||
AutomatedOffense = penalty.Type == Detection.DetectionType.Bone
|
||||
? $"{penalty.Type}-{(int) penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
|
||||
? $"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
|
||||
: $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
|
||||
}
|
||||
};
|
||||
@ -900,7 +925,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
string flagReason = penalty.Type == Cheat.Detection.DetectionType.Bone
|
||||
? $"{penalty.Type}-{(int) penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
|
||||
? $"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
|
||||
: $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}";
|
||||
|
||||
penaltyClient.AdministeredPenalties = new List<EFPenalty>()
|
||||
@ -939,19 +964,19 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
// update the total stats
|
||||
_servers[serverId].ServerStatistics.TotalKills += 1;
|
||||
|
||||
|
||||
if (attackerStats == null)
|
||||
{
|
||||
_log.LogWarning("Stats for {Client} are not yet initialized", attacker.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (victimStats == null)
|
||||
{
|
||||
_log.LogWarning("Stats for {Client} are not yet initialized", victim.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// this happens when the round has changed
|
||||
if (attackerStats.SessionScore == 0)
|
||||
{
|
||||
@ -964,10 +989,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
var estimatedAttackerScore = attacker.CurrentServer.GameName != Server.Game.CSGO
|
||||
? attacker.Score
|
||||
? attacker.Score
|
||||
: attackerStats.SessionKills * 50;
|
||||
var estimatedVictimScore = attacker.CurrentServer.GameName != Server.Game.CSGO
|
||||
? victim.Score
|
||||
var estimatedVictimScore = attacker.CurrentServer.GameName != Server.Game.CSGO
|
||||
? victim.Score
|
||||
: victimStats.SessionKills * 50;
|
||||
|
||||
attackerStats.SessionScore = estimatedAttackerScore;
|
||||
@ -1055,7 +1080,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
/// <returns></returns>
|
||||
public async Task UpdateStatHistory(EFClient client, EFClientStatistics clientStats)
|
||||
{
|
||||
int currentSessionTime = (int) (DateTime.UtcNow - client.LastConnection).TotalSeconds;
|
||||
int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds;
|
||||
|
||||
// don't update their stat history if they haven't played long
|
||||
if (currentSessionTime < 60)
|
||||
@ -1228,7 +1253,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext();
|
||||
var minPlayTime = _config.TopPlayersMinPlayTime;
|
||||
|
||||
|
||||
var performances = await context.Set<EFClientStatistics>()
|
||||
.AsNoTracking()
|
||||
.Where(stat => stat.ClientId == clientId)
|
||||
@ -1236,7 +1261,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.Where(stats => stats.UpdatedAt >= Extensions.FifteenDaysAgo())
|
||||
.Where(stats => stats.TimePlayed >= minPlayTime)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
if (clientStats.TimePlayed >= minPlayTime)
|
||||
{
|
||||
clientStats.ZScore = await _serverDistributionCalculator.GetZScoreForServer(serverId,
|
||||
@ -1267,8 +1292,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
if (performances.Any(performance => performance.TimePlayed >= minPlayTime))
|
||||
{
|
||||
var aggregateZScore = performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
|
||||
|
||||
var aggregateZScore =
|
||||
performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
|
||||
|
||||
int? aggregateRanking = await context.Set<EFClientStatistics>()
|
||||
.Where(stat => stat.ClientId != clientId)
|
||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime))
|
||||
@ -1287,7 +1313,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
clientStats.Client?.ToString(), aggregateZScore);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var aggregateRankingSnapshot = new EFClientRankingHistory
|
||||
{
|
||||
ClientId = clientId,
|
||||
@ -1310,7 +1336,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.Where(r => r.ClientId == clientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
.CountAsync();
|
||||
|
||||
|
||||
var mostRecent = await context.Set<EFClientRankingHistory>()
|
||||
.Where(r => r.ClientId == clientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
@ -1322,14 +1348,20 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
context.Update(mostRecent);
|
||||
}
|
||||
|
||||
if (totalRankingEntries > EFClientRankingHistory.MaxRankingCount)
|
||||
const int maxRankingCount = 1728; // 60 / 2.5 * 24 * 3 ( 3 days at sample every 2.5 minutes)
|
||||
|
||||
if (totalRankingEntries > maxRankingCount)
|
||||
{
|
||||
var lastRating = await context.Set<EFClientRankingHistory>()
|
||||
.Where(r => r.ClientId == clientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
.OrderBy(r => r.CreatedDateTime)
|
||||
.FirstOrDefaultAsync();
|
||||
context.Remove(lastRating);
|
||||
|
||||
if (lastRating is not null)
|
||||
{
|
||||
context.Remove(lastRating);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1338,7 +1370,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
/// </summary>
|
||||
/// <param name="attackerStats">Stats of the attacker</param>
|
||||
/// <param name="victimStats">Stats of the victim</param>
|
||||
public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats,
|
||||
public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats,
|
||||
EFClient attacker, EFClient victim)
|
||||
{
|
||||
bool suicide = attackerStats.ClientId == victimStats.ClientId;
|
||||
@ -1364,7 +1396,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
// calculate elo
|
||||
var attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) -
|
||||
Math.Log(Math.Max(1, attackerStats.EloRating));
|
||||
Math.Log(Math.Max(1, attackerStats.EloRating));
|
||||
var winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E));
|
||||
|
||||
attackerStats.EloRating += 6.0 * (1 - winPercentage);
|
||||
@ -1374,8 +1406,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2));
|
||||
|
||||
// update after calculation
|
||||
attackerStats.TimePlayed += (int) (DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
|
||||
victimStats.TimePlayed += (int) (DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
|
||||
attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
|
||||
victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
|
||||
attackerStats.LastActive = DateTime.UtcNow;
|
||||
victimStats.LastActive = DateTime.UtcNow;
|
||||
}
|
||||
@ -1413,11 +1445,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
var killSpm = scoreDifference / timeSinceLastCalc;
|
||||
var spmMultiplier = 2.934 *
|
||||
Math.Pow(
|
||||
_servers[clientStats.ServerId]
|
||||
.TeamCount((IW4Info.Team) clientStats.Team == IW4Info.Team.Allies
|
||||
? IW4Info.Team.Axis
|
||||
: IW4Info.Team.Allies), -0.454);
|
||||
Math.Pow(
|
||||
_servers[clientStats.ServerId]
|
||||
.TeamCount((IW4Info.Team)clientStats.Team == IW4Info.Team.Allies
|
||||
? IW4Info.Team.Axis
|
||||
: IW4Info.Team.Allies), -0.454);
|
||||
killSpm *= Math.Max(1, spmMultiplier);
|
||||
|
||||
// update this for ac tracking
|
||||
@ -1434,8 +1466,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
// calculate the weight of the new play time against last 10 hours of gameplay
|
||||
int totalPlayTime = (clientStats.TimePlayed == 0)
|
||||
? (int) (DateTime.UtcNow - clientStats.LastActive).TotalSeconds
|
||||
: clientStats.TimePlayed + (int) (DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
|
||||
? (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds
|
||||
: clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
|
||||
|
||||
double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0));
|
||||
|
||||
@ -1455,7 +1487,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
|
||||
{
|
||||
_log.LogWarning("clientStats SPM/Skill NaN {@killInfo}",
|
||||
new {killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference});
|
||||
new
|
||||
{
|
||||
killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference
|
||||
});
|
||||
clientStats.SPM = 0;
|
||||
clientStats.Skill = 0;
|
||||
}
|
||||
@ -1496,11 +1531,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
public void ResetKillstreaks(Server sv)
|
||||
{
|
||||
foreach (var session in sv.GetClientsAsList()
|
||||
.Select(_client => new
|
||||
{
|
||||
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
|
||||
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
|
||||
}))
|
||||
.Select(_client => new
|
||||
{
|
||||
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
|
||||
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
|
||||
}))
|
||||
{
|
||||
session.stat?.StartNewSession();
|
||||
session.detection?.OnMapChange();
|
||||
@ -1562,8 +1597,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
foreach (var stats in sv.GetClientsAsList()
|
||||
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))
|
||||
.Where(_stats => _stats != null))
|
||||
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))
|
||||
.Where(_stats => _stats != null))
|
||||
{
|
||||
await SaveClientStats(stats);
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Responses;
|
||||
|
||||
public class ClientNoteMetaResponse
|
||||
{
|
||||
public string Note { get; set; }
|
||||
public int OriginEntityId { get; set; }
|
||||
[JsonIgnore]
|
||||
public string OriginEntityName { get; set; }
|
||||
public DateTime ModifiedDate { get; set; }
|
||||
}
|
@ -33,5 +33,6 @@ namespace SharedLibraryCore.Dtos
|
||||
public string ConnectProtocolUrl { get;set; }
|
||||
public string CurrentServerName { get; set; }
|
||||
public IGeoLocationResult GeoLocationInfo { get; set; }
|
||||
public ClientNoteMetaResponse NoteMeta { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -938,6 +938,14 @@ namespace SharedLibraryCore.Services
|
||||
return clientList;
|
||||
}
|
||||
|
||||
public async Task<string> GetClientNameById(int clientId)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext();
|
||||
var match = await context.Clients.Select(client => new { client.CurrentAlias.Name, client.ClientId })
|
||||
.FirstOrDefaultAsync(client => client.ClientId == clientId);
|
||||
return match?.Name;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -192,17 +192,7 @@ namespace SharedLibraryCore.Services
|
||||
.Where(FilterById);
|
||||
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<EFPenalty>> GetActivePenaltiesByClientId(int clientId)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
return await context.PenaltyIdentifiers
|
||||
.Where(identifier => identifier.Penalty.Offender.ClientId == clientId)
|
||||
.Select(identifier => identifier.Penalty)
|
||||
.Where(Filter)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<EFPenalty>> ActivePenaltiesByRecentIdentifiers(int linkId)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
|
@ -2,17 +2,20 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using Data.Models.Client;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using WebfrontCore.Permissions;
|
||||
using WebfrontCore.ViewModels;
|
||||
|
||||
namespace WebfrontCore.Controllers
|
||||
@ -20,6 +23,7 @@ namespace WebfrontCore.Controllers
|
||||
public class ActionController : BaseController
|
||||
{
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
private readonly IMetaServiceV2 _metaService;
|
||||
private readonly string _banCommandName;
|
||||
private readonly string _tempbanCommandName;
|
||||
private readonly string _unbanCommandName;
|
||||
@ -29,11 +33,14 @@ namespace WebfrontCore.Controllers
|
||||
private readonly string _flagCommandName;
|
||||
private readonly string _unflagCommandName;
|
||||
private readonly string _setLevelCommandName;
|
||||
private readonly string _setClientTagCommandName;
|
||||
private readonly string _addClientNoteCommandName;
|
||||
|
||||
public ActionController(IManager manager, IEnumerable<IManagerCommand> registeredCommands,
|
||||
ApplicationConfiguration appConfig) : base(manager)
|
||||
ApplicationConfiguration appConfig, IMetaServiceV2 metaService) : base(manager)
|
||||
{
|
||||
_appConfig = appConfig;
|
||||
_metaService = metaService;
|
||||
|
||||
foreach (var cmd in registeredCommands)
|
||||
{
|
||||
@ -69,6 +76,12 @@ namespace WebfrontCore.Controllers
|
||||
case "OfflineMessageCommand":
|
||||
_offlineMessageCommandName = cmd.Name;
|
||||
break;
|
||||
case "SetClientTagCommand":
|
||||
_setClientTagCommandName = cmd.Name;
|
||||
break;
|
||||
case "AddClientNoteCommand":
|
||||
_addClientNoteCommandName = cmd.Name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -582,6 +595,103 @@ namespace WebfrontCore.Controllers
|
||||
}));
|
||||
}
|
||||
|
||||
public async Task<IActionResult> SetClientTagForm(int id, CancellationToken token)
|
||||
{
|
||||
var tags = await _metaService.GetPersistentMetaValue<List<LookupValue<string>>>(EFMeta.ClientTagNameV2,
|
||||
token) ?? new List<LookupValue<string>>();
|
||||
var existingTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
|
||||
EFMeta.ClientTagNameV2, id, Manager.CancellationToken);
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_SUBMIT"],
|
||||
Name = Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TAG"],
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "clientTag",
|
||||
Type = "select",
|
||||
Label = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_FORM_TAG"],
|
||||
Values = tags.ToDictionary(
|
||||
item => item.Value == existingTag?.Value ? $"!selected!{item.Value}" : item.Value,
|
||||
item => item.Value)
|
||||
}
|
||||
},
|
||||
Action = nameof(SetClientTag),
|
||||
ShouldRefresh = true
|
||||
};
|
||||
|
||||
return View("_ActionForm", info);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> SetClientTag(int targetId, string clientTag)
|
||||
{
|
||||
if (targetId <= 0 || string.IsNullOrWhiteSpace(clientTag))
|
||||
{
|
||||
return Json(new[]
|
||||
{
|
||||
new CommandResponseInfo
|
||||
{
|
||||
Response = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_NONE"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var server = Manager.GetServers().First();
|
||||
return await Task.FromResult(RedirectToAction("Execute", "Console", new
|
||||
{
|
||||
serverId = server.EndPoint,
|
||||
command =
|
||||
$"{_appConfig.CommandPrefix}{_setClientTagCommandName} @{targetId} {clientTag}"
|
||||
}));
|
||||
}
|
||||
|
||||
public async Task<IActionResult> AddClientNoteForm(int id)
|
||||
{
|
||||
var existingNote = await _metaService.GetPersistentMetaValue<ClientNoteMetaResponse>("ClientNotes", id);
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_CONFIGURATION_BUTTON_SAVE"],
|
||||
Name = Localization["WEBFRONT_PROFILE_CONTEXT_MENU_NOTE"],
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "note",
|
||||
Label = Localization["WEBFRONT_ACTION_NOTE_FORM_NOTE"],
|
||||
Value = existingNote?.Note,
|
||||
Type = "textarea"
|
||||
}
|
||||
},
|
||||
Action = nameof(AddClientNote),
|
||||
ShouldRefresh = true
|
||||
};
|
||||
|
||||
return View("_ActionForm", info);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> AddClientNote(int targetId, string note)
|
||||
{
|
||||
if (note?.Length > 350 || note?.Count(c => c == '\n') > 4)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status400BadRequest, new[]
|
||||
{
|
||||
new CommandResponseInfo
|
||||
{
|
||||
Response = Localization["WEBFRONT_ACTION_NOTE_INVALID_LENGTH"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var server = Manager.GetServers().First();
|
||||
return await Task.FromResult(RedirectToAction("Execute", "Console", new
|
||||
{
|
||||
serverId = server.EndPoint,
|
||||
command =
|
||||
$"{_appConfig.CommandPrefix}{_addClientNoteCommandName} @{targetId} {note}"
|
||||
}));
|
||||
}
|
||||
|
||||
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
|
||||
.Concat(_appConfig.GlobalRules)
|
||||
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
|
||||
|
@ -12,6 +12,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using SharedLibraryCore.Services;
|
||||
using Stats.Config;
|
||||
using WebfrontCore.Permissions;
|
||||
using WebfrontCore.ViewComponents;
|
||||
@ -23,13 +24,15 @@ namespace WebfrontCore.Controllers
|
||||
private readonly IMetaServiceV2 _metaService;
|
||||
private readonly StatsConfiguration _config;
|
||||
private readonly IGeoLocationService _geoLocationService;
|
||||
private readonly ClientService _clientService;
|
||||
|
||||
public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config,
|
||||
IGeoLocationService geoLocationService) : base(manager)
|
||||
IGeoLocationService geoLocationService, ClientService clientService) : base(manager)
|
||||
{
|
||||
_metaService = metaService;
|
||||
_config = config;
|
||||
_geoLocationService = geoLocationService;
|
||||
_clientService = clientService;
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
@ -53,18 +56,25 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
_metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2, EFMeta.ClientTagNameV2, client.ClientId,
|
||||
token),
|
||||
_metaService.GetPersistentMeta("GravatarEmail", client.ClientId, token)
|
||||
_metaService.GetPersistentMeta("GravatarEmail", client.ClientId, token),
|
||||
};
|
||||
|
||||
var persistentMeta = await Task.WhenAll(persistentMetaTask);
|
||||
var tag = persistentMeta[0];
|
||||
var gravatar = persistentMeta[1];
|
||||
var note = await _metaService.GetPersistentMetaValue<ClientNoteMetaResponse>("ClientNotes", client.ClientId,
|
||||
token);
|
||||
|
||||
if (tag?.Value != null)
|
||||
{
|
||||
client.SetAdditionalProperty(EFMeta.ClientTagV2, tag.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(note?.Note))
|
||||
{
|
||||
note.OriginEntityName = await _clientService.GetClientNameById(note.OriginEntityId);
|
||||
}
|
||||
|
||||
// even though we haven't set their level to "banned" yet
|
||||
// (ie they haven't reconnected with the infringing player identifier)
|
||||
// we want to show them as banned as to not confuse people.
|
||||
@ -123,7 +133,8 @@ namespace WebfrontCore.Controllers
|
||||
: ingameClient.CurrentServer.IP,
|
||||
ingameClient.CurrentServer.Port),
|
||||
CurrentServerName = ingameClient?.CurrentServer?.Hostname,
|
||||
GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString)
|
||||
GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString),
|
||||
NoteMeta = string.IsNullOrWhiteSpace(note?.Note) ? null: note
|
||||
};
|
||||
|
||||
var meta = await _metaService.GetRuntimeMeta<InformationResponse>(new ClientPaginationRequest
|
||||
|
@ -14,13 +14,13 @@ public enum WebfrontEntity
|
||||
AuditPage,
|
||||
RecentPlayersPage,
|
||||
ProfilePage,
|
||||
AdminMenu
|
||||
AdminMenu,
|
||||
ClientNote
|
||||
}
|
||||
|
||||
public enum WebfrontPermission
|
||||
{
|
||||
Read,
|
||||
Create,
|
||||
Update,
|
||||
Write,
|
||||
Delete
|
||||
}
|
||||
|
@ -25,11 +25,20 @@
|
||||
@if (inputType == "select")
|
||||
{
|
||||
<select name="@input.Name" class="form-control" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">
|
||||
@foreach (var item in input.Values)
|
||||
@foreach (var (key, item) in input.Values)
|
||||
{
|
||||
<option value="@item.Key">
|
||||
<color-code value="@item.Value"></color-code>
|
||||
</option>
|
||||
if (key.StartsWith("!selected!"))
|
||||
{
|
||||
<option value="@key.Replace("!selected!", "")" selected>
|
||||
<color-code value="@item"></color-code>
|
||||
</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@key">
|
||||
<color-code value="@item"></color-code>
|
||||
</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
}
|
||||
@ -42,6 +51,11 @@
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
|
||||
else if (inputType == "textarea")
|
||||
{
|
||||
<textarea name="@input.Name" class="form-control @(input.Required ? "required" : "")" placeholder="@input.Placeholder" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">@value</textarea>
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
|
@ -30,7 +30,7 @@
|
||||
</td>
|
||||
<td>
|
||||
@info.Data
|
||||
<td>
|
||||
<td class="text-force-break font-weight-light">
|
||||
@info.NewValue
|
||||
</td>
|
||||
<td class="text-right">
|
||||
@ -40,32 +40,36 @@
|
||||
|
||||
<!-- mobile -->
|
||||
<tr class="d-table-row d-lg-none d-flex bg-dark-dm bg-light-lm">
|
||||
<td class="bg-primary text-light text-right flex-grow-0">
|
||||
<td class="bg-primary text-light text-right flex-grow-0 w-quarter d-flex flex-column">
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_INFO"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_CURRENT"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</div>
|
||||
<div class="mt-5 mb-5 mt-auto">@loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</div>
|
||||
</td>
|
||||
<td>
|
||||
<td class="w-three-quarter d-flex flex-column">
|
||||
<div class="mt-5 mb-5">@info.Action</div>
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.OriginId" class="link-inverse">
|
||||
<color-code value="@info.OriginName"></color-code>
|
||||
</a>
|
||||
<div class="mt-5 mb-5">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.OriginId" class="link-inverse">
|
||||
<color-code value="@info.OriginName"></color-code>
|
||||
</a>
|
||||
</div>
|
||||
@if (info.TargetId != null)
|
||||
{
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.TargetId" class="mt-5 mb-5">
|
||||
<color-code value="@info.TargetName"></color-code>
|
||||
</a>
|
||||
<div class="mt-5 mb-5">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.TargetId">
|
||||
<color-code value="@info.TargetName"></color-code>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mt-5 mb-5">–</div>
|
||||
}
|
||||
<div class="mt-5 mb-5">@info.Data</div>
|
||||
<div class="mt-5 mb-5">@info.NewValue</div>
|
||||
<div class="mt-5 mb-5">@info.When</div>
|
||||
<div class="mt-5 mb-5 text-force-break">@info.NewValue</div>
|
||||
<div class="mt-5 mb-5 text-muted">@info.When.ToStandardFormat()</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
@ -173,6 +173,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.NoteMeta?.Note))
|
||||
{
|
||||
<has-permission entity="ClientNote" required-permission="Read">
|
||||
<div class="rounded border p-10 m-10 d-flex flex-column flex-md-row" style="border-style: dashed !important">
|
||||
<i class="align-self-center oi oi-clipboard"></i>
|
||||
<div class="align-self-center font-size-12 font-weight-light pl-10 pr-10">
|
||||
@foreach (var line in Model.NoteMeta.Note.Split("\n"))
|
||||
{
|
||||
<div class="text-force-break">@line.TrimEnd('\r')</div>
|
||||
}
|
||||
<div class="mt-5">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.NoteMeta.OriginEntityId" class="no-decoration ">
|
||||
<color-code value="@Model.NoteMeta.OriginEntityName"></color-code>
|
||||
</a>
|
||||
<span>— @Model.NoteMeta.ModifiedDate.HumanizeForCurrentCulture()</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</has-permission>
|
||||
}
|
||||
|
||||
<div class="flex-fill d-flex justify-content-center justify-content-md-end mt-10 mt-md-0">
|
||||
<!-- country flag -->
|
||||
<div id="ipGeoDropdown" class="dropdown with-arrow align-self-center">
|
||||
@ -279,9 +300,30 @@
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (ViewBag.Authorized)
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TAG"],
|
||||
IsButton = true,
|
||||
Reference = "SetClientTag",
|
||||
Icon = "oi-tag",
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
|
||||
if ((ViewBag.PermissionsSet as IEnumerable<string>).HasPermission(WebfrontEntity.ClientNote, WebfrontPermission.Write))
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_NOTE"],
|
||||
IsButton = true,
|
||||
Reference = "AddClientNote",
|
||||
Icon = "oi-clipboard",
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MESSAGE"],
|
||||
|
@ -124,11 +124,12 @@
|
||||
var spm = Model.ServerId != null ? serverLegacyStat?.SPM.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.SPM), 0).ToNumericalString();
|
||||
|
||||
var performanceHistory = Model.Ratings
|
||||
.OrderBy(rating => rating.CreatedDateTime)
|
||||
.Select(rating => new PerformanceHistory { Performance = rating.PerformanceMetric, OccurredAt = rating.CreatedDateTime });
|
||||
|
||||
if (performance != null)
|
||||
if (performance != null && performance != Model.Ratings.FirstOrDefault().PerformanceMetric)
|
||||
{
|
||||
performanceHistory = performanceHistory.Append(new PerformanceHistory { Performance = performance.Value, OccurredAt = DateTime.UtcNow });
|
||||
performanceHistory = performanceHistory.Append(new PerformanceHistory { Performance = performance.Value, OccurredAt = Model.Ratings.FirstOrDefault()?.CreatedDateTime ?? DateTime.UtcNow });
|
||||
}
|
||||
|
||||
var score = allPerServer.Any()
|
||||
@ -285,7 +286,7 @@
|
||||
<!-- history graph -->
|
||||
@if (performanceHistory.Count() > 5)
|
||||
{
|
||||
<div class="w-half m-auto ml-lg-auto " id="client_performance_history_container">
|
||||
<div class="w-full w-lg-half m-auto ml-lg-auto" id="client_performance_history_container">
|
||||
<canvas id="client_performance_history" data-history="@(JsonSerializer.Serialize(performanceHistory))"></canvas>
|
||||
</div>
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
@using IW4MAdmin.Plugins.Stats
|
||||
@using System.Text.Json.Serialization
|
||||
@using System.Text.Json
|
||||
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
|
||||
@{
|
||||
@ -86,7 +85,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full w-md-half client-rating-graph pt-10 pb-10">
|
||||
<canvas id="rating_history_@(stat.ClientId + "_" + stat.Id)" data-history="@(JsonSerializer.Serialize(stat.PerformanceHistory))"></canvas>
|
||||
<canvas id="rating_history_@(stat.ClientId + "_" + stat.Id)" data-history="@(JsonSerializer.Serialize(stat.PerformanceHistory.OrderBy(perf => perf.OccurredAt)))"></canvas>
|
||||
</div>
|
||||
<div class="w-quarter align-self-center d-flex justify-content-center">
|
||||
<img class="w-100 h-100" src="~/images/stats/ranks/rank_@(stat.ZScore.RankIconIndexForZScore()).png" alt="@stat.Performance"/>
|
||||
|
@ -41,14 +41,14 @@
|
||||
|
||||
<!-- mobile -->
|
||||
<tr class="d-table-row d-lg-none d-flex border-bottom">
|
||||
<td class="bg-primary text-light text-right">
|
||||
<td class="bg-primary text-light text-right d-flex flex-column w-quarter">
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_OFFENSE"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</div>
|
||||
<div class="mt-5 mb-5 mt-auto">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</div>
|
||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_TIME"]</div>
|
||||
</td>
|
||||
<td class="flex-fill">
|
||||
<td class=" d-flex flex-column w-three-quarter">
|
||||
<div class="mt-5 mb-5">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.OffenderId" >
|
||||
<color-code value="@Model.OffenderName"></color-code>
|
||||
@ -57,13 +57,13 @@
|
||||
<div class="mt-5 mb-5 penalties-color-@Model.PenaltyTypeText.ToLower()">
|
||||
@ViewBag.Localization[$"WEBFRONT_PENALTY_{Model.PenaltyType.ToString().ToUpper()}"]
|
||||
</div>
|
||||
<div class="mt-5 mb-5">
|
||||
<div class="mt-5 mb-5 text-force-break">
|
||||
<color-code value="@($"{Model.Offense}{(ViewBag.Authorized ? Model.AdditionalPenaltyInformation : "")}")"></color-code>
|
||||
</div>
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.PunisherId" class="mt-5 mb-5 @((!ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy) || Model.PunisherLevel == 0 ? "text-dark-lm text-light-dm" : "level-color-" + (int)Model.PunisherLevel)">
|
||||
<color-code value="@Model.PunisherName"></color-code>
|
||||
</a>
|
||||
<div class="mt-5 mb-5">
|
||||
<div class="mt-5 mb-5 text-muted">
|
||||
@if (Model.Expired)
|
||||
{
|
||||
<span>@Model.TimePunishedString</span>
|
||||
|
@ -59,7 +59,7 @@
|
||||
{
|
||||
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 flex-row-reverse" : "")">
|
||||
<has-permission entity="AdminMenu" required-permission="Update">
|
||||
<has-permission entity="AdminMenu" required-permission="Write">
|
||||
<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"></i>
|
||||
</a>
|
||||
|
@ -31,7 +31,7 @@
|
||||
<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>
|
||||
</a>
|
||||
<has-permission entity="AdminMenu" required-permission="Update">
|
||||
<has-permission entity="AdminMenu" required-permission="Write">
|
||||
<!-- send message button -->
|
||||
<a href="#actionModal" class="profile-action text-light align-self-center" data-action="chat" data-action-id="@Model.ID">
|
||||
<i class="oi oi-chat ml-5 mr-5"></i>
|
||||
|
@ -12,7 +12,7 @@
|
||||
"outputFileName": "wwwroot/js/global.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/lib/jquery/dist/jquery.js",
|
||||
"wwwroot/lib/moment.js/moment-with-locales.min.js",
|
||||
"wwwroot/lib/moment.js/min/moment-with-locales.min.js",
|
||||
"wwwroot/lib/moment-timezone/moment-timezone.min.js",
|
||||
"wwwroot/lib/chart.js/dist/Chart.bundle.min.js",
|
||||
"wwwroot/lib/halfmoon/js/halfmoon.min.js",
|
||||
|
@ -84,10 +84,6 @@
|
||||
border-bottom-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
#penalty_filter_selection {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotation {
|
||||
from {
|
||||
-webkit-transform: rotate(359deg);
|
||||
|
@ -26,7 +26,7 @@ function setupPerformanceGraph() {
|
||||
}
|
||||
const chart = $('#client_performance_history');
|
||||
const container = $('#client_performance_history_container');
|
||||
chart.attr('height', summary.height());
|
||||
chart.attr('height', summary.height() * 1.5);
|
||||
chart.attr('width', container.width());
|
||||
renderPerformanceChart();
|
||||
}
|
||||
@ -394,7 +394,7 @@ function renderPerformanceChart() {
|
||||
position: 'right',
|
||||
ticks: {
|
||||
precision: 0,
|
||||
stepSize: 3,
|
||||
stepSize: max - min / 2,
|
||||
callback: function (value, index, values) {
|
||||
if (index === values.length - 1) {
|
||||
return min;
|
||||
|
@ -88,7 +88,7 @@ function getStatsChart(id) {
|
||||
position: 'right',
|
||||
ticks: {
|
||||
precision: 0,
|
||||
stepSize: 3,
|
||||
stepSize: max - min / 2,
|
||||
callback: function (value, index, values) {
|
||||
if (index === values.length - 1) {
|
||||
return min;
|
||||
|
Reference in New Issue
Block a user