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";
|
Name = "readmessage";
|
||||||
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
|
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
|
||||||
Alias = "rm";
|
Alias = "rm";
|
||||||
Permission = EFClient.Permission.Flagged;
|
Permission = EFClient.Permission.User;
|
||||||
|
|
||||||
_contextFactory = contextFactory;
|
_contextFactory = contextFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
@ -9,6 +9,7 @@ using SharedLibraryCore.Helpers;
|
|||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@ -771,17 +772,28 @@ namespace IW4MAdmin
|
|||||||
|
|
||||||
else if (E.Type == GameEvent.EventType.MetaUpdated)
|
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 parts = E.Data.Split(",");
|
||||||
var banPenalty = penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
|
|
||||||
|
if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
|
||||||
|
int.TryParse(parts[1], out var low))
|
||||||
|
{
|
||||||
|
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)
|
if (banPenalty is not null && E.Origin.Level != Permission.Banned)
|
||||||
{
|
{
|
||||||
ServerLogger.LogInformation(
|
ServerLogger.LogInformation(
|
||||||
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
|
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
|
||||||
E.Origin.ToString(), persistentClientId);
|
E.Origin.ToString(), guid);
|
||||||
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(persistentClientId), Utilities.IW4MAdminClient(this), true);
|
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(guid),
|
||||||
|
Utilities.IW4MAdminClient(this), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1289,28 +1301,17 @@ namespace IW4MAdmin
|
|||||||
this.GamePassword = gamePassword.Value;
|
this.GamePassword = gamePassword.Value;
|
||||||
UpdateMap(mapname);
|
UpdateMap(mapname);
|
||||||
|
|
||||||
if (RconParser.CanGenerateLogPath)
|
if (RconParser.CanGenerateLogPath && string.IsNullOrEmpty(ServerConfig.ManualLogPath))
|
||||||
{
|
{
|
||||||
bool needsRestart = false;
|
|
||||||
|
|
||||||
if (logsync.Value == 0)
|
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
|
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))
|
if (string.IsNullOrWhiteSpace(logfile.Value))
|
||||||
{
|
{
|
||||||
logfile.Value = "games_mp.log";
|
logfile.Value = "games_mp.log";
|
||||||
await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken);
|
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
|
// this DVAR isn't set until the a map is loaded
|
||||||
|
@ -33,7 +33,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
builder.Append(header);
|
builder.Append(header);
|
||||||
builder.Append(config.NoticeLineSeparator);
|
builder.Append(config.NoticeLineSeparator);
|
||||||
// build the reason
|
// 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)
|
if (isNewLineSeparator)
|
||||||
{
|
{
|
||||||
|
@ -7,8 +7,6 @@ namespace Data.Models.Client.Stats
|
|||||||
{
|
{
|
||||||
public class EFClientRankingHistory: AuditFields
|
public class EFClientRankingHistory: AuditFields
|
||||||
{
|
{
|
||||||
public const int MaxRankingCount = 1728;
|
|
||||||
|
|
||||||
[Key]
|
[Key]
|
||||||
public long ClientRankingHistoryId { get; set; }
|
public long ClientRankingHistoryId { get; set; }
|
||||||
|
|
||||||
|
@ -29,8 +29,9 @@ onPlayerConnect( player )
|
|||||||
for( ;; )
|
for( ;; )
|
||||||
{
|
{
|
||||||
level waittill( "connected", player );
|
level waittill( "connected", player );
|
||||||
player setClientDvar("cl_autorecord", 1);
|
player setClientDvars( "cl_autorecord", 1,
|
||||||
player setClientDvar("cl_demosKeep", 200);
|
"cl_demosKeep", 200 );
|
||||||
|
|
||||||
player thread waitForFrameThread();
|
player thread waitForFrameThread();
|
||||||
player thread waitForAttack();
|
player thread waitForAttack();
|
||||||
}
|
}
|
||||||
@ -60,7 +61,7 @@ getHttpString( url )
|
|||||||
|
|
||||||
runRadarUpdates()
|
runRadarUpdates()
|
||||||
{
|
{
|
||||||
interval = int(getDvar("sv_printradar_updateinterval"));
|
interval = getDvarInt( "sv_printradar_updateinterval", 500 );
|
||||||
|
|
||||||
for ( ;; )
|
for ( ;; )
|
||||||
{
|
{
|
||||||
@ -191,7 +192,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastAttack = int(getTime()) - int(self.lastAttackTime);
|
lastAttack = getTime() - self.lastAttackTime;
|
||||||
isAlive = isAlive(self);
|
isAlive = isAlive(self);
|
||||||
|
|
||||||
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
||||||
|
@ -53,7 +53,7 @@ waitForAttack()
|
|||||||
|
|
||||||
runRadarUpdates()
|
runRadarUpdates()
|
||||||
{
|
{
|
||||||
interval = int(getDvar("sv_printradar_updateinterval"));
|
interval = getDvarInt( "sv_printradar_updateinterval", 500 );
|
||||||
|
|
||||||
for ( ;; )
|
for ( ;; )
|
||||||
{
|
{
|
||||||
@ -183,7 +183,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastAttack = int(getTime()) - int(self.lastAttackTime);
|
lastAttack = getTime() - self.lastAttackTime;
|
||||||
isAlive = isAlive(self);
|
isAlive = isAlive(self);
|
||||||
|
|
||||||
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
||||||
|
@ -60,7 +60,7 @@ waitForAttack()
|
|||||||
|
|
||||||
runRadarUpdates()
|
runRadarUpdates()
|
||||||
{
|
{
|
||||||
interval = int(getDvar("sv_printradar_updateinterval"));
|
interval = getDvarInt( "sv_printradar_updateinterval" );
|
||||||
|
|
||||||
for ( ;; )
|
for ( ;; )
|
||||||
{
|
{
|
||||||
@ -190,7 +190,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastAttack = int(getTime()) - int(self.lastAttackTime);
|
lastAttack = getTime() - self.lastAttackTime;
|
||||||
isAlive = isAlive(self);
|
isAlive = isAlive(self);
|
||||||
|
|
||||||
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
||||||
|
@ -59,7 +59,7 @@ init()
|
|||||||
|
|
||||||
OnPlayerConnect()
|
OnPlayerConnect()
|
||||||
{
|
{
|
||||||
level endon ( "disconnect" );
|
level endon ( "game_ended" );
|
||||||
|
|
||||||
for ( ;; )
|
for ( ;; )
|
||||||
{
|
{
|
||||||
@ -104,7 +104,7 @@ OnPlayerSpawned()
|
|||||||
|
|
||||||
OnPlayerDisconnect()
|
OnPlayerDisconnect()
|
||||||
{
|
{
|
||||||
level endon ( "disconnect" );
|
self endon ( "disconnect" );
|
||||||
|
|
||||||
for ( ;; )
|
for ( ;; )
|
||||||
{
|
{
|
||||||
@ -139,8 +139,6 @@ OnPlayerJoinedSpectators()
|
|||||||
|
|
||||||
OnGameEnded()
|
OnGameEnded()
|
||||||
{
|
{
|
||||||
level endon ( "disconnect" );
|
|
||||||
|
|
||||||
for ( ;; )
|
for ( ;; )
|
||||||
{
|
{
|
||||||
level waittill( "game_ended" );
|
level waittill( "game_ended" );
|
||||||
@ -167,24 +165,29 @@ DisplayWelcomeData()
|
|||||||
|
|
||||||
SetPersistentData()
|
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 )
|
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 )
|
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()
|
PlayerConnectEvents()
|
||||||
@ -228,8 +231,7 @@ PlayerTrackingOnInterval()
|
|||||||
|
|
||||||
MonitorClientEvents()
|
MonitorClientEvents()
|
||||||
{
|
{
|
||||||
level endon( "disconnect" );
|
level endon( "game_ended" );
|
||||||
self endon( "disconnect" );
|
|
||||||
|
|
||||||
for ( ;; )
|
for ( ;; )
|
||||||
{
|
{
|
||||||
@ -324,6 +326,107 @@ DecrementClientMeta( metaKey, decrementValue, clientId )
|
|||||||
SetClientMeta( metaKey, decrementValue, clientId, "decrement" );
|
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 )
|
GenerateJoinTeamString( isSpectator )
|
||||||
{
|
{
|
||||||
team = self.team;
|
team = self.team;
|
||||||
@ -476,7 +579,7 @@ MonitorBus()
|
|||||||
|
|
||||||
QueueEvent( request, eventType, notifyEntity )
|
QueueEvent( request, eventType, notifyEntity )
|
||||||
{
|
{
|
||||||
level endon( "disconnect" );
|
level endon( "game_ended" );
|
||||||
|
|
||||||
start = GetTime();
|
start = GetTime();
|
||||||
maxWait = level.eventBus.timeout * 1000; // 30 seconds
|
maxWait = level.eventBus.timeout * 1000; // 30 seconds
|
||||||
@ -511,6 +614,8 @@ QueueEvent( request, eventType, notifyEntity )
|
|||||||
notifyEntity NotifyClientEventTimeout( eventType );
|
notifyEntity NotifyClientEventTimeout( eventType );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetDvar( level.eventBus.inVar, "" );
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}`);
|
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}, Direction=${event.data['direction']} ${token}`);
|
||||||
if (event.data['direction'] != null) {
|
if (event.data['direction'] != null) {
|
||||||
event.data['direction'] = 'up'
|
event.data['direction'] = 'up'
|
||||||
? metaService.IncrementPersistentMeta(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'], event.data['value'], clientId, token).GetAwaiter().GetResult();
|
: metaService.DecrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult();
|
||||||
} else {
|
} else {
|
||||||
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult();
|
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.ClientId == clientInfo.ClientId)
|
||||||
.Where(r => r.ServerId == serverId)
|
.Where(r => r.ServerId == serverId)
|
||||||
.Where(r => r.Ranking != null)
|
.Where(r => r.Ranking != null)
|
||||||
.OrderByDescending(r => r.UpdatedDateTime)
|
.OrderByDescending(r => r.CreatedDateTime)
|
||||||
.Take(250)
|
.Take(250)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
@ -117,7 +117,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
return 0;
|
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
|
return (ranking) => ranking.ServerId == serverId
|
||||||
&& ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned
|
&& ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned
|
||||||
@ -138,6 +139,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
.CountAsync();
|
.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)
|
public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null)
|
||||||
{
|
{
|
||||||
await using var context = _contextFactory.CreateContext(false);
|
await using var context = _contextFactory.CreateContext(false);
|
||||||
@ -150,23 +162,37 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
.Take(count)
|
.Take(count)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var rankings = await context.Set<EFClientRankingHistory>()
|
var rankingsDict = new Dictionary<int, List<RankingSnapshot>>();
|
||||||
.Where(ranking => clientIdsList.Contains(ranking.ClientId))
|
|
||||||
.Where(ranking => ranking.ServerId == serverId)
|
foreach (var clientId in clientIdsList)
|
||||||
.Select(ranking => new
|
|
||||||
{
|
{
|
||||||
ranking.ClientId,
|
var eachRank = await context.Set<EFClientRankingHistory>()
|
||||||
ranking.Client.CurrentAlias.Name,
|
.Where(ranking => ranking.ClientId == clientId)
|
||||||
ranking.Client.LastConnection,
|
.Where(ranking => ranking.ServerId == serverId)
|
||||||
ranking.PerformanceMetric,
|
.OrderByDescending(ranking => ranking.CreatedDateTime)
|
||||||
ranking.ZScore,
|
.Select(ranking => new RankingSnapshot
|
||||||
ranking.Ranking,
|
{
|
||||||
ranking.CreatedDateTime
|
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();
|
.ToListAsync();
|
||||||
|
|
||||||
var rankingsDict = rankings.GroupBy(rank => rank.ClientId)
|
if (rankingsDict.ContainsKey(clientId))
|
||||||
.ToDictionary(rank => rank.Key, rank => rank.OrderBy(r => r.CreatedDateTime).ToList());
|
{
|
||||||
|
rankingsDict[clientId] = rankingsDict[clientId].Concat(eachRank).Distinct()
|
||||||
|
.OrderByDescending(ranking => ranking.CreatedDateTime).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rankingsDict.Add(clientId, eachRank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var statsInfo = await context.Set<EFClientStatistics>()
|
var statsInfo = await context.Set<EFClientStatistics>()
|
||||||
.Where(stat => clientIdsList.Contains(stat.ClientId))
|
.Where(stat => clientIdsList.Contains(stat.ClientId))
|
||||||
@ -187,7 +213,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var finished = statsInfo
|
var finished = statsInfo
|
||||||
.OrderByDescending(stat => rankingsDict[stat.ClientId].Last().PerformanceMetric)
|
.OrderByDescending(stat => rankingsDict[stat.ClientId].First().PerformanceMetric)
|
||||||
.Select((s, index) => new TopStatsInfo
|
.Select((s, index) => new TopStatsInfo
|
||||||
{
|
{
|
||||||
ClientId = s.ClientId,
|
ClientId = s.ClientId,
|
||||||
@ -195,24 +221,23 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
Deaths = s.Deaths,
|
Deaths = s.Deaths,
|
||||||
Kills = s.Kills,
|
Kills = s.Kills,
|
||||||
KDR = Math.Round(s.KDR, 2),
|
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),
|
.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,
|
Name = rankingsDict[s.ClientId].First().Name,
|
||||||
Performance = Math.Round(rankingsDict[s.ClientId].Last().PerformanceMetric ?? 0, 2),
|
Performance = Math.Round(rankingsDict[s.ClientId].First().PerformanceMetric ?? 0, 2),
|
||||||
RatingChange = (rankingsDict[s.ClientId].First().Ranking -
|
RatingChange = (rankingsDict[s.ClientId].Last().Ranking -
|
||||||
rankingsDict[s.ClientId].Last().Ranking) ?? 0,
|
rankingsDict[s.ClientId].First().Ranking) ?? 0,
|
||||||
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => new PerformanceHistory
|
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => new PerformanceHistory
|
||||||
{ Performance = ranking.PerformanceMetric ?? 0, OccurredAt = ranking.CreatedDateTime })
|
{ Performance = ranking.PerformanceMetric ?? 0, OccurredAt = ranking.CreatedDateTime })
|
||||||
.ToList(),
|
.ToList(),
|
||||||
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
|
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
|
||||||
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed),
|
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed),
|
||||||
Ranking = index + start + 1,
|
Ranking = index + start + 1,
|
||||||
ZScore = rankingsDict[s.ClientId].Last().ZScore,
|
ZScore = rankingsDict[s.ClientId].First().ZScore,
|
||||||
ServerId = serverId
|
ServerId = serverId
|
||||||
})
|
})
|
||||||
.OrderBy(r => r.Ranking)
|
.OrderBy(r => r.Ranking)
|
||||||
.Take(60)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return finished;
|
return finished;
|
||||||
@ -675,11 +700,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
TimeSinceLastAttack = long.Parse(lastAttackTime),
|
TimeSinceLastAttack = long.Parse(lastAttackTime),
|
||||||
GameName = (int)attacker.CurrentServer.GameName
|
GameName = (int)attacker.CurrentServer.GameName
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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);
|
damage, offset, lastAttackTime);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -1267,7 +1292,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
if (performances.Any(performance => performance.TimePlayed >= minPlayTime))
|
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>()
|
int? aggregateRanking = await context.Set<EFClientStatistics>()
|
||||||
.Where(stat => stat.ClientId != clientId)
|
.Where(stat => stat.ClientId != clientId)
|
||||||
@ -1322,16 +1348,22 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
context.Update(mostRecent);
|
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>()
|
var lastRating = await context.Set<EFClientRankingHistory>()
|
||||||
.Where(r => r.ClientId == clientId)
|
.Where(r => r.ClientId == clientId)
|
||||||
.Where(r => r.ServerId == serverId)
|
.Where(r => r.ServerId == serverId)
|
||||||
.OrderBy(r => r.CreatedDateTime)
|
.OrderBy(r => r.CreatedDateTime)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (lastRating is not null)
|
||||||
|
{
|
||||||
context.Remove(lastRating);
|
context.Remove(lastRating);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs the incrementation of kills and deaths for client statistics
|
/// Performs the incrementation of kills and deaths for client statistics
|
||||||
@ -1455,7 +1487,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
|
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
|
||||||
{
|
{
|
||||||
_log.LogWarning("clientStats SPM/Skill NaN {@killInfo}",
|
_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.SPM = 0;
|
||||||
clientStats.Skill = 0;
|
clientStats.Skill = 0;
|
||||||
}
|
}
|
||||||
|
@ -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 ConnectProtocolUrl { get;set; }
|
||||||
public string CurrentServerName { get; set; }
|
public string CurrentServerName { get; set; }
|
||||||
public IGeoLocationResult GeoLocationInfo { get; set; }
|
public IGeoLocationResult GeoLocationInfo { get; set; }
|
||||||
|
public ClientNoteMetaResponse NoteMeta { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -938,6 +938,14 @@ namespace SharedLibraryCore.Services
|
|||||||
return clientList;
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,16 +193,6 @@ namespace SharedLibraryCore.Services
|
|||||||
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync();
|
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)
|
public async Task<List<EFPenalty>> ActivePenaltiesByRecentIdentifiers(int linkId)
|
||||||
{
|
{
|
||||||
await using var context = _contextFactory.CreateContext(false);
|
await using var context = _contextFactory.CreateContext(false);
|
||||||
|
@ -2,17 +2,20 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Data.Models;
|
||||||
using Data.Models.Client;
|
using Data.Models.Client;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Commands;
|
using SharedLibraryCore.Commands;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
using SharedLibraryCore.Dtos;
|
using SharedLibraryCore.Dtos;
|
||||||
|
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||||
using SharedLibraryCore.Helpers;
|
using SharedLibraryCore.Helpers;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using WebfrontCore.Permissions;
|
|
||||||
using WebfrontCore.ViewModels;
|
using WebfrontCore.ViewModels;
|
||||||
|
|
||||||
namespace WebfrontCore.Controllers
|
namespace WebfrontCore.Controllers
|
||||||
@ -20,6 +23,7 @@ namespace WebfrontCore.Controllers
|
|||||||
public class ActionController : BaseController
|
public class ActionController : BaseController
|
||||||
{
|
{
|
||||||
private readonly ApplicationConfiguration _appConfig;
|
private readonly ApplicationConfiguration _appConfig;
|
||||||
|
private readonly IMetaServiceV2 _metaService;
|
||||||
private readonly string _banCommandName;
|
private readonly string _banCommandName;
|
||||||
private readonly string _tempbanCommandName;
|
private readonly string _tempbanCommandName;
|
||||||
private readonly string _unbanCommandName;
|
private readonly string _unbanCommandName;
|
||||||
@ -29,11 +33,14 @@ namespace WebfrontCore.Controllers
|
|||||||
private readonly string _flagCommandName;
|
private readonly string _flagCommandName;
|
||||||
private readonly string _unflagCommandName;
|
private readonly string _unflagCommandName;
|
||||||
private readonly string _setLevelCommandName;
|
private readonly string _setLevelCommandName;
|
||||||
|
private readonly string _setClientTagCommandName;
|
||||||
|
private readonly string _addClientNoteCommandName;
|
||||||
|
|
||||||
public ActionController(IManager manager, IEnumerable<IManagerCommand> registeredCommands,
|
public ActionController(IManager manager, IEnumerable<IManagerCommand> registeredCommands,
|
||||||
ApplicationConfiguration appConfig) : base(manager)
|
ApplicationConfiguration appConfig, IMetaServiceV2 metaService) : base(manager)
|
||||||
{
|
{
|
||||||
_appConfig = appConfig;
|
_appConfig = appConfig;
|
||||||
|
_metaService = metaService;
|
||||||
|
|
||||||
foreach (var cmd in registeredCommands)
|
foreach (var cmd in registeredCommands)
|
||||||
{
|
{
|
||||||
@ -69,6 +76,12 @@ namespace WebfrontCore.Controllers
|
|||||||
case "OfflineMessageCommand":
|
case "OfflineMessageCommand":
|
||||||
_offlineMessageCommandName = cmd.Name;
|
_offlineMessageCommandName = cmd.Name;
|
||||||
break;
|
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
|
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
|
||||||
.Concat(_appConfig.GlobalRules)
|
.Concat(_appConfig.GlobalRules)
|
||||||
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
|
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))
|
||||||
|
@ -12,6 +12,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Data.Models;
|
using Data.Models;
|
||||||
|
using SharedLibraryCore.Services;
|
||||||
using Stats.Config;
|
using Stats.Config;
|
||||||
using WebfrontCore.Permissions;
|
using WebfrontCore.Permissions;
|
||||||
using WebfrontCore.ViewComponents;
|
using WebfrontCore.ViewComponents;
|
||||||
@ -23,13 +24,15 @@ namespace WebfrontCore.Controllers
|
|||||||
private readonly IMetaServiceV2 _metaService;
|
private readonly IMetaServiceV2 _metaService;
|
||||||
private readonly StatsConfiguration _config;
|
private readonly StatsConfiguration _config;
|
||||||
private readonly IGeoLocationService _geoLocationService;
|
private readonly IGeoLocationService _geoLocationService;
|
||||||
|
private readonly ClientService _clientService;
|
||||||
|
|
||||||
public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config,
|
public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config,
|
||||||
IGeoLocationService geoLocationService) : base(manager)
|
IGeoLocationService geoLocationService, ClientService clientService) : base(manager)
|
||||||
{
|
{
|
||||||
_metaService = metaService;
|
_metaService = metaService;
|
||||||
_config = config;
|
_config = config;
|
||||||
_geoLocationService = geoLocationService;
|
_geoLocationService = geoLocationService;
|
||||||
|
_clientService = clientService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete]
|
[Obsolete]
|
||||||
@ -53,18 +56,25 @@ namespace WebfrontCore.Controllers
|
|||||||
{
|
{
|
||||||
_metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2, EFMeta.ClientTagNameV2, client.ClientId,
|
_metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2, EFMeta.ClientTagNameV2, client.ClientId,
|
||||||
token),
|
token),
|
||||||
_metaService.GetPersistentMeta("GravatarEmail", client.ClientId, token)
|
_metaService.GetPersistentMeta("GravatarEmail", client.ClientId, token),
|
||||||
};
|
};
|
||||||
|
|
||||||
var persistentMeta = await Task.WhenAll(persistentMetaTask);
|
var persistentMeta = await Task.WhenAll(persistentMetaTask);
|
||||||
var tag = persistentMeta[0];
|
var tag = persistentMeta[0];
|
||||||
var gravatar = persistentMeta[1];
|
var gravatar = persistentMeta[1];
|
||||||
|
var note = await _metaService.GetPersistentMetaValue<ClientNoteMetaResponse>("ClientNotes", client.ClientId,
|
||||||
|
token);
|
||||||
|
|
||||||
if (tag?.Value != null)
|
if (tag?.Value != null)
|
||||||
{
|
{
|
||||||
client.SetAdditionalProperty(EFMeta.ClientTagV2, tag.Value);
|
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
|
// even though we haven't set their level to "banned" yet
|
||||||
// (ie they haven't reconnected with the infringing player identifier)
|
// (ie they haven't reconnected with the infringing player identifier)
|
||||||
// we want to show them as banned as to not confuse people.
|
// we want to show them as banned as to not confuse people.
|
||||||
@ -123,7 +133,8 @@ namespace WebfrontCore.Controllers
|
|||||||
: ingameClient.CurrentServer.IP,
|
: ingameClient.CurrentServer.IP,
|
||||||
ingameClient.CurrentServer.Port),
|
ingameClient.CurrentServer.Port),
|
||||||
CurrentServerName = ingameClient?.CurrentServer?.Hostname,
|
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
|
var meta = await _metaService.GetRuntimeMeta<InformationResponse>(new ClientPaginationRequest
|
||||||
|
@ -14,13 +14,13 @@ public enum WebfrontEntity
|
|||||||
AuditPage,
|
AuditPage,
|
||||||
RecentPlayersPage,
|
RecentPlayersPage,
|
||||||
ProfilePage,
|
ProfilePage,
|
||||||
AdminMenu
|
AdminMenu,
|
||||||
|
ClientNote
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum WebfrontPermission
|
public enum WebfrontPermission
|
||||||
{
|
{
|
||||||
Read,
|
Read,
|
||||||
Create,
|
Write,
|
||||||
Update,
|
|
||||||
Delete
|
Delete
|
||||||
}
|
}
|
||||||
|
@ -25,12 +25,21 @@
|
|||||||
@if (inputType == "select")
|
@if (inputType == "select")
|
||||||
{
|
{
|
||||||
<select name="@input.Name" class="form-control" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">
|
<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">
|
if (key.StartsWith("!selected!"))
|
||||||
<color-code value="@item.Value"></color-code>
|
{
|
||||||
|
<option value="@key.Replace("!selected!", "")" selected>
|
||||||
|
<color-code value="@item"></color-code>
|
||||||
</option>
|
</option>
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<option value="@key">
|
||||||
|
<color-code value="@item"></color-code>
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
</select>
|
</select>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +52,11 @@
|
|||||||
</div>
|
</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
|
else
|
||||||
{
|
{
|
||||||
<input type="@inputType" name="@input.Name" value="@value" class="form-control @(input.Required ? "required" : "")" placeholder="@input.Placeholder" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">
|
<input type="@inputType" name="@input.Name" value="@value" class="form-control @(input.Required ? "required" : "")" placeholder="@input.Placeholder" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@info.Data
|
@info.Data
|
||||||
<td>
|
<td class="text-force-break font-weight-light">
|
||||||
@info.NewValue
|
@info.NewValue
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
@ -40,32 +40,36 @@
|
|||||||
|
|
||||||
<!-- mobile -->
|
<!-- mobile -->
|
||||||
<tr class="d-table-row d-lg-none d-flex bg-dark-dm bg-light-lm">
|
<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_TYPE"]</div>
|
||||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</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_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_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_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>
|
<td class="w-three-quarter d-flex flex-column">
|
||||||
<div class="mt-5 mb-5">@info.Action</div>
|
<div class="mt-5 mb-5">@info.Action</div>
|
||||||
|
<div class="mt-5 mb-5">
|
||||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.OriginId" class="link-inverse">
|
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.OriginId" class="link-inverse">
|
||||||
<color-code value="@info.OriginName"></color-code>
|
<color-code value="@info.OriginName"></color-code>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
@if (info.TargetId != null)
|
@if (info.TargetId != null)
|
||||||
{
|
{
|
||||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.TargetId" class="mt-5 mb-5">
|
<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>
|
<color-code value="@info.TargetName"></color-code>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="mt-5 mb-5">–</div>
|
<div class="mt-5 mb-5">–</div>
|
||||||
}
|
}
|
||||||
<div class="mt-5 mb-5">@info.Data</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 text-force-break">@info.NewValue</div>
|
||||||
<div class="mt-5 mb-5">@info.When</div>
|
<div class="mt-5 mb-5 text-muted">@info.When.ToStandardFormat()</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
@ -173,6 +173,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="flex-fill d-flex justify-content-center justify-content-md-end mt-10 mt-md-0">
|
||||||
<!-- country flag -->
|
<!-- country flag -->
|
||||||
<div id="ipGeoDropdown" class="dropdown with-arrow align-self-center">
|
<div id="ipGeoDropdown" class="dropdown with-arrow align-self-center">
|
||||||
@ -282,6 +303,27 @@
|
|||||||
|
|
||||||
if (ViewBag.Authorized)
|
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
|
menuItems.Items.Add(new SideContextMenuItem
|
||||||
{
|
{
|
||||||
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MESSAGE"],
|
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 spm = Model.ServerId != null ? serverLegacyStat?.SPM.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.SPM), 0).ToNumericalString();
|
||||||
|
|
||||||
var performanceHistory = Model.Ratings
|
var performanceHistory = Model.Ratings
|
||||||
|
.OrderBy(rating => rating.CreatedDateTime)
|
||||||
.Select(rating => new PerformanceHistory { Performance = rating.PerformanceMetric, OccurredAt = 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()
|
var score = allPerServer.Any()
|
||||||
@ -285,7 +286,7 @@
|
|||||||
<!-- history graph -->
|
<!-- history graph -->
|
||||||
@if (performanceHistory.Count() > 5)
|
@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>
|
<canvas id="client_performance_history" data-history="@(JsonSerializer.Serialize(performanceHistory))"></canvas>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
@using IW4MAdmin.Plugins.Stats
|
@using IW4MAdmin.Plugins.Stats
|
||||||
@using System.Text.Json.Serialization
|
|
||||||
@using System.Text.Json
|
@using System.Text.Json
|
||||||
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
|
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
|
||||||
@{
|
@{
|
||||||
@ -86,7 +85,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full w-md-half client-rating-graph pt-10 pb-10">
|
<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>
|
||||||
<div class="w-quarter align-self-center d-flex justify-content-center">
|
<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"/>
|
<img class="w-100 h-100" src="~/images/stats/ranks/rank_@(stat.ZScore.RankIconIndexForZScore()).png" alt="@stat.Performance"/>
|
||||||
|
@ -41,14 +41,14 @@
|
|||||||
|
|
||||||
<!-- mobile -->
|
<!-- mobile -->
|
||||||
<tr class="d-table-row d-lg-none d-flex border-bottom">
|
<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_NAME"]</div>
|
||||||
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]</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_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>
|
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_TIME"]</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="flex-fill">
|
<td class=" d-flex flex-column w-three-quarter">
|
||||||
<div class="mt-5 mb-5">
|
<div class="mt-5 mb-5">
|
||||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.OffenderId" >
|
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.OffenderId" >
|
||||||
<color-code value="@Model.OffenderName"></color-code>
|
<color-code value="@Model.OffenderName"></color-code>
|
||||||
@ -57,13 +57,13 @@
|
|||||||
<div class="mt-5 mb-5 penalties-color-@Model.PenaltyTypeText.ToLower()">
|
<div class="mt-5 mb-5 penalties-color-@Model.PenaltyTypeText.ToLower()">
|
||||||
@ViewBag.Localization[$"WEBFRONT_PENALTY_{Model.PenaltyType.ToString().ToUpper()}"]
|
@ViewBag.Localization[$"WEBFRONT_PENALTY_{Model.PenaltyType.ToString().ToUpper()}"]
|
||||||
</div>
|
</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>
|
<color-code value="@($"{Model.Offense}{(ViewBag.Authorized ? Model.AdditionalPenaltyInformation : "")}")"></color-code>
|
||||||
</div>
|
</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)">
|
<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>
|
<color-code value="@Model.PunisherName"></color-code>
|
||||||
</a>
|
</a>
|
||||||
<div class="mt-5 mb-5">
|
<div class="mt-5 mb-5 text-muted">
|
||||||
@if (Model.Expired)
|
@if (Model.Expired)
|
||||||
{
|
{
|
||||||
<span>@Model.TimePunishedString</span>
|
<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}";
|
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" : "")">
|
<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">
|
<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>
|
<i class="oi oi-circle-x font-size-12 @levelColorClass"></i>
|
||||||
</a>
|
</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"]">
|
<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>
|
<i class="oi oi-play-circle ml-5 mr-5"></i>
|
||||||
</a>
|
</a>
|
||||||
<has-permission entity="AdminMenu" required-permission="Update">
|
<has-permission entity="AdminMenu" required-permission="Write">
|
||||||
<!-- send message button -->
|
<!-- send message button -->
|
||||||
<a href="#actionModal" class="profile-action text-light align-self-center" data-action="chat" data-action-id="@Model.ID">
|
<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>
|
<i class="oi oi-chat ml-5 mr-5"></i>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"outputFileName": "wwwroot/js/global.min.js",
|
"outputFileName": "wwwroot/js/global.min.js",
|
||||||
"inputFiles": [
|
"inputFiles": [
|
||||||
"wwwroot/lib/jquery/dist/jquery.js",
|
"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/moment-timezone/moment-timezone.min.js",
|
||||||
"wwwroot/lib/chart.js/dist/Chart.bundle.min.js",
|
"wwwroot/lib/chart.js/dist/Chart.bundle.min.js",
|
||||||
"wwwroot/lib/halfmoon/js/halfmoon.min.js",
|
"wwwroot/lib/halfmoon/js/halfmoon.min.js",
|
||||||
|
@ -84,10 +84,6 @@
|
|||||||
border-bottom-color: var(--secondary-color);
|
border-bottom-color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#penalty_filter_selection {
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes rotation {
|
@-webkit-keyframes rotation {
|
||||||
from {
|
from {
|
||||||
-webkit-transform: rotate(359deg);
|
-webkit-transform: rotate(359deg);
|
||||||
|
@ -26,7 +26,7 @@ function setupPerformanceGraph() {
|
|||||||
}
|
}
|
||||||
const chart = $('#client_performance_history');
|
const chart = $('#client_performance_history');
|
||||||
const container = $('#client_performance_history_container');
|
const container = $('#client_performance_history_container');
|
||||||
chart.attr('height', summary.height());
|
chart.attr('height', summary.height() * 1.5);
|
||||||
chart.attr('width', container.width());
|
chart.attr('width', container.width());
|
||||||
renderPerformanceChart();
|
renderPerformanceChart();
|
||||||
}
|
}
|
||||||
@ -394,7 +394,7 @@ function renderPerformanceChart() {
|
|||||||
position: 'right',
|
position: 'right',
|
||||||
ticks: {
|
ticks: {
|
||||||
precision: 0,
|
precision: 0,
|
||||||
stepSize: 3,
|
stepSize: max - min / 2,
|
||||||
callback: function (value, index, values) {
|
callback: function (value, index, values) {
|
||||||
if (index === values.length - 1) {
|
if (index === values.length - 1) {
|
||||||
return min;
|
return min;
|
||||||
|
@ -88,7 +88,7 @@ function getStatsChart(id) {
|
|||||||
position: 'right',
|
position: 'right',
|
||||||
ticks: {
|
ticks: {
|
||||||
precision: 0,
|
precision: 0,
|
||||||
stepSize: 3,
|
stepSize: max - min / 2,
|
||||||
callback: function (value, index, values) {
|
callback: function (value, index, values) {
|
||||||
if (index === values.length - 1) {
|
if (index === values.length - 1) {
|
||||||
return min;
|
return min;
|
||||||
|
Reference in New Issue
Block a user