Compare commits

...

28 Commits

Author SHA1 Message Date
0a55c54c42 update to game interface/integration for persistent stat data 2022-07-13 16:10:16 -05:00
f43f7b5040 misc webfront tweaks 2022-07-10 21:06:58 -05:00
540cf7489d update pluto t6 parser for unknown ip 2022-07-10 20:09:57 -05:00
1a72faee60 add date stamp to performance graphs / increase number of performance rating snapshots / localize graph timestamps 2022-07-10 17:06:46 -05:00
4e44bb5ea1 fix rcon issue on restart 2022-07-09 20:57:00 -05:00
9e17bcc38f improve ban management display and additional translations 2022-07-09 16:32:23 -05:00
4b33b33d01 fix issue with alert on warn in game interface 2022-07-09 14:23:08 -05:00
6f1bc7ab90 cleanup table display of admins on mobile display 2022-07-09 13:54:35 -05:00
63e1774cb6 gracefully handle when infoString does not include all expected data 2022-07-09 10:52:27 -05:00
61df873bb1 more localization tweaks 2022-07-08 20:40:27 -05:00
052eeb0615 fix tag on welcome issue 2022-07-08 20:39:58 -05:00
88e67747fe add option to normalize diacritics for rcon parsers (applied to T6) 2022-07-06 15:42:31 -05:00
5db94723aa Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2022-07-06 10:02:09 -05:00
ea8216ecdf Add H1 maps and gametypes (#252) 2022-07-06 10:01:01 -05:00
6abbcbe464 prevent waiting for response on quit command 2022-07-06 09:55:06 -05:00
57484690b6 clean up display and uniformity of social icons 2022-07-06 09:49:44 -05:00
7a022a1973 fix grouping of commands on help page 2022-07-05 15:57:39 -05:00
7108e23a03 fix issue with context menu close not working on mobile 2022-07-05 15:15:25 -05:00
77d25890da clean up some more translations 2022-07-05 12:42:17 -05:00
2fca68a7ea update webfront translation strings 2022-07-05 12:02:43 -05:00
a6c0a94f6c support per-command override of rcon timeouts / update t5 parser to reflect 2022-07-01 09:59:11 -05:00
71abaac9e1 remove reports on ban/tempban 2022-07-01 09:14:57 -05:00
e07651b931 fix toast message issue on pages with query params 2022-06-28 10:03:05 -05:00
5a2ee36df9 use "unknown" ip as bot indicator 2022-06-28 09:15:37 -05:00
2daa4991d1 fix issue with previous change 2022-06-21 16:57:06 -05:00
775c0a91b5 small parser changes 2022-06-21 16:33:11 -05:00
55bccc7d3d ensure commands are not displayed/usable for unsupported games 2022-06-17 13:11:44 -05:00
4322e8d882 add migration logic for MySQL case sensitivity 2022-06-17 09:44:14 -05:00
84 changed files with 1175 additions and 588 deletions

View File

@ -61,7 +61,7 @@ namespace IW4MAdmin.Application
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource;
private CancellationTokenSource _tokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
@ -94,8 +94,8 @@ namespace IW4MAdmin.Application
ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow;
PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
AdditionalEventParsers = new List<IEventParser> { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication();
_logger = logger;
_tokenSource = new CancellationTokenSource();
@ -613,6 +613,7 @@ namespace IW4MAdmin.Application
{
IsRestartRequested = true;
Stop().GetAwaiter().GetResult();
_tokenSource = new CancellationTokenSource();
}
[Obsolete]

View File

@ -564,7 +564,56 @@
"Alias": "Momentum"
}
]
}
},
{
"Game": "H1",
"Gametypes": [
{
"Name": "conf",
"Alias": "Kill Confirmed"
},
{
"Name": "ctf",
"Alias": "Capture The Flag"
},
{
"Name": "dd",
"Alias": "Demolition"
},
{
"Name": "dm",
"Alias": "Free For All"
},
{
"Name": "dom",
"Alias": "Domination"
},
{
"Name": "gun",
"Alias": "Gun Game"
},
{
"Name": "hp",
"Alias": "Hardpoint"
},
{
"Name": "koth",
"Alias": "Headquarters"
},
{
"Name": "sab",
"Alias": "Sabotage"
},
{
"Name": "sd",
"Alias": "Search & Destroy"
},
{
"Name": "war",
"Alias": "Team Deathmatch"
}
]
}
],
"Maps": [
{
@ -1768,6 +1817,103 @@
}
]
},
{
"Game": "H1",
"Maps": [
{
"Alias": "Ambush",
"Name": "mp_convoy"
},
{
"Alias": "Backlot",
"Name": "mp_backlot"
},
{
"Alias": "Bloc",
"Name": "mp_bloc"
},
{
"Alias": "Bog",
"Name": "mp_bog"
},
{
"Alias": "Countdown",
"Name": "mp_countdown"
},
{
"Alias": "Crash",
"Name": "mp_crash"
},
{
"Alias": "Crossfire",
"Name": "mp_crossfire"
},
{
"Alias": "District",
"Name": "mp_citystreets"
},
{
"Alias": "Downpour",
"Name": "mp_farm"
},
{
"Alias": "Overgrown",
"Name": "mp_overgrown"
},
{
"Alias": "Pipeline",
"Name": "mp_pipeline"
},
{
"Alias": "Shipment",
"Name": "mp_shipment"
},
{
"Alias": "Showdown",
"Name": "mp_showdown"
},
{
"Alias": "Strike",
"Name": "mp_strike"
},
{
"Alias": "Vacant",
"Name": "mp_vacant"
},
{
"Alias": "Wet Work",
"Name": "mp_cargoship"
},
{
"Alias": "Winter Crash",
"Name": "mp_crash_snow"
},
{
"Alias": "Broadcast",
"Name": "mp_broadcast"
},
{
"Alias": "Creek",
"Name": "mp_creek"
},
{
"Alias": "Chinatown",
"Name": "mp_carentan"
},
{
"Alias": "Killhouse",
"Name": "mp_killhouse"
},
{
"Alias": "Day Break",
"Name": "mp_farm_spring"
},
{
"Alias": "Beach Bog",
"Name": "mp_bog_summer"
}
]
},
{
"Game": "CSGO",
"Maps": [

View File

@ -154,10 +154,10 @@ namespace IW4MAdmin
{
if (E.IsBlocking)
{
await E.Origin?.Lock();
await E.Origin.Lock();
}
bool canExecuteCommand = true;
var canExecuteCommand = true;
try
{
@ -166,30 +166,30 @@ namespace IW4MAdmin
return;
}
Command C = null;
Command command = null;
if (E.Type == GameEvent.EventType.Command)
{
try
{
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
command = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
}
catch (CommandException e)
{
ServerLogger.LogWarning(e, "Error validating command from event {@event}",
ServerLogger.LogWarning(e, "Error validating command from event {@Event}",
new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId });
E.FailReason = GameEvent.EventFailReason.Invalid;
}
if (C != null)
if (command != null)
{
E.Extra = C;
E.Extra = command;
}
}
try
{
var loginPlugin = Manager.Plugins.FirstOrDefault(_plugin => _plugin.Name == "Login");
var loginPlugin = Manager.Plugins.FirstOrDefault(plugin => plugin.Name == "Login");
if (loginPlugin != null)
{
@ -204,15 +204,15 @@ namespace IW4MAdmin
}
// hack: this prevents commands from getting executing that 'shouldn't' be
if (E.Type == GameEvent.EventType.Command && E.Extra is Command command &&
if (E.Type == GameEvent.EventType.Command && E.Extra is Command cmd &&
(canExecuteCommand || E.Origin?.Level == Permission.Console))
{
ServerLogger.LogInformation("Executing command {comamnd} for {client}", command.Name, E.Origin.ToString());
await command.ExecuteAsync(E);
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name, E.Origin.ToString());
await cmd.ExecuteAsync(E);
}
var pluginTasks = Manager.Plugins
.Where(_plugin => _plugin.Name != "Login")
.Where(plugin => plugin.Name != "Login")
.Select(async plugin => await CreatePluginTask(plugin, E));
await Task.WhenAll(pluginTasks);
@ -373,9 +373,9 @@ namespace IW4MAdmin
var clientTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
EFMeta.ClientTagNameV2, E.Origin.ClientId, Manager.CancellationToken);
if (clientTag?.LinkedMeta != null)
if (clientTag?.Value != null)
{
E.Origin.Tag = clientTag.LinkedMeta.Value;
E.Origin.Tag = clientTag.Value;
}
try
@ -689,23 +689,50 @@ namespace IW4MAdmin
else
{
Gametype = dict["gametype"];
Hostname = dict["hostname"];
if (dict.ContainsKey("gametype"))
{
Gametype = dict["gametype"];
}
string mapname = dict["mapname"] ?? CurrentMap.Name;
UpdateMap(mapname);
if (dict.ContainsKey("hostname"))
{
Hostname = dict["hostname"];
}
var newMapName = dict.ContainsKey("mapname")
? dict["mapname"] ?? CurrentMap.Name
: CurrentMap.Name;
UpdateMap(newMapName);
}
}
else
{
var dict = (Dictionary<string, string>) E.Extra;
Gametype = dict["g_gametype"];
Hostname = dict["sv_hostname"];
MaxClients = int.Parse(dict["sv_maxclients"]);
var dict = (Dictionary<string, string>)E.Extra;
if (dict.ContainsKey("g_gametype"))
{
Gametype = dict["g_gametype"];
}
string mapname = dict["mapname"];
UpdateMap(mapname);
if (dict.ContainsKey("sv_hostname"))
{
Hostname = dict["sv_hostname"];
}
if (dict.ContainsKey("sv_maxclients"))
{
MaxClients = int.Parse(dict["sv_maxclients"]);
}
else if (dict.ContainsKey("com_maxclients"))
{
MaxClients = int.Parse(dict["com_maxclients"]);
}
if (dict.ContainsKey("mapname"))
{
UpdateMap(dict["mapname"]);
}
}
if (E.GameTime.HasValue)
@ -741,6 +768,23 @@ namespace IW4MAdmin
{
E.Origin.UpdateTeam(E.Extra as string);
}
else if (E.Type == GameEvent.EventType.MetaUpdated)
{
if (E.Extra is "PersistentStatClientId" && int.TryParse(E.Data, out var persistentClientId))
{
var penalties = await Manager.GetPenaltyService().GetActivePenaltiesByClientId(persistentClientId);
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(), persistentClientId);
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(persistentClientId), Utilities.IW4MAdminClient(this), true);
}
}
}
lock (ChatHistory)
{
@ -1472,6 +1516,11 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Creating tempban penalty for {TargetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
foreach (var reports in Manager.GetServers().Select(server => server.Reports))
{
reports.RemoveAll(report => report.Target.ClientId == targetClient.ClientId);
}
if (activeClient.IsIngame)
{
@ -1502,6 +1551,11 @@ namespace IW4MAdmin
activeClient.SetLevel(Permission.Banned, originClient);
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
foreach (var reports in Manager.GetServers().Select(server => server.Reports))
{
reports.RemoveAll(report => report.Target.ClientId == targetClient.ClientId);
}
if (activeClient.IsIngame)
{
ServerLogger.LogDebug("Attempting to kicking newly banned client {ActiveClient}", activeClient.ToString());

View File

@ -10,6 +10,7 @@ namespace IW4MAdmin.Application.Misc
{
public event EventHandler<GameEvent> OnClientDisconnect;
public event EventHandler<GameEvent> OnClientConnect;
public event EventHandler<GameEvent> OnClientMetaUpdated;
private readonly ILogger _logger;
@ -33,6 +34,11 @@ namespace IW4MAdmin.Application.Misc
{
OnClientDisconnect?.Invoke(this, gameEvent);
}
if (gameEvent.Type == GameEvent.EventType.MetaUpdated)
{
OnClientMetaUpdated?.Invoke(this, gameEvent);
}
}
catch (Exception ex)

View File

@ -7,7 +7,10 @@ using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
@ -19,13 +22,15 @@ public class MetaServiceV2 : IMetaServiceV2
{
private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory)
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory, IServiceProvider serviceProvider)
{
_logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory;
_serviceProvider = serviceProvider;
}
public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId,
@ -64,6 +69,26 @@ public class MetaServiceV2 : IMetaServiceV2
}
await context.SaveChangesAsync(token);
var manager = _serviceProvider.GetRequiredService<IManager>();
var matchingClient = manager.GetActiveClients().FirstOrDefault(client => client.ClientId == clientId);
var server = matchingClient?.CurrentServer ?? manager.GetServers().FirstOrDefault();
if (server is not null)
{
manager.AddEvent(new GameEvent
{
Type = GameEvent.EventType.MetaUpdated,
Origin = matchingClient ?? new EFClient
{
ClientId = clientId
},
Data = metaValue,
Extra = metaKey,
Owner = server
});
}
}
public async Task SetPersistentMetaValue<T>(string metaKey, T metaValue, int clientId,

View File

@ -116,7 +116,8 @@ namespace IW4MAdmin.Application.Misc
typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly,
typeof(Utilities).Assembly,
typeof(Encoding).Assembly
typeof(Encoding).Assembly,
typeof(CancellationTokenSource).Assembly
})
.CatchClrExceptions()
.AddObjectConverter(new PermissionLevelToStringConverter()));

View File

@ -53,7 +53,7 @@ namespace IW4MAdmin.Application.RConParsers
Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7);
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n(?:latched: \"(.+)?\"\n)? *(.+)$";
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n?(?:latched: \"(.+)?\"\n?)? *(.+)$";
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
@ -82,7 +82,7 @@ namespace IW4MAdmin.Application.RConParsers
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default)
{
command = command.FormatMessageForEngine(Configuration?.ColorCodeMapping);
command = command.FormatMessageForEngine(Configuration);
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token);
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
}
@ -105,7 +105,7 @@ namespace IW4MAdmin.Application.RConParsers
lineSplit = Array.Empty<string>();
}
var response = string.Join('\n', lineSplit).TrimEnd('\0');
var response = string.Join('\n', lineSplit).Replace("\n", "").TrimEnd('\0');
var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (response.Contains("Unknown command") ||
@ -368,15 +368,28 @@ namespace IW4MAdmin.Application.RConParsers
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
default;
public TimeSpan OverrideTimeoutForCommand(string command)
public TimeSpan? OverrideTimeoutForCommand(string command)
{
if (command.Contains("map_rotate", StringComparison.InvariantCultureIgnoreCase) ||
command.StartsWith("map ", StringComparison.InvariantCultureIgnoreCase))
if (string.IsNullOrEmpty(command))
{
return TimeSpan.FromSeconds(30);
return TimeSpan.Zero;
}
var commandToken = command.Split(' ', StringSplitOptions.RemoveEmptyEntries).First().ToLower();
if (!Configuration.OverrideCommandTimeouts.ContainsKey(commandToken))
{
return TimeSpan.Zero;
}
return TimeSpan.Zero;
var timeoutValue = Configuration.OverrideCommandTimeouts[commandToken];
if (timeoutValue.HasValue && timeoutValue.Value != 0) // JINT doesn't seem to be able to properly set nulls on dictionaries
{
return TimeSpan.FromSeconds(timeoutValue.Value);
}
return null;
}
}
}

View File

@ -26,12 +26,14 @@ namespace IW4MAdmin.Application.RConParsers
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>();
public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>();
public IDictionary<string, int?> OverrideCommandTimeouts { get; set; } = new Dictionary<string, int?>();
public int NoticeMaximumLines { get; set; } = 8;
public int NoticeMaxCharactersPerLine { get; set; } = 50;
public string NoticeLineSeparator { get; set; } = Environment.NewLine;
public int? DefaultRConPort { get; set; }
public string DefaultInstallationDirectoryHint { get; set; }
public short FloodProtectInterval { get; set; } = 750;
public bool ShouldRemoveDiacritics { get; set; }
public ColorCodeMapping ColorCodeMapping { get; set; } = new ColorCodeMapping
{
@ -58,6 +60,25 @@ namespace IW4MAdmin.Application.RConParsers
StatusHeader = parserRegexFactory.CreateParserRegex();
HostnameStatus = parserRegexFactory.CreateParserRegex();
MaxPlayersStatus = parserRegexFactory.CreateParserRegex();
const string mapRotateCommand = "map_rotate";
const string mapCommand = "map";
const string fastRestartCommand = "fast_restart";
const string quitCommand = "quit";
foreach (var command in new[] { mapRotateCommand, mapCommand, fastRestartCommand})
{
if (!OverrideCommandTimeouts.ContainsKey(command))
{
OverrideCommandTimeouts.Add(command, 45);
}
}
if (!OverrideCommandTimeouts.ContainsKey(quitCommand))
{
OverrideCommandTimeouts.Add(quitCommand, 0); // we don't want to wait for a response when we quit the server
}
}
}
}

View File

@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@ -8,12 +9,19 @@ namespace Data.Migrations.MySql
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"create index IX_EFClientMessages_TimeSentDesc on efclientmessages (TimeSent desc);");
try
{
migrationBuilder.Sql(@"create index IX_EFClientMessages_TimeSentDesc on EFClientMessages (TimeSent desc);");
}
catch
{
migrationBuilder.Sql(@"create index IX_EFClientMessages_TimeSentDesc on efclientmessages (TimeSent desc);");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"drop index IX_EFClientMessages_TimeSentDesc on efclientmessages;");
migrationBuilder.Sql(@"drop index IX_EFClientMessages_TimeSentDesc on EFClientMessages;");
}
}
}

View File

@ -7,7 +7,7 @@ namespace Data.Models.Client.Stats
{
public class EFClientRankingHistory: AuditFields
{
public const int MaxRankingCount = 30;
public const int MaxRankingCount = 1728;
[Key]
public long ClientRankingHistoryId { get; set; }
@ -28,4 +28,4 @@ namespace Data.Models.Client.Stats
public double? ZScore { get; set; }
public double? PerformanceMetric { get; set; }
}
}
}

View File

@ -53,8 +53,6 @@ init()
level thread OnPlayerConnect();
}
//////////////////////////////////
// Client Methods
//////////////////////////////////
@ -167,6 +165,28 @@ DisplayWelcomeData()
self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection );
}
SetPersistentData()
{
storedClientId = self GetPlayerData( "bests", "none" );
if ( storedClientId != 0 )
{
if ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn( "Uploading persistent client id " + storedClientId );
}
SetClientMeta( "PersistentStatClientId", storedClientId );
}
if ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn( "Persisting client id " + self.persistentClientId );
}
self SetPlayerData( "bests", "none", int( self.persistentClientId ) );
}
PlayerConnectEvents()
{
self endon( "disconnect" );
@ -643,6 +663,7 @@ OnClientDataReceived( event )
self.persistentClientId = event.data["clientId"];
self thread DisplayWelcomeData();
self setPersistentData();
}
OnExecuteCommand( event )

View File

@ -253,8 +253,10 @@ namespace Integrations.Cod
try
{
connectionState.LastQuery = DateTime.Now;
var timeout = _parser.OverrideTimeoutForCommand(parameters);
waitForResponse = waitForResponse && timeout.HasValue;
response = await SendPayloadAsync(payload, waitForResponse,
_parser.OverrideTimeoutForCommand(parameters), token);
timeout ?? TimeSpan.Zero, token);
if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse)
{
@ -456,6 +458,12 @@ namespace Integrations.Cod
connectionState.SendEventArgs.DisconnectReuseSocket = true;
}
if (connectionState.ReceiveEventArgs.UserToken is ConnectionUserToken { CancellationToken.IsCancellationRequested: true })
{
// after a graceful restart we need to reset the receive user token as the cancellation has been updated
connectionState.ReceiveEventArgs.UserToken = connectionState.SendEventArgs.UserToken;
}
connectionState.SendEventArgs.SetBuffer(payload);
// send the data to the server

View File

@ -46,7 +46,7 @@ let plugin = {
break;
case 'warn':
const warningTitle = _localization.LocalizationIndex['GLOBAL_WARNING'];
sendScriptCommand(server, 'Alert', gameEvent.Target, {
sendScriptCommand(server, 'Alert', gameEvent.Origin, gameEvent.Target, {
alertType: warningTitle + '!',
message: gameEvent.Data
});
@ -463,7 +463,11 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
if (input.length > 0) {
const event = parseEvent(input)
logger.WriteDebug(`Processing input... ${event.eventType} ${event.subType} ${event.data} ${event.clientNumber}`);
logger.WriteDebug(`Processing input... ${event.eventType} ${event.subType} ${event.data.toString()} ${event.clientNumber}`);
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
const threading = importNamespace('System.Threading');
const token = new threading.CancellationTokenSource().Token;
// todo: refactor to mapping if possible
if (event.eventType === 'ClientDataRequested') {
@ -475,8 +479,8 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
let data = [];
if (event.subType === 'Meta') {
const metaService = _serviceResolver.ResolveService('IMetaService');
const meta = metaService.GetPersistentMeta(event.data, client).GetAwaiter().GetResult();
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
const meta = metaService.GetPersistentMeta(event.data, client, token).GetAwaiter().GetResult();
data[event.data] = meta === null ? '' : meta.Value;
} else {
data = {
@ -510,19 +514,19 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Fail'});
} else {
if (event.subType === 'Meta') {
const metaService = _serviceResolver.ResolveService('IMetaService');
try {
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}`);
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).GetAwaiter().GetResult()
: metaService.DecrementPersistentMeta(event.data['key'], event.data['value'], clientId).GetAwaiter().GetResult();
? metaService.IncrementPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult()
: metaService.DecrementPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult();
} else {
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId).GetAwaiter().GetResult();
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult();
}
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Complete'});
} catch (error) {
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Fail'});
logger.WriteError('Could not persist client meta ' + error.toString());
}
}
}

View File

@ -20,7 +20,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)? *(.+)$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
rconParser.Configuration.WaitForResponse = false;

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax, Xerxes',
version: 1.2,
version: 1.4,
name: 'Plutonium T6 Parser',
isParser: true,
@ -29,9 +29,10 @@ var plugin = {
rconParser.Configuration.NoticeLineSeparator = '. ';
rconParser.Configuration.DefaultRConPort = 4976;
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t6';
rconParser.Configuration.ShouldRemoveDiacritics = true;
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+|0) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+|0) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback|unknown) +(?:-?[0-9]+) +(?:[0-9]+) *$';
rconParser.Configuration.Status.AddMapping(100, 1);
rconParser.Configuration.Status.AddMapping(101, 2);
rconParser.Configuration.Status.AddMapping(102, 3);

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.1,
version: 0.2,
name: 'Plutonium T5 Parser',
isParser: true,
@ -23,6 +23,11 @@ var plugin = {
rconParser.Configuration.DefaultRConPort = 3074;
rconParser.Configuration.CanGenerateLogPath = false;
rconParser.Configuration.OverrideCommandTimeouts.Clear();
rconParser.Configuration.OverrideCommandTimeouts.Add('map', 0);
rconParser.Configuration.OverrideCommandTimeouts.Add('map_rotate', 0);
rconParser.Configuration.OverrideCommandTimeouts.Add('fast_restart', 0);
rconParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
rconParser.GameName = 6; // T5
eventParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';

View File

@ -17,7 +17,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'kickClient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'kickClient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)? *(.+)$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
rconParser.Configuration.Status.AddMapping(102, 4);
@ -37,4 +37,4 @@ var plugin = {
onUnloadAsync: function() {},
onTickAsync: function(server) {}
};
};

View File

@ -19,8 +19,14 @@ namespace IW4MAdmin.Plugins.Stats.Web.Dtos
public int Kills { get; set; }
public int Deaths { get; set; }
public int RatingChange { get; set; }
public List<double> PerformanceHistory { get; set; }
public List<PerformanceHistory> PerformanceHistory { get; set; }
public double? ZScore { get; set; }
public long? ServerId { get; set; }
}
public class PerformanceHistory
{
public double? Performance { get; set; }
public DateTime OccurredAt { get; set; }
}
}

View File

@ -79,6 +79,7 @@ namespace Stats.Helpers
.Where(r => r.ServerId == serverId)
.Where(r => r.Ranking != null)
.OrderByDescending(r => r.UpdatedDateTime)
.Take(250)
.ToListAsync();
var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest);

View File

@ -188,29 +188,32 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var finished = statsInfo
.OrderByDescending(stat => rankingsDict[stat.ClientId].Last().PerformanceMetric)
.Select((s, index) => new TopStatsInfo()
{
ClientId = s.ClientId,
Id = (int?) serverId ?? 0,
Deaths = s.Deaths,
Kills = s.Kills,
KDR = Math.Round(s.KDR, 2),
LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection))
.HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false),
LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().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,
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => ranking.PerformanceMetric ?? 0).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,
ServerId = serverId
})
.OrderBy(r => r.Ranking)
.ToList();
.Select((s, index) => new TopStatsInfo
{
ClientId = s.ClientId,
Id = (int?)serverId ?? 0,
Deaths = s.Deaths,
Kills = s.Kills,
KDR = Math.Round(s.KDR, 2),
LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection))
.HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false),
LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().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,
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,
ServerId = serverId
})
.OrderBy(r => r.Ranking)
.Take(60)
.ToList();
return finished;
}
@ -289,7 +292,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var finished = topPlayers.Select(s => new TopStatsInfo()
{
ClientId = s.ClientId,
Id = (int?) serverId ?? 0,
Id = (int?)serverId ?? 0,
Deaths = s.Deaths,
Kills = s.Kills,
KDR = Math.Round(s.KDR, 2),
@ -302,9 +305,19 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1
? ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When)
.Select(r => r.Performance).ToList()
: new List<double>()
{clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance},
.Select(r => new PerformanceHistory { Performance = r.Performance, OccurredAt = r.When })
.ToList()
: new List<PerformanceHistory>
{
new()
{
Performance = clientRatingsDict[s.ClientId].Performance, OccurredAt = DateTime.UtcNow
},
new()
{
Performance = clientRatingsDict[s.ClientId].Performance, OccurredAt = DateTime.UtcNow
}
},
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed)
})

View File

@ -53,7 +53,7 @@ namespace IW4MAdmin.Plugins.Welcome
{
var newPlayer = gameEvent.Origin;
if (newPlayer.Level >= Permission.Trusted && !gameEvent.Origin.Masked||
!string.IsNullOrEmpty(newPlayer.GetAdditionalProperty<string>("ClientTag")) &&
!string.IsNullOrEmpty(newPlayer.Tag) &&
newPlayer.Level != Permission.Flagged && newPlayer.Level != Permission.Banned &&
!newPlayer.Masked)
gameEvent.Owner.Broadcast(
@ -88,7 +88,7 @@ namespace IW4MAdmin.Plugins.Welcome
{
msg = msg.Replace("{{ClientName}}", joining.Name);
msg = msg.Replace("{{ClientLevel}}",
$"{Utilities.ConvertLevelToColor(joining.Level, joining.ClientPermission.Name)}{(string.IsNullOrEmpty(joining.GetAdditionalProperty<string>("ClientTag")) ? "" : $" (Color::White)({joining.GetAdditionalProperty<string>("ClientTag")}(Color::White))")}");
$"{Utilities.ConvertLevelToColor(joining.Level, joining.ClientPermission.Name)}{(string.IsNullOrEmpty(joining.Tag) ? "" : $" (Color::White){joining.Tag}(Color::White)")}");
// this prevents it from trying to evaluate it every message
if (msg.Contains("{{ClientLocation}}"))
{
@ -111,7 +111,7 @@ namespace IW4MAdmin.Plugins.Welcome
try
{
var response =
await wc.GetStringAsync(new Uri($"http://ip-api.com/json/{ip}"));
await wc.GetStringAsync(new Uri($"http://ip-api.com/json/{ip}?lang={Utilities.CurrentLocalization.LocalizationName.Split("-").First().ToLower()}"));
var responseObj = JObject.Parse(response);
response = responseObj["country"]?.ToString();

View File

@ -11,163 +11,180 @@ namespace SharedLibraryCore.Commands
{
public class CommandProcessing
{
public static async Task<Command> ValidateCommand(GameEvent E, ApplicationConfiguration appConfig,
public static async Task<Command> ValidateCommand(GameEvent gameEvent, ApplicationConfiguration appConfig,
CommandConfiguration commandConfig)
{
var loc = Utilities.CurrentLocalization.LocalizationIndex;
var Manager = E.Owner.Manager;
var isBroadcast = E.Data.StartsWith(appConfig.BroadcastCommandPrefix);
var manager = gameEvent.Owner.Manager;
var isBroadcast = gameEvent.Data.StartsWith(appConfig.BroadcastCommandPrefix);
var prefixLength = isBroadcast ? appConfig.BroadcastCommandPrefix.Length : appConfig.CommandPrefix.Length;
var CommandString = E.Data.Substring(prefixLength, E.Data.Length - prefixLength).Split(' ')[0];
E.Message = E.Data;
var commandString =
gameEvent.Data.Substring(prefixLength, gameEvent.Data.Length - prefixLength).Split(' ')[0];
gameEvent.Message = gameEvent.Data;
Command C = null;
foreach (Command cmd in Manager.GetCommands()
Command matchedCommand = null;
foreach (var availableCommand in manager.GetCommands()
.Where(c => c.Name != null))
if (cmd.Name.Equals(CommandString, StringComparison.OrdinalIgnoreCase) ||
(cmd.Alias ?? "").Equals(CommandString, StringComparison.OrdinalIgnoreCase))
{
if ((availableCommand.SupportedGames?.Any() ?? false) &&
!availableCommand.SupportedGames.Contains(gameEvent.Owner.GameName))
{
C = cmd;
continue;
}
if (C == null)
{
E.Origin.Tell(loc["COMMAND_UNKNOWN"]);
throw new CommandException($"{E.Origin} entered unknown command \"{CommandString}\"");
}
C.IsBroadcast = isBroadcast;
var allowImpersonation = commandConfig?.Commands?.ContainsKey(C.GetType().Name) ?? false
? commandConfig.Commands[C.GetType().Name].AllowImpersonation
: C.AllowImpersonation;
if (!allowImpersonation && E.ImpersonationOrigin != null)
{
E.ImpersonationOrigin.Tell(loc["COMMANDS_RUN_AS_FAIL"]);
throw new CommandException($"Command {C.Name} cannot be run as another client");
}
E.Data = E.Data.RemoveWords(1);
var Args = E.Data.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
// todo: the code below can be cleaned up
if (E.Origin.Level < C.Permission)
{
E.Origin.Tell(loc["COMMAND_NOACCESS"]);
throw new CommandException($"{E.Origin} does not have access to \"{C.Name}\"");
}
if (Args.Length < C.RequiredArgumentCount)
{
E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
E.Origin.Tell(C.Syntax);
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
}
if (C.RequiresTarget)
{
if (Args.Length > 0)
if (availableCommand.Name.Equals(commandString, StringComparison.OrdinalIgnoreCase) ||
(availableCommand.Alias ?? "").Equals(commandString, StringComparison.OrdinalIgnoreCase))
{
if (!int.TryParse(Args[0], out var cNum))
matchedCommand = (Command)availableCommand;
}
}
if (matchedCommand == null)
{
gameEvent.Origin.Tell(loc["COMMAND_UNKNOWN"]);
throw new CommandException($"{gameEvent.Origin} entered unknown command \"{commandString}\"");
}
matchedCommand.IsBroadcast = isBroadcast;
var allowImpersonation = commandConfig?.Commands?.ContainsKey(matchedCommand.GetType().Name) ?? false
? commandConfig.Commands[matchedCommand.GetType().Name].AllowImpersonation
: matchedCommand.AllowImpersonation;
if (!allowImpersonation && gameEvent.ImpersonationOrigin != null)
{
gameEvent.ImpersonationOrigin.Tell(loc["COMMANDS_RUN_AS_FAIL"]);
throw new CommandException($"Command {matchedCommand.Name} cannot be run as another client");
}
gameEvent.Data = gameEvent.Data.RemoveWords(1);
var args = gameEvent.Data.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
// todo: the code below can be cleaned up
if (gameEvent.Origin.Level < matchedCommand.Permission)
{
gameEvent.Origin.Tell(loc["COMMAND_NOACCESS"]);
throw new CommandException($"{gameEvent.Origin} does not have access to \"{matchedCommand.Name}\"");
}
if (args.Length < matchedCommand.RequiredArgumentCount)
{
gameEvent.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
gameEvent.Origin.Tell(matchedCommand.Syntax);
throw new CommandException(
$"{gameEvent.Origin} did not supply enough arguments for \"{matchedCommand.Name}\"");
}
if (matchedCommand.RequiresTarget)
{
if (args.Length > 0)
{
if (!int.TryParse(args[0], out var cNum))
{
cNum = -1;
}
if (Args[0][0] == '@') // user specifying target by database ID
if (args[0][0] == '@') // user specifying target by database ID
{
int.TryParse(Args[0].Substring(1, Args[0].Length - 1), out var dbID);
int.TryParse(args[0].Substring(1, args[0].Length - 1), out var dbID);
var found = await Manager.GetClientService().Get(dbID);
var found = await manager.GetClientService().Get(dbID);
if (found != null)
{
found = Manager.FindActiveClient(found);
E.Target = found;
E.Target.CurrentServer = found.CurrentServer ?? E.Owner;
E.Data = string.Join(" ", Args.Skip(1));
found = manager.FindActiveClient(found);
gameEvent.Target = found;
gameEvent.Target.CurrentServer = found.CurrentServer ?? gameEvent.Owner;
gameEvent.Data = string.Join(" ", args.Skip(1));
}
}
else if (Args[0].Length < 3 && cNum > -1 && cNum < E.Owner.MaxClients
else if (args[0].Length < 3 && cNum > -1 && cNum < gameEvent.Owner.MaxClients
) // user specifying target by client num
{
if (E.Owner.Clients[cNum] != null)
if (gameEvent.Owner.Clients[cNum] != null)
{
E.Target = E.Owner.Clients[cNum];
E.Data = string.Join(" ", Args.Skip(1));
gameEvent.Target = gameEvent.Owner.Clients[cNum];
gameEvent.Data = string.Join(" ", args.Skip(1));
}
}
}
List<EFClient> matchingPlayers;
if (E.Target == null && C.RequiresTarget) // Find active player including quotes (multiple words)
if (gameEvent.Target == null &&
matchedCommand.RequiresTarget) // Find active player including quotes (multiple words)
{
matchingPlayers = E.Owner.GetClientByName(E.Data);
matchingPlayers = gameEvent.Owner.GetClientByName(gameEvent.Data);
if (matchingPlayers.Count > 1)
{
E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
throw new CommandException($"{E.Origin} had multiple players found for {C.Name}");
gameEvent.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
throw new CommandException(
$"{gameEvent.Origin} had multiple players found for {matchedCommand.Name}");
}
if (matchingPlayers.Count == 1)
{
E.Target = matchingPlayers.First();
gameEvent.Target = matchingPlayers.First();
var escapedName = Regex.Escape(E.Target.CleanedName);
var escapedName = Regex.Escape(gameEvent.Target.CleanedName);
var reg = new Regex($"(\"{escapedName}\")|({escapedName})", RegexOptions.IgnoreCase);
E.Data = reg.Replace(E.Data, "", 1).Trim();
gameEvent.Data = reg.Replace(gameEvent.Data, "", 1).Trim();
if (E.Data.Length == 0 && C.RequiredArgumentCount > 1)
if (gameEvent.Data.Length == 0 && matchedCommand.RequiredArgumentCount > 1)
{
E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
E.Origin.Tell(C.Syntax);
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
gameEvent.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
gameEvent.Origin.Tell(matchedCommand.Syntax);
throw new CommandException(
$"{gameEvent.Origin} did not supply enough arguments for \"{matchedCommand.Name}\"");
}
}
}
if (E.Target == null && C.RequiresTarget && Args.Length > 0) // Find active player as single word
if (gameEvent.Target == null && matchedCommand.RequiresTarget &&
args.Length > 0) // Find active player as single word
{
matchingPlayers = E.Owner.GetClientByName(Args[0]);
matchingPlayers = gameEvent.Owner.GetClientByName(args[0]);
if (matchingPlayers.Count > 1)
{
E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
gameEvent.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
foreach (var p in matchingPlayers)
E.Origin.Tell($"[(Color::Yellow){p.ClientNumber}(Color::White)] {p.Name}");
throw new CommandException($"{E.Origin} had multiple players found for {C.Name}");
gameEvent.Origin.Tell($"[(Color::Yellow){p.ClientNumber}(Color::White)] {p.Name}");
throw new CommandException(
$"{gameEvent.Origin} had multiple players found for {matchedCommand.Name}");
}
if (matchingPlayers.Count == 1)
{
E.Target = matchingPlayers.First();
gameEvent.Target = matchingPlayers.First();
var escapedName = Regex.Escape(E.Target.CleanedName);
var escapedArg = Regex.Escape(Args[0]);
var escapedName = Regex.Escape(gameEvent.Target.CleanedName);
var escapedArg = Regex.Escape(args[0]);
var reg = new Regex($"({escapedName})|({escapedArg})", RegexOptions.IgnoreCase);
E.Data = reg.Replace(E.Data, "", 1).Trim();
gameEvent.Data = reg.Replace(gameEvent.Data, "", 1).Trim();
if ((E.Data.Trim() == E.Target.CleanedName.ToLower().Trim() ||
E.Data == string.Empty) &&
C.RequiresTarget)
if ((gameEvent.Data.Trim() == gameEvent.Target.CleanedName.ToLower().Trim() ||
gameEvent.Data == string.Empty) &&
matchedCommand.RequiresTarget)
{
E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
E.Origin.Tell(C.Syntax);
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
gameEvent.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
gameEvent.Origin.Tell(matchedCommand.Syntax);
throw new CommandException(
$"{gameEvent.Origin} did not supply enough arguments for \"{matchedCommand.Name}\"");
}
}
}
if (E.Target == null && C.RequiresTarget)
if (gameEvent.Target == null && matchedCommand.RequiresTarget)
{
E.Origin.Tell(loc["COMMAND_TARGET_NOTFOUND"]);
throw new CommandException($"{E.Origin} specified invalid player for \"{C.Name}\"");
gameEvent.Origin.Tell(loc["COMMAND_TARGET_NOTFOUND"]);
throw new CommandException(
$"{gameEvent.Origin} specified invalid player for \"{matchedCommand.Name}\"");
}
}
E.Data = E.Data.Trim();
return C;
gameEvent.Data = gameEvent.Data.Trim();
return matchedCommand;
}
}
}
}

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using static Data.Models.Client.EFClient;
using static SharedLibraryCore.Server;
@ -35,6 +36,6 @@ namespace SharedLibraryCore.Configuration
/// Specifies the games supporting the functionality of the command
/// </summary>
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
public Game[] SupportedGames { get; set; } = new Game[0];
public Game[] SupportedGames { get; set; } = Array.Empty<Game>();
}
}
}

View File

@ -204,6 +204,11 @@ namespace SharedLibraryCore
/// client logged out of webfront
/// </summary>
Logout = 113,
/// <summary>
/// meta value updated on client
/// </summary>
MetaUpdated = 114,
// events "generated" by IW4MAdmin
/// <summary>

View File

@ -110,6 +110,6 @@ namespace SharedLibraryCore.Interfaces
/// </summary>
/// <param name="command">name of command being executed</param>
/// <returns></returns>
TimeSpan OverrideTimeoutForCommand(string command);
TimeSpan? OverrideTimeoutForCommand(string command);
}
}

View File

@ -74,6 +74,11 @@ namespace SharedLibraryCore.Interfaces
/// </summary>
IDictionary<string, string> DefaultDvarValues { get; }
/// <summary>
/// contains a setup of commands that have override timeouts
/// </summary>
IDictionary<string, int?> OverrideCommandTimeouts { get; }
/// <summary>
/// specifies how many lines can be used for ingame notice
/// </summary>
@ -100,7 +105,11 @@ namespace SharedLibraryCore.Interfaces
string DefaultInstallationDirectoryHint { get; }
ColorCodeMapping ColorCodeMapping { get; }
short FloodProtectInterval { get; }
/// <summary>
/// indicates if diacritics (accented characters) should be normalized
/// </summary>
bool ShouldRemoveDiacritics { get; }
}
}

View File

@ -100,7 +100,10 @@ namespace SharedLibraryCore.Database.Models
[NotMapped] public int Score { get; set; }
[NotMapped] public bool IsBot => NetworkId == Name.GenerateGuidFromString() || IPAddressString == System.Net.IPAddress.Broadcast.ToString();
[NotMapped]
public bool IsBot => NetworkId == Name.GenerateGuidFromString() ||
IPAddressString == System.Net.IPAddress.Broadcast.ToString() ||
IPAddressString == "unknown";
[NotMapped] public bool IsZombieClient => IsBot && Name == "Zombie";
@ -170,7 +173,7 @@ namespace SharedLibraryCore.Database.Models
?.CorrelationId ?? Guid.NewGuid()
};
e.Output.Add(message.FormatMessageForEngine(CurrentServer?.RconParser.Configuration.ColorCodeMapping)
e.Output.Add(message.FormatMessageForEngine(CurrentServer?.RconParser.Configuration)
.StripColors());
CurrentServer?.Manager.AddEvent(e);

View File

@ -225,7 +225,7 @@ namespace SharedLibraryCore
var formattedMessage = string.Format(RconParser.Configuration.CommandPrefixes.Say ?? "",
$"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{message}");
ServerLogger.LogDebug("All-> {Message}",
message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping).StripColors());
message.FormatMessageForEngine(RconParser.Configuration).StripColors());
var e = new GameEvent
{
@ -289,13 +289,13 @@ namespace SharedLibraryCore
else
{
ServerLogger.LogDebug("Tell[{ClientNumber}]->{Message}", targetClient.ClientNumber,
message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping).StripColors());
message.FormatMessageForEngine(RconParser.Configuration).StripColors());
}
if (targetClient.Level == Data.Models.Client.EFClient.Permission.Console)
{
Console.ForegroundColor = ConsoleColor.Green;
var cleanMessage = message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping)
var cleanMessage = message.FormatMessageForEngine(RconParser.Configuration)
.StripColors();
using (LogContext.PushProperty("Server", ToString()))
{

View File

@ -193,6 +193,16 @@ namespace SharedLibraryCore.Services
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);

View File

@ -71,31 +71,7 @@ namespace SharedLibraryCore
/// </summary>
public const long WORLD_ID = -1;
public static Dictionary<Permission, string> PermissionLevelOverrides { get; } =
new Dictionary<Permission, string>();
public static string HttpRequest(string location, string header, string headerValue)
{
using (var RequestClient = new HttpClient())
{
RequestClient.DefaultRequestHeaders.Add(header, headerValue);
var response = RequestClient.GetStringAsync(location).Result;
return response;
}
}
//Get string with specified number of spaces -- really only for visual output
public static string GetSpaces(int Num)
{
var SpaceString = string.Empty;
while (Num > 0)
{
SpaceString += ' ';
Num--;
}
return SpaceString;
}
public static Dictionary<Permission, string> PermissionLevelOverrides { get; } = new ();
//Remove words from a space delimited string
public static string RemoveWords(this string str, int num)
@ -133,12 +109,12 @@ namespace SharedLibraryCore
{
var lookingFor = str.ToLower();
for (var Perm = Permission.User; Perm < Permission.Console; Perm++)
if (lookingFor.Contains(Perm.ToString().ToLower())
for (var perm = Permission.User; perm < Permission.Console; perm++)
if (lookingFor.Contains(perm.ToString().ToLower())
|| lookingFor.Contains(CurrentLocalization
.LocalizationIndex[$"GLOBAL_PERMISSION_{Perm.ToString().ToUpper()}"].ToLower()))
.LocalizationIndex[$"GLOBAL_PERMISSION_{perm.ToString().ToUpper()}"].ToLower()))
{
return Perm;
return perm;
}
return Permission.Banned;
@ -171,9 +147,25 @@ namespace SharedLibraryCore
return str.Replace("//", "/ /");
}
public static string FormatMessageForEngine(this string str, ColorCodeMapping mapping)
public static string RemoveDiacritics(this string text)
{
if (mapping == null || string.IsNullOrEmpty(str))
var normalizedString = text.Normalize(NormalizationForm.FormD);
var stringBuilder = new StringBuilder();
foreach (var c in from c in normalizedString.EnumerateRunes()
let unicodeCategory = Rune.GetUnicodeCategory(c)
where unicodeCategory != UnicodeCategory.NonSpacingMark
select c)
{
stringBuilder.Append(c);
}
return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
}
public static string FormatMessageForEngine(this string str, IRConParserConfiguration config)
{
if (config == null || string.IsNullOrEmpty(str))
{
return str;
}
@ -184,13 +176,19 @@ namespace SharedLibraryCore
foreach (var match in colorCodeMatches.Where(m => m.Success))
{
var key = match.Groups[1].ToString();
output = output.Replace(match.Value, mapping.TryGetValue(key, out var code) ? code : "");
output = output.Replace(match.Value, config.ColorCodeMapping.TryGetValue(key, out var code) ? code : "");
}
if (config.ShouldRemoveDiacritics)
{
output = output.RemoveDiacritics();
}
return output.FixIW4ForwardSlash();
}
private static readonly IList<string> _zmGameTypes = new[] { "zclassic", "zstandard", "zcleansed", "zgrief" };
private static readonly IList<string> ZmGameTypes = new[]
{ "zclassic", "zstandard", "zcleansed", "zgrief", "zom", "cmp" };
/// <summary>
/// indicates if the given server is running a zombie game mode
@ -199,7 +197,8 @@ namespace SharedLibraryCore
/// <returns></returns>
public static bool IsZombieServer(this Server server)
{
return server.GameName == Game.T6 && _zmGameTypes.Contains(server.Gametype.ToLower());
return new[] { Game.T4, Game.T5, Game.T6 }.Contains(server.GameName) &&
ZmGameTypes.Contains(server.Gametype.ToLower());
}
public static bool IsCodGame(this Server server)
@ -233,7 +232,7 @@ namespace SharedLibraryCore
{
var localized =
CurrentLocalization.LocalizationIndex[$"GLOBAL_PERMISSION_{permission.ToString().ToUpper()}"];
return PermissionLevelOverrides.ContainsKey(permission) && PermissionLevelOverrides[permission] != localized
return PermissionLevelOverrides.ContainsKey(permission) && PermissionLevelOverrides[permission] != permission.ToString()
? PermissionLevelOverrides[permission]
: localized;
}
@ -262,11 +261,6 @@ namespace SharedLibraryCore
return str.StartsWith(broadcastCommandPrefix);
}
public static IManagerCommand AsCommand(this GameEvent gameEvent)
{
return gameEvent.Extra as IManagerCommand;
}
/// <summary>
/// Get the full gametype name
/// </summary>
@ -433,7 +427,7 @@ namespace SharedLibraryCore
{
var success = IPAddress.TryParse(str, out var ip);
return success && ip.GetAddressBytes().Count(_byte => _byte == 0) != 4
? (int?)BitConverter.ToInt32(ip.GetAddressBytes(), 0)
? BitConverter.ToInt32(ip.GetAddressBytes(), 0)
: null;
}
@ -482,11 +476,6 @@ namespace SharedLibraryCore
return Game.UKN;
}
public static string EscapeMarkdown(this string markdownString)
{
return markdownString.Replace("<", "\\<").Replace(">", "\\>").Replace("|", "\\|");
}
public static TimeSpan ParseTimespan(this string input)
{
var expressionMatch = Regex.Match(input, @"([0-9]+)(\w+)");
@ -608,7 +597,7 @@ namespace SharedLibraryCore
public static bool PromptBool(this string question, string description = null, bool defaultValue = true)
{
Console.Write($"{question}?{(string.IsNullOrEmpty(description) ? " " : $" ({description}) ")}[y/n]: ");
var response = Console.ReadLine().ToLower().FirstOrDefault();
var response = Console.ReadLine()?.ToLower().FirstOrDefault();
return response != 0 ? response == 'y' : defaultValue;
}
@ -639,7 +628,7 @@ namespace SharedLibraryCore
Console.WriteLine(new string('=', 52));
var selectionIndex = PromptInt(CurrentLocalization.LocalizationIndex["SETUP_PROMPT_MAKE_SELECTION"], null,
hasDefault ? 0 : 1, selections.Length, hasDefault ? 0 : (int?)null);
hasDefault ? 0 : 1, selections.Length, hasDefault ? 0 : null);
if (!hasDefault)
{
@ -667,13 +656,13 @@ namespace SharedLibraryCore
$"{question}{(string.IsNullOrEmpty(description) ? "" : $" ({description})")}{(defaultValue == null ? "" : $" [{CurrentLocalization.LocalizationIndex["SETUP_PROMPT_DEFAULT"]} {defaultValue.Value.ToString()}]")}: ");
int response;
string inputOrDefault()
string InputOrDefault()
{
var input = Console.ReadLine();
return string.IsNullOrEmpty(input) && defaultValue != null ? defaultValue.ToString() : input;
}
while (!int.TryParse(inputOrDefault(), out response) ||
while (!int.TryParse(InputOrDefault(), out response) ||
response < minValue ||
response > maxValue)
{
@ -698,7 +687,7 @@ namespace SharedLibraryCore
/// <returns></returns>
public static string PromptString(this string question, string description = null, string defaultValue = null)
{
string inputOrDefault()
string InputOrDefault()
{
var input = Console.ReadLine();
return string.IsNullOrEmpty(input) && defaultValue != null ? defaultValue : input;
@ -709,7 +698,7 @@ namespace SharedLibraryCore
{
Console.Write(
$"{question}{(string.IsNullOrEmpty(description) ? "" : $" ({description})")}{(defaultValue == null ? "" : $" [{CurrentLocalization.LocalizationIndex["SETUP_PROMPT_DEFAULT"]} {defaultValue}]")}: ");
response = inputOrDefault();
response = InputOrDefault();
} while (string.IsNullOrWhiteSpace(response) && response != defaultValue);
return response;

View File

@ -24,7 +24,7 @@ namespace WebfrontCore.Controllers
{
if (clientId == 0 || string.IsNullOrEmpty(password))
{
return Unauthorized("Invalid credentials");
return Unauthorized(Localization["WEBFRONT_ACTION_LOGIN_ERROR"]);
}
try
@ -73,16 +73,16 @@ namespace WebfrontCore.Controllers
: HttpContext.Connection.RemoteIpAddress?.ToString()
});
return Ok($"Welcome {privilegedClient.Name}. You are now logged in");
return Ok(Localization["WEBFRONT_ACTION_LOGIN_SUCCESS"].FormatExt(privilegedClient.CleanedName));
}
}
catch (Exception)
{
return Unauthorized("Could not validate credentials");
return Unauthorized(Localization["WEBFRONT_ACTION_LOGIN_ERROR"]);
}
return Unauthorized("Invalid credentials");
return Unauthorized(Localization["WEBFRONT_ACTION_LOGIN_ERROR"]);
}
[HttpGet]

View File

@ -78,7 +78,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_BAN_NAME"],
Name = "Ban",
Name = Localization["WEBFRONT_ACTION_BAN_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -152,7 +152,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_UNBAN_NAME"],
Name = "Unban",
Name = Localization["WEBFRONT_ACTION_UNBAN_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -193,7 +193,7 @@ namespace WebfrontCore.Controllers
var login = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_LOGIN_NAME"],
Name = "Login",
Name = Localization["WEBFRONT_ACTION_LOGIN_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -226,7 +226,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_EDIT"],
Name = "Edit",
Name = Localization["WEBFRONT_ACTION_LABEL_EDIT"],
Inputs = new List<InputInfo>
{
new()
@ -291,7 +291,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_SUBMIT_MESSAGE"],
Name = "Chat",
Name = Localization["WEBFRONT_ACTION_LABEL_SUBMIT_MESSAGE"],
Inputs = new List<InputInfo>
{
new()
@ -367,7 +367,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_FLAG_NAME"],
Name = "Flag",
Name = Localization["WEBFRONT_ACTION_FLAG_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -406,7 +406,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_UNFLAG_NAME"],
Name = "Unflag",
Name = Localization["WEBFRONT_ACTION_UNFLAG_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -438,7 +438,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_KICK_NAME"],
Name = "Kick",
Name = Localization["WEBFRONT_ACTION_KICK_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -487,8 +487,8 @@ namespace WebfrontCore.Controllers
{
var info = new ActionInfo
{
ActionButtonLabel = "Dismiss",
Name = "Dismiss Alert?",
ActionButtonLabel = Localization["WEBFRONT_ACTION_DISMISS_ALERT_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_DISMISS_ALERT_SINGLE"],
Inputs = new List<InputInfo>
{
new()
@ -512,7 +512,7 @@ namespace WebfrontCore.Controllers
{
new CommandResponseInfo
{
Response = "Alert dismissed"
Response = Localization["WEBFRONT_ACTION_DISMISS_ALERT_SINGLE_RESPONSE"]
}
});
}
@ -521,8 +521,8 @@ namespace WebfrontCore.Controllers
{
var info = new ActionInfo
{
ActionButtonLabel = "Dismiss",
Name = "Dismiss All Alerts?",
ActionButtonLabel = Localization["WEBFRONT_ACTION_DISMISS_ALERT_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_DISMISS_ALERT_MANY"],
Inputs = new List<InputInfo>
{
new()
@ -546,7 +546,7 @@ namespace WebfrontCore.Controllers
{
new CommandResponseInfo
{
Response = "Alerts dismissed"
Response = Localization["WEBFRONT_ACTION_DISMISS_ALERT_MANY_RESPONSE"]
}
});
}
@ -555,14 +555,14 @@ namespace WebfrontCore.Controllers
{
var info = new ActionInfo
{
ActionButtonLabel = "Send",
Name = "Compose Message",
ActionButtonLabel = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_BUTTON_COMPOSE"],
Inputs = new List<InputInfo>
{
new()
{
Name = "message",
Label = "Message Content",
Label = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_FORM_CONTENT"],
},
},
Action = "OfflineMessage",

View File

@ -56,7 +56,7 @@ namespace WebfrontCore.Controllers
ViewBag.ClientIP = request.ClientIP;
ViewBag.ClientGuid = request.ClientGuid;
ViewBag.Title = "Ban Management";
ViewBag.Title = Localization["WEBFRONT_NAV_TITLE_BAN_MANAGEMENT"];
return View(results.Results);
}

View File

@ -146,12 +146,8 @@ namespace WebfrontCore.Controllers
clientDto.Meta.AddRange(Authorized ? meta : meta.Where(m => !m.IsSensitive));
var strippedName = clientDto.Name.StripColors();
ViewBag.Title = strippedName.Substring(strippedName.Length - 1).ToLower()[0] == 's'
? strippedName + "'"
: strippedName + "'s";
ViewBag.Title += " " + Localization["WEBFRONT_CLIENT_PROFILE_TITLE"];
ViewBag.Description = $"Client information for {strippedName}";
ViewBag.Keywords = $"IW4MAdmin, client, profile, {strippedName}";
ViewBag.Title = $"{strippedName} | {Localization["WEBFRONT_CLIENT_PROFILE_TITLE"]}";
ViewBag.Description = Localization["WEBFRONT_PROFILE_DESCRIPTION"].FormatExt(strippedName);
ViewBag.UseNewStats = _config?.EnableAdvancedMetrics ?? true;
return View("Profile/Index", clientDto);
@ -214,7 +210,7 @@ namespace WebfrontCore.Controllers
ViewBag.SearchTerm = clientName;
ViewBag.ResultCount = clientsDto.Count;
ViewBag.Title = "Search Results";
ViewBag.Title = Localization["WEBFRONT_SEARCH_RESULTS_TITLE"];
return View("Find/Index", clientsDto);
}

View File

@ -221,7 +221,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
{
return View("~/Views/Client/_MessageContext.cshtml", new List<MessageResponse>
{
new MessageResponse()
new()
{
ClientId = penalty.OffenderId,
Message = penalty.AutomatedOffense,

View File

@ -100,7 +100,7 @@ namespace WebfrontCore.Controllers
new CommandResponseInfo
{
ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMADS_RESTART_SUCCESS"]
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_SUCCESS"]
}
};
}

View File

@ -74,21 +74,28 @@ namespace WebfrontCore.Controllers
ViewBag.CommandPrefix = Manager.GetApplicationSettings().Configuration().CommandPrefix;
// we don't need to the name of the shared library assembly
var excludedAssembly = typeof(BaseController).Assembly;
var commands = Manager.GetCommands()
.Where(_cmd => _cmd.Permission <= Client.Level)
.OrderByDescending(_cmd => _cmd.Permission)
.GroupBy(_cmd =>
.Where(command => command.Permission <= Client.Level)
.OrderByDescending(command => command.Permission)
.GroupBy(command =>
{
// we need the plugin type the command is defined in
var pluginType = _cmd.GetType().Assembly.GetTypes().FirstOrDefault(_type =>
_type.Assembly != excludedAssembly && typeof(IPlugin).IsAssignableFrom(_type));
return pluginType == null ? _translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"] :
pluginType.Name == "ScriptPlugin" ? _translationLookup["WEBFRONT_HELP_SCRIPT_PLUGIN"] :
Manager.Plugins.FirstOrDefault(_plugin => _plugin.GetType().FullName == pluginType.FullName)?
.Name; // for now we're just returning the name of the plugin, maybe later we'll include more info
if (command.GetType().Name == "ScriptCommand")
{
return _translationLookup["WEBFRONT_HELP_SCRIPT_PLUGIN"];
}
var assemblyName = command.GetType().Assembly.GetName().Name;
if (assemblyName is "IW4MAdmin" or "SharedLibraryCore")
{
return _translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"];
}
var pluginType = command.GetType().Assembly.GetTypes()
.FirstOrDefault(type => typeof(IPlugin).IsAssignableFrom(type));
return Manager.Plugins.FirstOrDefault(plugin => plugin.GetType() == pluginType)?.Name ??
_translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"];
})
.Select(_grp => (_grp.Key, _grp.AsEnumerable()));
.Select(group => (group.Key, group.AsEnumerable()));
return View(commands);
}

View File

@ -86,7 +86,7 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
OffenderName = penalty.Penalty.Offender.CurrentAlias.Name,
Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense)
? penalty.Penalty.Offense
: "Anticheat Detection",
: Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_BAN_INFO_AC_DETECTION"],
LinkId = penalty.Penalty.Offender.AliasLinkId,
penalty.Penalty.OffenderId,
penalty.Penalty.PunisherId,
@ -113,7 +113,7 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
OffenderName = penalty.Offender.CurrentAlias.Name,
Offense = string.IsNullOrEmpty(penalty.AutomatedOffense)
? penalty.Offense
: "Anticheat Detection",
: Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_BAN_INFO_AC_DETECTION"],
LinkId = penalty.Offender.AliasLinkId,
penalty.OffenderId,
penalty.PunisherId,

View File

@ -35,19 +35,19 @@
@foreach (var social in Model.CommunityInformation.SocialAccounts ?? Array.Empty<SocialAccountConfiguration>())
{
<div>
<a href="@social.Url" target="_blank" title="@social.Title">
<a href="@social.Url" target="_blank" title="@social.Title" class="d-flex no-decoration">
@if (!string.IsNullOrWhiteSpace(social.IconId))
{
<span class="oi @social.IconId"></span>
<i class="oi @social.IconId mr-5" style="width: 1.6rem;"></i>
}
else if (!string.IsNullOrWhiteSpace(social.IconUrl))
{
var url = Uri.TryCreate(social.IconUrl, UriKind.Absolute, out var parsedUrl)
? parsedUrl.AbsoluteUri
: $"images/community/{social.IconUrl}";
<img class="img-fluid" style="max-width: 1rem; fill: white" src="@url" alt="@social.Title"/>
<img class="img-fluid mr-5" style="width: 1.6rem; fill: white" src="@url" alt="@social.Title"/>
}
<span class="ml-1">@social.Title</span>
<div class="ml-1">@social.Title</div>
</a>
</div>
}

View File

@ -61,6 +61,6 @@
}
<div class="ml-auto">
<button type="submit" class="btn btn-primary">@Model.ActionButtonLabel</button>
<a href="#" class="btn mr-5 ml-5" role="button" onclick="halfmoon.toggleModal('actionModal');">Close</a>
<a href="#" class="btn mr-5 ml-5" role="button" onclick="halfmoon.toggleModal('actionModal');">@ViewBag.Localization["WEBFRONT_ACTION_MODAL_BUTTON_CLOSE"]</a>
</div>
</form>

View File

@ -4,12 +4,12 @@
<h2 class="content-title mt-20 mb-10">@ViewBag.Title</h2>
@if (!Model.Any())
{
<div class="text-muted mb-10">Search for records...</div>
<div class="text-muted mb-10">@ViewBag.Localization["WEBFRONT_BAN_MGMT_SUBTITLE"]</div>
}
<form method="get" class="mt-10">
<div class="d-flex flex-column flex-md-row">
<div class="input-group">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientNameInput" name="clientName" value="@ViewBag.ClientName" placeholder="Client Name">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientNameInput" name="clientName" value="@ViewBag.ClientName" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_NAME"]">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
@ -18,7 +18,7 @@
</div>
<div class="input-group mr-md-5 ml-md-10 mt-10 mb-5 mt-md-0 mb-md-0">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientGuidInput" name="clientGuid" value="@ViewBag.ClientGuid" placeholder="Client GUID">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientGuidInput" name="clientGuid" value="@ViewBag.ClientGuid" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_GUID"]">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
@ -26,7 +26,7 @@
</div>
</div>
<div class="input-group mr-md-10 ml-md-5 mb-10 mt-5 mt-md-0 mb-md-0">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIPInput" name="clientIP" value="@ViewBag.ClientIP" placeholder="Client IP">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIPInput" name="clientIP" value="@ViewBag.ClientIP" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_IP"]">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
@ -34,7 +34,7 @@
</div>
</div>
<div class="input-group">
<input type="number" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIdInput" name="clientId" value="@ViewBag.ClientId" placeholder="Client Id">
<input type="number" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIdInput" name="clientId" value="@ViewBag.ClientId" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_ID"]">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>

View File

@ -10,12 +10,16 @@
<div class="card p-10 m-0 mt-15 mb-15">
<div class="d-flex flex-row flex-wrap">
<div class="d-flex p-15 mr-md-10 w-full w-md-200 bg-very-dark-dm bg-light-ex-lm rounded">
<div class="align-self-center ">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@ban.ClientId" class="font-size-18 no-decoration">@ban.ClientName</a>
<br/>
<div data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{ban.Game}"]">
<div class="badge">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{ban.Game}"])</div>
<div class="align-self-center w-full">
<div class="d-flex font-size-16">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@ban.ClientId" class="no-decoration flex-fill text-force-break mr-5">
<color-code value="@ban.ClientName"></color-code>
</a>
<div data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{ban.Game}"]">
<div class="badge align-self-center">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{ban.Game}"])</div>
</div>
</div>
<has-permission entity="ClientGuid" required-permission="Read">
<div class="text-muted">@ban.NetworkId.ToString("X")</div>
</has-permission>
@ -25,15 +29,18 @@
<br/>
@if (ban.AttachedPenalty is not null)
{
<div class="text-muted font-weight-light">@ban.AttachedPenalty.Offense.CapClientName(30)</div>
<div class="text-muted font-weight-light">
<color-code value="@ban.AttachedPenalty.Offense.CapClientName(30)"></color-code>
</div>
<div class="text-danger font-weight-light">@ban.AttachedPenalty.DateTime.ToStandardFormat()</div>
<div class="btn profile-action w-100" data-action="unban" data-action-id="@ban.ClientId">Unban</div>
<br/>
<div class="btn profile-action" ata-action="unban" data-action-id="@ban.ClientId">@ViewBag.Localization["WEBFRONT_BAN_MGMT_ACTION_UNBAN"]</div>
}
else
{
<div class="align-self-end text-muted font-weight-light">
<span class="oi oi-warning font-size-12"></span>
<span>Link-Only Ban</span>
<span>@ViewBag.Localization["WEBFRONT_BAN_MGMT_LINK_ONLY"]</span>
</div>
}
</div>
@ -42,15 +49,20 @@
@foreach (var associatedEntity in ban.AssociatedPenalties)
{
<div class="d-flex flex-wrap flex-column w-full w-md-200 p-10">
<div data-toggle="tooltip" data-title="Linked via shared IP" class="d-flex">
<i class="oi oi-link-intact align-self-center"></i>
<div class="text-truncate ml-5 mr-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@associatedEntity.OffenderInfo.ClientId" class="font-size-18 no-decoration">@associatedEntity.OffenderInfo.ClientName</a>
<div class="d-flex flex-wrap flex-column w-full w-md-200 p-10 border rounded mt-10 mt-md-0" style="border-style: dashed !important;">
<div class="d-flex font-size-16">
<div data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_BAN_MGMT_TOOLTIP_LINKED"]" class="d-flex flex-fill">
<i class="oi oi-link-intact align-self-center"></i>
<div class="text-truncate ml-5 mr-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@associatedEntity.OffenderInfo.ClientId" class="no-decoration text-force-break">
<color-code value="@associatedEntity.OffenderInfo.ClientName"></color-code>
</a>
</div>
</div>
<div data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{ban.Game}"]">
<div class="badge">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{ban.Game}"])</div>
</div>
</div>
<div data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{ban.Game}"]">
<div class="badge">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{ban.Game}"])</div>
</div>
<br/>
<has-permission entity="ClientGuid" required-permission="Read">
@ -60,9 +72,11 @@
<div class="text-muted">@associatedEntity.OffenderInfo.IPAddress.ConvertIPtoString()</div>
</has-permission>
<br/>
<div class="text-muted font-weight-light">@associatedEntity.Offense.CapClientName(30)</div>
<div class="text-muted font-weight-light">
<color-code value="@associatedEntity.Offense.CapClientName(30)"></color-code>
</div>
<div class="text-danger font-weight-light">@associatedEntity.DateTime.ToStandardFormat()</div>
<div class="btn profile-action mt-10 w-100" data-action="unban" data-action-id="@associatedEntity.OffenderInfo.ClientId">Unban</div>
<div class="btn profile-action mt-10" data-action="unban" data-action-id="@associatedEntity.OffenderInfo.ClientId">@ViewBag.Localization["WEBFRONT_BAN_MGMT_ACTION_UNBAN"]</div>
</div>
}
</div>

View File

@ -30,11 +30,11 @@
</td>
<td>
@info.Data
<td >
<td>
@info.NewValue
</td>
<td class="text-right">
@info.When.ToString()
@info.When.ToStandardFormat()
</td>
</tr>
@ -63,9 +63,9 @@
{
<div class="mt-5 mb-5">&ndash;</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">@info.When.ToString()</div>
<div class="mt-5 mb-5">@info.When</div>
</td>
</tr>
}

View File

@ -5,8 +5,30 @@
<!-- desktop -->
<div class="content mt-0">
<h2 class="content-title mt-20 mb-0">Search Results</h2>
<div class="text-muted mb-15"><span class="badge">@ViewBag.SearchTerm</span> returned <span class="text-primary">@ViewBag.ResultCount</span> matche(s)</div>
<h2 class="content-title mt-20 mb-0">@loc["WEBFRONT_SEARCH_RESULTS_TITLE"]</h2>
<div class="text-muted mb-15">
@foreach (var match in Utilities.SplitTranslationTokens("WEBFRONT_SEARCH_RESULTS_SUBTITLE_FORMAT"))
{
if (match.IsInterpolation)
{
if (match.MatchValue == "searchTerm")
{
<span class="badge">
@ViewBag.SearchTerm
</span>
}
else if (match.MatchValue == "searchCount")
{
<span class="text-primary">@ViewBag.ResultCount</span>
}
}
else
{
<span>@match.MatchValue</span>
}
}
</div>
<table class="table d-none d-md-table">
<thead>

View File

@ -9,7 +9,7 @@
else
{
<h2 class="content-title mt-20 mb-0">Search Results</h2>
<h2 class="content-title mt-20 mb-0">@ViewBag.Localization["WEBFRONT_SEARCH_RESULTS_TITLE"]</h2>
<div class="text-muted mb-15">@Html.Raw(Utilities.FormatExt(ViewBag.Localization["WEBFRONT_STATS_MESSAGES_FOUND"], $"<span class=\"badge\">{Model.TotalResultCount.ToString("N0")}</span>"))</div>
<table class="table bg-dark-dm bg-light-lm rounded" style="table-layout: fixed">
@ -27,7 +27,7 @@ else
</table>
<div id="loaderLoad" class="mt-10 m-auto text-center d-none d-lg-block">
<i class="loader-load-more oi oi-chevron-bottom "></i>
<i class="loader-load-more oi oi-chevron-bottom"></i>
</div>
@section scripts {

View File

@ -24,7 +24,7 @@
<color-code value="@(message.ServerName ?? "--")"></color-code>
</td>
<td colspan="15%" class="text-right text-break">
@message.When
@message.When.ToStandardFormat()
</td>
</tr>
@ -53,8 +53,7 @@
<div>
<color-code value="@(message.ServerName ?? "--")"></color-code>
</div>
<div> @message.When</div>
<div>@message.When.ToStandardFormat()</div>
</td>
</tr>
}

View File

@ -2,15 +2,14 @@
<div class="content mt-0">
<h4 class="content-title mt-20">@ViewBag.Title</h4>
@foreach (var key in Model.Keys)
{
<table class="table mb-20" style="table-layout:fixed;">
<thead>
<tr class="level-bgcolor-@((int)key)">
<th class="text-light">@key.ToLocalizedLevelName()</th>
<th>Game</th>
<th class="text-right font-weight-bold">Last Connected</th>
<th colspan="50%" class="text-light">@key.ToLocalizedLevelName()</th>
<th colspan="20%">@ViewBag.Localization["WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"]</th>
<th colspan="30%" class="text-right font-weight-bold text-force-break">@ViewBag.Localization["WEBFRONT_SEARCH_LAST_CONNECTED"]</th>
</tr>
</thead>
<tbody>
@ -22,22 +21,25 @@
continue;
}
<tr class="bg-dark-dm bg-light-lm">
<td>
<td colspan="50%">
@if (client.IsMasked)
{
<span data-toggle="tooltip" data-title="Client is masked">
<span data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_PRIVILEGED_TOOLTIP_MASKED"]">
<span class="oi oi-shield mr-5 font-size-12"></span>
</span>
}
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId" class="text-force-break">
<color-code value="@client.Name"></color-code>
</a>
</td>
<td>
<td colspan="20%" class="d-none d-md-table-cell">
<div class="badge">@ViewBag.Localization[$"GAME_{client.Game}"]</div>
</td>
<td class="text-right">
<td colspan="20%" class="d-table-cell d-md-none">
<div class="badge">@(Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{client.Game}"] as string))</div>
</td>
<td colspan="30%" class="text-right">
@client.LastConnection.HumanizeForCurrentCulture()
</td>
</tr>
@ -46,5 +48,4 @@
</tbody>
</table>
}
</div>

View File

@ -58,7 +58,7 @@
</has-permission>
}
<h2 class="content-title mb-0">Player Profile</h2>
<h2 class="content-title mb-0">@ViewBag.Localization["WEBFRONT_PROFILE_TITLE"]</h2>
<div class="font-size-12 text-muted">@ViewBag.Localization[$"GAME_{Model.Game}"]</div>
<div id="profile_wrapper" class="mb-10 mt-10">
@ -66,7 +66,7 @@
<!-- online status indicator -->
@if (Model.Online)
{
<div class="bg-success rounded-circle position-absolute status-indicator z-20 mt-10 ml-10" data-toggle="tooltip" data-placement="bottom" data-title="Client is online"></div>
<div class="bg-success rounded-circle position-absolute status-indicator z-20 mt-10 ml-10" data-toggle="tooltip" data-placement="bottom" data-title="@ViewBag.Localization["WEBFRONT_PROFILE_TOOLTIP_ONLINE"]"></div>
<div class="bg-success rounded-circle position-absolute status-indicator with-ripple z-10 mt-10 ml-10"></div>
}
@ -82,7 +82,7 @@
<div class="d-flex flex-column align-self-center ml-20 mr-20 mt-10 mb-10 mt-md-0 mb-md-0 text-center text-md-left">
<!-- name -->
<div id="profile_name">
<span class="font-size-20 font-weight-medium">
<span class="font-size-20 font-weight-medium text-force-break">
<color-code value="@Model.Name"></color-code>
</span>
<has-permission entity="MetaAliasUpdate" required-permission="Read">
@ -105,7 +105,7 @@
@if (Model.Aliases.Count > 15)
{
<div class="dropdown-divider"></div>
<span class="dropdown-item bg-dark-dm bg-light-lm">...and @(Model.Aliases.Count - 15) more</span>
<span class="dropdown-item bg-dark-dm bg-light-lm">@((ViewBag.Localization["WEBFRONT_PROFILE_ALIAS_COUNT_MORE_FORMAT"] as string).FormatExt(Model.Aliases.Count - 15))</span>
}
</div>
</div>
@ -115,7 +115,7 @@
<has-permission entity="ClientLevel" required-permission="Read">
<div class="align-self-center align-self-md-start font-weight-bold font-size-16 level-color-@Model.LevelInt">
<div class="d-flex flex-row">
<span>@Model.Level</span>
<color-code value="@Model.Level"></color-code>
</div>
</div>
</has-permission>
@ -126,7 +126,7 @@
<div class="text-muted" data-toggle="dropdown" id="altGuidFormatsDropdown" aria-haspopup="true" aria-expanded="false">@Model.NetworkId.ToString("X")</div>
<div class="dropdown-menu" aria-labelledby="altGuidFormatsDropdown">
<div class="p-10 font-size-12">
<div class="">Alternative Formats</div>
<div class="">@ViewBag.Localization["WEBFRONT_PROFILE_POPOVER_ALTERNATIVE_GUID"]</div>
<div class="dropdown-divider mt-5 mb-5"></div>
<div class="text-muted font-weight-lighter">@((ulong)Model.NetworkId)</div>
</div>
@ -164,7 +164,7 @@
@if (Model.IPs.Count > 15)
{
<div class="dropdown-divider"></div>
<span class="dropdown-item bg-dark-dm bg-light-lm">...and @(Model.IPs.Count - 15) more</span>
<span class="dropdown-item bg-dark-dm bg-light-lm">@((ViewBag.Localization["WEBFRONT_PROFILE_ALIAS_COUNT_MORE_FORMAT"] as string).FormatExt(Model.IPs.Count - 15))</span>
}
</div>
</div>
@ -252,18 +252,18 @@
@{
var menuItems = new SideContextMenuItems
{
MenuTitle = "Actions",
MenuTitle = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TITLE"]
};
if (Model.Online)
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Join Game",
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_JOIN"],
IsLink = true,
IsButton = true,
Reference = Model.ConnectProtocolUrl,
Tooltip = $"Playing on {Model.CurrentServerName.StripColors()}",
Tooltip = (ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TOOLTIP_JOIN"] as string).FormatExt(Model.CurrentServerName.StripColors()),
Icon = "oi-play-circle"
});
}
@ -272,7 +272,7 @@
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Change Level",
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_LEVEL"],
IsButton = true,
Reference = "edit",
Icon = "oi-cog",
@ -284,7 +284,7 @@
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Message",
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MESSAGE"],
IsButton = true,
Reference = "OfflineMessage",
Icon = "oi oi-envelope-closed",
@ -294,7 +294,7 @@
menuItems.Items.Add(new SideContextMenuItem
{
Title = "View Stats",
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_STATS"],
IsButton = true,
IsLink = true,
Reference = Url.Action("Advanced", "ClientStatistics", new { id = Model.ClientId }),
@ -305,7 +305,7 @@
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = isFlagged ? "Unflag" : "Flag",
Title = isFlagged ? ViewBag.Localization["WEBFRONT_ACTION_UNFLAG_NAME"] : ViewBag.Localization["WEBFRONT_ACTION_FLAG_NAME"],
IsButton = true,
Reference = isFlagged ? "unflag" : "flag",
Icon = "oi-flag",
@ -317,7 +317,7 @@
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Kick",
Title = ViewBag.Localization["WEBFRONT_ACTION_KICK_NAME"],
IsButton = true,
Reference = "kick",
Icon = "oi-circle-x",
@ -329,7 +329,7 @@
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Ban",
Title = ViewBag.Localization["WEBFRONT_ACTION_BAN_NAME"],
IsButton = true,
Reference = "ban",
Icon = "oi-lock-unlocked",
@ -341,7 +341,7 @@
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Unban",
Title = ViewBag.Localization["WEBFRONT_ACTION_UNBAN_NAME"],
IsButton = true,
Reference = "unban",
Icon = "oi-lock-locked",

View File

@ -41,7 +41,6 @@
{
<color-code value="@Model.Offense"></color-code>
}
</span>
}

View File

@ -18,7 +18,7 @@
@{var results = Utilities.SplitTranslationTokens(meta.meta.Key);}
@if (results.Any(_result => _result.IsInterpolation))
@if (results.Any(result => result.IsInterpolation))
{
foreach (var result in results)
{
@ -41,6 +41,5 @@
}
</div>
}
<!-- </div> -->
}
</div>

View File

@ -7,7 +7,7 @@
}
<span class="client-message" data-serverid="@Model.ServerId" data-when="@Model.When.ToFileTimeUtc()">
<span data-title="View Context" data-toggle="tooltip" data-placement="right">
<span data-title="@ViewBag.Localization["WEBFRONT_META_TOOLTIP_CONTEXT"]" data-toggle="tooltip" data-placement="right">
<span class="oi oi-chevron-right align-middle client-message-prefix" style="font-size: 0.75rem; margin-top: -0.256rem"></span>
</span>
<span class="text-muted @(Model.IsQuickMessage ? "font-weight-bold" : "")">

View File

@ -7,10 +7,12 @@
@using Humanizer.Localisation
@using IW4MAdmin.Plugins.Stats
@using WebfrontCore.ViewModels
@using System.Text.Json
@using IW4MAdmin.Plugins.Stats.Web.Dtos
@model Stats.Dtos.AdvancedStatsInfo
@{
ViewBag.Title = "Advanced Client Statistics";
ViewBag.Title = ViewBag.Localization["WEBFRONT_ADV_STATS_TITLE"];
ViewBag.Description = Model.ClientName.StripColors();
const string headshotKey = "MOD_HEAD_SHOT";
@ -122,11 +124,11 @@
var spm = Model.ServerId != null ? serverLegacyStat?.SPM.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.SPM), 0).ToNumericalString();
var performanceHistory = Model.Ratings
.Select(rating => rating.PerformanceMetric);
.Select(rating => new PerformanceHistory { Performance = rating.PerformanceMetric, OccurredAt = rating.CreatedDateTime });
if (performance != null)
{
performanceHistory = performanceHistory.Append(performance.Value);
performanceHistory = performanceHistory.Append(new PerformanceHistory { Performance = performance.Value, OccurredAt = DateTime.UtcNow });
}
var score = allPerServer.Any()
@ -233,7 +235,7 @@
<div class="content row mt-20">
<!-- main content -->
<div class="col-12 col-lg-9 mt-0">
<h2 class="content-title mb-0">Player Stats</h2>
<h2 class="content-title mb-0">@ViewBag.Title</h2>
<span class="text-muted">
<color-code value="@(Model.Servers.FirstOrDefault(server => server.Endpoint == Model.ServerEndpoint)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
</span>
@ -263,6 +265,7 @@
{
<div class="h5 mb-0">@ViewBag.Localization["WEBFRONT_ADV_STATS_EXPIRED"]</div>
}
if (Model.ServerId != null)
{
<div class="h5 mb-0">@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_PERFORMANCE"] as string).FormatExt($"<span class=\"text-primary\">{performance.ToNumericalString()}</span>"))</div>
@ -283,7 +286,7 @@
@if (performanceHistory.Count() > 5)
{
<div class="w-half m-auto ml-lg-auto " id="client_performance_history_container">
<canvas id="client_performance_history" data-history="@Html.Raw(Json.Serialize(performanceHistory))"></canvas>
<canvas id="client_performance_history" data-history="@(JsonSerializer.Serialize(performanceHistory))"></canvas>
</div>
}
</div>
@ -375,7 +378,8 @@
@{
var menuItems = new SideContextMenuItems
{
MenuTitle = "Game", Items = Model.Servers.Select(server => new SideContextMenuItem
MenuTitle = ViewBag.Localization["WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"],
Items = Model.Servers.Select(server => new SideContextMenuItem
{
IsLink = true,
Reference = Url.Action("Advanced", "ClientStatistics", new { serverId = server.Endpoint }),

View File

@ -1,4 +1,6 @@
@using IW4MAdmin.Plugins.Stats
@using System.Text.Json.Serialization
@using System.Text.Json
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
@{
Layout = null;
@ -12,7 +14,7 @@
<i class="oi oi-timer align-self-center mb-10" style="font-size: 6rem;"></i>
<div class="p-15">
<h2 class="content-title mb-0">@Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_NOQUALIFY"]</h2>
<span class="text-muted">Check back after some more time has passed</span>
<span class="text-muted">@ViewBag.Localization["WEBFRONT_TOP_PLAYERS_NOQUALIFY_SUBTITLE"]</span>
</div>
</div>
</div>
@ -83,7 +85,8 @@
</div>
</div>
</div>
<div class="w-full w-md-half client-rating-graph" id="rating_history_@(stat.ClientId + "_" + stat.Id)" data-history="@Html.Raw(Json.Serialize(stat.PerformanceHistory))">
<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>
</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"/>

View File

@ -3,12 +3,12 @@
<div class="content mt-20 row">
<div class="col-12 col-lg-9 mt-0">
<h2 class="content-title mb-0">Top Players</h2>
<h2 class="content-title mb-0">@ViewBag.Localization["WEBFRONT_TOP_PLAYERS_TITLE"]</h2>
<span class="text-muted">
<color-code value="@(Model.FirstOrDefault(m => m.Endpoint == ViewBag.SelectedServerId)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
&mdash; <span class="text-primary">@ViewBag.TotalRankedClients.ToString("#,##0")</span> Ranked Players
&mdash; <span class="text-primary">@ViewBag.TotalRankedClients.ToString("#,##0")</span> @ViewBag.Localization["WEBFRONT_TOP_PLAYERS_SUBTITLE"]
</span>
<div id="topPlayersContainer">
@await Component.InvokeAsync("TopPlayers", new { count = 25, offset = 0, serverEndpoint = ViewBag.SelectedServerId })
</div>
@ -21,7 +21,8 @@
@{
var menuItems = new SideContextMenuItems
{
MenuTitle = "Game", Items = Model.Select(server => new SideContextMenuItem
MenuTitle = ViewBag.Localization["WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"],
Items = Model.Select(server => new SideContextMenuItem
{
IsLink = true,
Reference = Url.Action("TopPlayers", "Stats", new { serverId = server.Endpoint }),

View File

@ -25,7 +25,7 @@
</div>
<div class="edit-file d-none flex-column" id="edit_file_@FormatHtmlId(file.FileName)" data-file-name="@file.FileName">
<pre class="mt-0 font-size-12 flex-fill border-bottom" spellcheck="false"><code class="code language-json editable" contenteditable="true" id="edit_file_code_@FormatHtmlId(file.FileName)">@file.FileContent</code></pre>
<button type="button" class="btn btn-primary m-15 mt-0 align-self-start file-save-button" data-file-name="@file.FileName">Save</button>
<button type="button" class="btn btn-primary m-15 mt-0 align-self-start file-save-button" data-file-name="@file.FileName">@ViewBag.Localization["WEBFRONT_CONFIGURATION_BUTTON_SAVE"]</button>
</div>
</div>
}

View File

@ -39,12 +39,12 @@
<div class="card m-0 rounded">
<div class="input-group mb-10">
<div class="input-group-prepend">
<span class="input-group-text">Server</span>
<span class="input-group-text">@ViewBag.Localization["WEBFRONT_CONSOLE_FORM_SERVER"]</span>
</div>
@Html.DropDownList("Server", Model.Select(s => new SelectListItem { Text = s.Name.StripColors(), Value = s.ID.ToString() }).ToList(), new { @class = "form-control", id = "console_server_select" })
</div>
<div class="input-group">
<input id="console_command_value" class="form-control" placeholder="Enter command..." type="text" required="required"/>
<input id="console_command_value" class="form-control" placeholder="@ViewBag.Localization["WEBFRONT_CONSOLE_FORM_PLACEHOLDER_COMMAND"]" type="text" required="required"/>
<div class="input-group-append">
<button id="console_command_button" class="btn btn-primary">
@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CONSOLE_EXECUTE"]

View File

@ -5,7 +5,7 @@
<div class="content mt-20">
@foreach (var (pluginName, commandList) in Model)
{
<h2 class="content-title mb-lg-20 mt-20 ">@(pluginName == "Native" ? "Command List" : pluginName)</h2>
<h2 class="content-title mb-lg-20 mt-20 ">@(pluginName == "Native" ? ViewBag.Localization["WEBFRONT_HELP_COMMANDS_NATIVE_TITLE"] : pluginName)</h2>
<table class="table rounded">
<thead>

View File

@ -12,7 +12,7 @@
}
<div class="content mt-20 row">
<div class="col-12 col-lg-9">
<h2 class="content-title mb-0">Server Overview</h2>
<h2 class="content-title mb-0">@ViewBag.Localization["WEBFRONT_SERVERS_TITLE"]</h2>
@if (Model.Game.HasValue)
{
<span class="text-muted">@loc[$"GAME_{Model.Game.Value}"]</span>
@ -31,11 +31,12 @@
</div>
@await Component.InvokeAsync("ServerList", Model.Game)
</div>
@{
var menuItems = new SideContextMenuItems
{
MenuTitle = "Game", Items = Model.ActiveServerGames.Select(game => new SideContextMenuItem
MenuTitle = ViewBag.Localization["WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"],
Items = Model.ActiveServerGames.Select(game => new SideContextMenuItem
{
IsLink = true,
Reference = Url.Action("Index", "Home", new { game }),

View File

@ -43,22 +43,22 @@
{
if (Model == EFPenalty.PenaltyType.Any)
{
<option value="@Convert.ToInt32(penaltyType)" selected="selected">@penaltyType.ToString()</option>
<option value="@Convert.ToInt32(penaltyType)" selected="selected">@loc[$"WEBFRONT_PENALTY_{penaltyType.ToString().ToUpper()}"]</option>
}
else
{
<option value="@Convert.ToInt32(penaltyType)">@penaltyType.ToString()</option>
<option value="@Convert.ToInt32(penaltyType)">@loc[$"WEBFRONT_PENALTY_{penaltyType.ToString().ToUpper()}"]</option>
}
}
else
{
if (penaltyType == Model)
{
<option value="@Convert.ToInt32(penaltyType)" selected="selected">@penaltyType.ToString()</option>
<option value="@Convert.ToInt32(penaltyType)" selected="selected">@loc[$"WEBFRONT_PENALTY_{penaltyType.ToString().ToUpper()}"]</option>
}
else
{
<option value="@Convert.ToInt32(penaltyType)">@penaltyType.ToString()</option>
<option value="@Convert.ToInt32(penaltyType)">@loc[$"WEBFRONT_PENALTY_{penaltyType.ToString().ToUpper()}"]</option>
}
}
}

View File

@ -1,7 +1,7 @@
@{
@{
Layout = null;
}
@model WebfrontCore.ViewModels.PenaltyFilterInfo
@await Component.InvokeAsync("PenaltyList", new { offset = Model.Offset, count= Model.Count, showOnly = Model.ShowOnly, ignoreAutomated = Model.IgnoreAutomated })
@await Component.InvokeAsync("PenaltyList", new { offset = Model.Offset, count = Model.Count, showOnly = Model.ShowOnly, ignoreAutomated = Model.IgnoreAutomated })

View File

@ -17,7 +17,7 @@
</a>
</td>
<td colspan="10%" class="penalties-color-@Model.PenaltyTypeText.ToLower()">
@Model.PenaltyType
@ViewBag.Localization[$"WEBFRONT_PENALTY_{Model.PenaltyType.ToString().ToUpper()}"]
</td>
<td colspan="35%">
<color-code value="@($"{Model.Offense}{(ViewBag.Authorized ? Model.AdditionalPenaltyInformation : "")}")"></color-code>
@ -55,7 +55,7 @@
</a>
</div>
<div class="mt-5 mb-5 penalties-color-@Model.PenaltyTypeText.ToLower()">
@Model.PenaltyType
@ViewBag.Localization[$"WEBFRONT_PENALTY_{Model.PenaltyType.ToString().ToUpper()}"]
</div>
<div class="mt-5 mb-5">
<color-code value="@($"{Model.Offense}{(ViewBag.Authorized ? Model.AdditionalPenaltyInformation : "")}")"></color-code>

View File

@ -14,7 +14,7 @@
<div class="content mt-20 row">
<div class="col-12 col-lg-9">
<h2 class="content-title mb-0">Live Radar</h2>
<h2 class="content-title mb-0">@ViewBag.Localization["WEBFRONT_LIVE_RADAR_TITLE"]</h2>
<div class="text-muted mb-15">
<color-code value="@((Model.FirstOrDefault(server => server.Endpoint == ViewBag.SelectedServerId) ?? Model.First()).Name)"></color-code>
</div>
@ -36,7 +36,8 @@
@{
var menuItems = new SideContextMenuItems
{
MenuTitle = "Game", Items = Model.Select(server => new SideContextMenuItem
MenuTitle = ViewBag.Localization["WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"],
Items = Model.Select(server => new SideContextMenuItem
{
IsLink = true,
// ReSharper disable Mvc.ActionNotResolved

View File

@ -8,7 +8,7 @@
<div class="col-12 col-lg-9">
@if (Model is not null)
{
<div class=" scoreboard-container" data-server-id="@ViewBag.SelectedServerId">
<div class="scoreboard-container" data-server-id="@ViewBag.SelectedServerId">
<partial name="_Scoreboard" for="@selectedServer"/>
</div>
}
@ -16,7 +16,8 @@
@{
var menuItems = new SideContextMenuItems
{
MenuTitle = "Server", Items = Model.Select(server => new SideContextMenuItem
MenuTitle = ViewBag.Localization["WEBFRONT_CONTEXT_MENU_GLOBAL_SERVER"],
Items = Model.Select(server => new SideContextMenuItem
{
IsLink = true,
Reference = Url.Action("Scoreboard", "Server", new { serverId = server.ServerId }),

View File

@ -30,67 +30,74 @@
}
<h4 class="content-title mb-0">
Scoreboard
@ViewBag.Localization["WEBFRONT_TITLE_SCOREBOARD"]
</h4>
<span class="text-muted">
<color-code value="@Model.ServerName"></color-code>
</span>
<table class="table table-sort mt-15"
data-sort-column="@(Model.OrderByKey ?? nameof(ClientScoreboardInfo.Score))"
data-sort-down="@Model.ShouldOrderDescending.ToString().ToLower()">
<tr class="bg-dark-dm bg-white-lm d-none d-lg-table-row">
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.ClientName)">@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_PLAYER"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.ClientName)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.Score)">@ViewBag.Localization["WEBFRONT_ADV_STATS_SCORE"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Score)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.Kills)">@ViewBag.Localization["WEBFRONT_ADV_STATS_KILLS"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Kills)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.Deaths)">@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_DEATHS"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Deaths)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.Kdr)">@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_RATIO"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Kdr)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.ScorePerMinute)">@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_SPM"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.ScorePerMinute)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.ZScore)">@ViewBag.Localization["WEBFRONT_ADV_STATS_ZSCORE"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.ZScore)))</th>
<th class="text-right table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.Ping)">@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_PING"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Ping)))</th>
</tr>
@foreach (var client in Model.ShouldOrderDescending ? Model.ClientInfo.OrderByDescending(OrderByFunc) : Model.ClientInfo.OrderBy(OrderByFunc))
{
<!-- desktop -->
<tr class="@GetTeamBackgroundColorClass(client) d-none d-lg-table-row">
<td>
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId" class="no-decoration text-light-dm text-dark-lm">
<color-code value="@client.ClientName"></color-code>
</a>
</td>
<td>@client.Score</td>
<td>@(client.Kills ?? 0)</td>
<td>@(client.Deaths ?? 0)</td>
<td>@Math.Round(client.Kdr ?? 0, 2)</td>
<td>@Math.Round(client.ScorePerMinute ?? 0)</td>
<td>@(client.ZScore is null or 0 ? "--" : Math.Round(client.ZScore.Value, 2).ToString(CultureInfo.CurrentCulture))</td>
<td class="text-right">@client.Ping</td>
@if (!Model.ClientInfo.Any())
{
<div class="mt-20">@ViewBag.Localization["WEBFRONT_SCOREBOARD_NO_PLAYERS"]</div>
}
else
{
<table class="table table-sort mt-15"
data-sort-column="@(Model.OrderByKey ?? nameof(ClientScoreboardInfo.Score))"
data-sort-down="@Model.ShouldOrderDescending.ToString().ToLower()">
<tr class="bg-dark-dm bg-white-lm d-none d-lg-table-row">
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.ClientName)">@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_PLAYER"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.ClientName)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.Score)">@ViewBag.Localization["WEBFRONT_ADV_STATS_SCORE"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Score)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.Kills)">@ViewBag.Localization["WEBFRONT_ADV_STATS_KILLS"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Kills)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.Deaths)">@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_DEATHS"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Deaths)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.Kdr)">@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_RATIO"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Kdr)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.ScorePerMinute)">@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_SPM"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.ScorePerMinute)))</th>
<th class="table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.ZScore)">@ViewBag.Localization["WEBFRONT_ADV_STATS_ZSCORE"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.ZScore)))</th>
<th class="text-right table-sort-column" data-column-name="@nameof(ClientScoreboardInfo.Ping)">@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_PING"]@Html.Raw(GetColumnSortDisplay(nameof(ClientScoreboardInfo.Ping)))</th>
</tr>
@foreach (var client in Model.ShouldOrderDescending ? Model.ClientInfo.OrderByDescending(OrderByFunc) : Model.ClientInfo.OrderBy(OrderByFunc))
{
<!-- desktop -->
<tr class="@GetTeamBackgroundColorClass(client) d-none d-lg-table-row">
<td>
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId" class="no-decoration text-light-dm text-dark-lm">
<color-code value="@client.ClientName"></color-code>
</a>
</td>
<td>@client.Score</td>
<td>@(client.Kills ?? 0)</td>
<td>@(client.Deaths ?? 0)</td>
<td>@Math.Round(client.Kdr ?? 0, 2)</td>
<td>@Math.Round(client.ScorePerMinute ?? 0)</td>
<td>@(client.ZScore is null or 0 ? "--" : Math.Round(client.ZScore.Value, 2).ToString(CultureInfo.CurrentCulture))</td>
<td class="text-right">@client.Ping</td>
</tr>
<tr class="d-table-row d-lg-none d-flex">
<td class="text-right bg-primary text-light flex-grow-0">
<div>@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_PLAYER"]</div>
<div>@ViewBag.Localization["WEBFRONT_ADV_STATS_SCORE"]</div>
<div>@ViewBag.Localization["WEBFRONT_ADV_STATS_KILLS"]</div>
<div>@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_DEATHS"]</div>
<div>@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_RATIO"]</div>
<div>@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_SPM"]</div>
<div>@ViewBag.Localization["WEBFRONT_ADV_STATS_ZSCORE"]</div>
<div>@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_PING"]</div>
</td>
<tr class="d-table-row d-lg-none d-flex">
<td class="text-right bg-primary text-light flex-grow-0">
<div>@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_PLAYER"]</div>
<div>@ViewBag.Localization["WEBFRONT_ADV_STATS_SCORE"]</div>
<div>@ViewBag.Localization["WEBFRONT_ADV_STATS_KILLS"]</div>
<div>@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_DEATHS"]</div>
<div>@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_RATIO"]</div>
<div>@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_SPM"]</div>
<div>@ViewBag.Localization["WEBFRONT_ADV_STATS_ZSCORE"]</div>
<div>@ViewBag.Localization["WEBFRONT_SCOREBOARD_TABLE_PING"]</div>
</td>
<td class="@GetTeamBackgroundColorClass(client) flex-fill">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId" class="no-decoration text-light-dm text-dark-lm">
<color-code value="@client.ClientName"></color-code>
</a>
<div>@client.Score</div>
<div>@(client.Kills ?? 0)</div>
<div>@(client.Deaths ?? 0)</div>
<div>@Math.Round(client.Kdr ?? 0, 2)</div>
<div>@Math.Round(client.ScorePerMinute ?? 0)</div>
<div>@(client.ZScore is null or 0 ? "--" : Math.Round(client.ZScore.Value, 2).ToString(CultureInfo.CurrentCulture))</div>
<div>@client.Ping</div>
</td>
</tr>
}
</table>
<td class="@GetTeamBackgroundColorClass(client) flex-fill">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId" class="no-decoration text-light-dm text-dark-lm">
<color-code value="@client.ClientName"></color-code>
</a>
<div>@client.Score</div>
<div>@(client.Kills ?? 0)</div>
<div>@(client.Deaths ?? 0)</div>
<div>@Math.Round(client.Kdr ?? 0, 2)</div>
<div>@Math.Round(client.ScorePerMinute ?? 0)</div>
<div>@(client.ZScore is null or 0 ? "--" : Math.Round(client.ZScore.Value, 2).ToString(CultureInfo.CurrentCulture))</div>
<div>@client.Ping</div>
</td>
</tr>
}
</table>
}

View File

@ -42,7 +42,7 @@
class="text-light align-self-center">
<i class="oi oi-spreadsheet ml-5 mr-5"></i>
</a>
<span class="ml-5 mr-5 text-light badge font-weight-light" data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{Model.Game}"]">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{Model.Game}"])</span>
<span class="ml-5 mr-5 text-light-dm text-primary-lm badge font-weight-light" data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{Model.Game}"]">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{Model.Game}"])</span>
</div>
</div>
<!-- second column -->

View File

@ -3,7 +3,20 @@
Layout = null;
}
<div class="mb-15 text-center font-weight-lighter">New clients connected in the last <span class="text-primary">24</span> hours</div>
<div class="mb-15 text-center font-weight-lighter">
@foreach (var match in Utilities.SplitTranslationTokens("WEBFRONT_MODAL_RECENT_CLIENTS_SUBTITLE"))
{
if (match.IsInterpolation && match.MatchValue == "time")
{
<span class="text-primary">24</span>
}
else
{
<span>@match.MatchValue</span>
}
}
</div>
<div id="recentClientContainer">
<partial name="~/Views/Shared/Components/Client/_RecentClients.cshtml" for="@Model"/>

View File

@ -58,7 +58,18 @@
<div class="profile-meta-entry loader-data-time" data-time="@meta.When.ToFileTimeUtc()" onclick="$('#metaContextDateToggle@(meta.When.ToFileTimeUtc())').show()">
<partial name="~/Views/Client/Profile/Meta/_@(meta.GetType().Name).cshtml" model="meta"/>
<div style="display:none" id="metaContextDateToggle@(meta.When.ToFileTimeUtc())">
Event occured at <span class="text-light">@meta.When.ToStandardFormat()</span>
@foreach (var match in Utilities.SplitTranslationTokens("WEBFRONT_META_TIME_CONTEXT"))
{
if (match.IsInterpolation && match.MatchValue == "event")
{
<span class="text-light-dm text-dark-lm">@meta.When.ToStandardFormat()</span>
}
else
{
<span>@match.MatchValue</span>
}
}
</div>
</div>
}

View File

@ -5,7 +5,7 @@
Layout = null;
}
<div class="dropdown with-arrow" data-toggle="dropdown" id="alert-toggle" aria-haspopup="true" aria-expanded="false">
<div data-toggle="tooltip" data-title="@(Model.Any() ? "View Alerts" : "No Alerts")" data-placement="bottom">
<div data-toggle="tooltip" data-title="@(Model.Any() ? ViewBag.Localization["WEBFRONT_ALERTS_SOME_TOOLTIP"] : ViewBag.Localization["WEBFRONT_ALERTS_NONE_TOOLTIP"])" data-placement="bottom">
<i class="oi oi-bell mt-5"></i>
</div>
@if (Model.Any())
@ -13,7 +13,7 @@
<div class="position-absolute bg-danger rounded-circle ml-10" style="width: 0.5em;height: 0.5em;top: 0;"></div>
<div class="dropdown-menu dropdown-menu-right w-250 w-md-400" aria-labelledby="alert-toggle">
<div class="d-flex">
<h6 class="dropdown-header">@ViewBag.Alerts.Count Alerts</h6>
<h6 class="dropdown-header">@((ViewBag.Localization["WEBFRONT_ALERTS_POPOVER_COUNT"] as string).FormatExt((int)ViewBag.Alerts.Count))</h6>
<i class="oi oi-circle-x font-size-12 text-danger align-self-center profile-action" data-action="DismissAllAlerts" data-action-id="@ViewBag.User.ClientId"></i>
</div>
<div class="dropdown-divider"></div>
@ -42,7 +42,7 @@
<div class="font-size-12 p-5">
<span class="text-force-break">@alert.Message</span>
<div class="text-muted d-flex">
<span>@alert.OccuredAt.Humanize()</span>
<span>@alert.OccuredAt.HumanizeForCurrentCulture()</span>
@if (!string.IsNullOrEmpty(alert.Source))
{
<span class="ml-5 mr-5">&#8226;</span>

View File

@ -4,8 +4,8 @@
@{
Layout = null;
}
<div class="content-title">Recent Reports</div>
<div class="text-muted">Last 24 hours</div>
<div class="content-title">@ViewBag.Localization["WEBFRONT_MODAL_REPORTS_TITLE"]</div>
<div class="text-muted">@ViewBag.Localization["WEBFRONT_MODAL_REPORTS_SUBTITLE"]</div>
@foreach (var server in Model.Where(server => server.Reports.Any()).OrderByDescending(server => server.Reports.Max(report => report.ReportedOn)))
{
@ -27,13 +27,12 @@
};
<div class="font-weight-bold">@report.ReportedOn.HumanizeForCurrentCulture()</div>
<div class="font-size-12">
<a asp-action="Profile" asp-controller="Client" asp-route-id="@report.Target.ClientId">
<color-code value="@report.Target.Name"></color-code>
</a>
<a asp-action="Profile" asp-controller="Client" asp-route-id="@report.Target.ClientId">
<color-code value="@report.Target.Name"></color-code>
</a>
<partial name="~/Views/Client/Profile/Meta/_ReceivedPenaltyResponse.cshtml" for="@penalty"/>
</div>
</div>
<hr/>
}
</div>
}

View File

@ -23,14 +23,14 @@
{
<!-- desktop -->
<tr class="bg-dark-dm bg-light-lm d-none d-lg-table-row">
<td colspan="@Model.Columns.Count">No data...</td>
<td colspan="@Model.Columns.Count">@ViewBag.Localization["WEBFRONT_DATATABLE_NO_DATA"]</td>
</tr>
<!-- mobile -->
<tr class="d-flex d-table-row d-lg-none">
<td class="bg-primary text-light text-right w-125">
&mdash;
</td>
<td class="bg-dark-dm bg-light-lm flex-fill w-200">No data...</td>
<td class="bg-dark-dm bg-light-lm flex-fill w-200">@ViewBag.Localization["WEBFRONT_DATATABLE_NO_DATA"]</td>
</tr>
}
@foreach (var row in Model.Rows)
@ -103,7 +103,7 @@
</table>
@if (Model.InitialRowCount > 0 && Model.Rows.Count > 0)
{
<button class="btn btn-block table-slide" data-toggle="tooltip" data-title="Show @(Model.Rows.Count - Model.InitialRowCount) more rows">
<button class="btn btn-block table-slide" data-toggle="tooltip" data-title="@((ViewBag.Localization["WEBFRONT_DATATABLE_LOAD_MORE_FORMAT"] as string).FormatExt(Model.Rows.Count - Model.InitialRowCount))">
<span class="oi oi-chevron-bottom"></span>
</button>
}

View File

@ -52,7 +52,7 @@
<span aria-hidden="true">&times;</span>
</a>
<div id="actionModalContent">
<h4 class="mt-20">No content available yet...</h4>
<h4 class="mt-20">@ViewBag.Localization["WEBFRONT_GLOBAL_MODAL_EMPTY"]</h4>
</div>
</div>
</div>
@ -98,21 +98,21 @@
<div class="d-none d-md-block">
<div class="badge-group ml-20" role="group" aria-label="...">
<span class="badge badge-primary">@(ViewBag.ClientCount ?? "-")</span>
<span class="badge bg-dark-dm bg-light-lm">Clients</span>
<span class="badge bg-dark-dm bg-light-lm">@ViewBag.Localization["WEBFRONT_LAYOUT_CLIENTS_ONLINE"]</span>
</div>
<has-permission entity="PrivilegedClientsPage" required-permission="Read">
<div class="badge-group ml-10" role="group" aria-label="...">
<span class="badge badge-success">@(ViewBag.AdminCount ?? "-")</span>
<span class="badge bg-dark-dm bg-light-lm">Admins</span>
<span class="badge bg-dark-dm bg-light-lm">@ViewBag.Localization["WEBFRONT_LAYOUT_ADMINS_ONLINE"]</span>
</div>
</has-permission>
<has-permission entity="AdminMenu" required-permission="Read">
<a href="#actionModal" class="profile-action no-decoration" data-action="RecentReports" data-toggle="tooltip" data-title="View recent reports" data-placement="bottom">
<a href="#actionModal" class="profile-action no-decoration" data-action="RecentReports" data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_MODAL_REPORTS_TOOLTIP_TITLE"]" data-placement="bottom">
<div class="badge-group ml-10" role="group">
<span class="badge badge-danger">@(ViewBag.ReportCount ?? "-")</span>
<span class="badge bg-dark-dm bg-light-lm">Reports</span>
<span class="badge bg-dark-dm bg-light-lm">@ViewBag.Localization["WEBFRONT_LAYOUT_REPORTS"]</span>
</div>
</a>
</has-permission>
@ -122,7 +122,7 @@
<div class="align-self-center">
@await Html.PartialAsync("Partials/_Notifications", (object)ViewBag.Alerts)
</div>
<div class="btn btn-action mr-10 ml-10" onclick="halfmoon.toggleDarkMode()" data-toggle="tooltip" data-title="Toggle display mode" data-placement="bottom">
<div class="btn btn-action mr-10 ml-10" onclick="halfmoon.toggleDarkMode()" data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_LAYOUT_TOGGLE_DISPLAY"]" data-placement="bottom">
<i class="oi oi-moon"></i>
</div>
<div class="d-none d-md-block ">
@ -136,7 +136,6 @@
</a>
</div>
</div>
</nav>
<partial name="_LeftNavBar"/>
@ -149,18 +148,13 @@
<div id="mainLoadingBar" class="progress-bar position-absolute flex-fill position-fixed z-30" style="display: none">
<div class="progress-bar-value"></div>
</div>
@RenderBody()
<div class="content">
<div class="badge text-muted">threadsafe.pw</div>
</div>
</div>
</div>
<environment include="Development">
<script type="text/javascript" src="~/lib/jquery/dist/jquery.js"></script>
<script type="text/javascript" src="~/lib/moment.js/moment.js"></script>
<script type="text/javascript" src="~/lib/moment.js/min/moment-with-locales.js"></script>
<script type="text/javascript" src="~/lib/moment-timezone/moment-timezone.js"></script>
<script type="text/javascript" src="~/lib/chart.js/dist/Chart.bundle.min.js"></script>
<script type="text/javascript" src="~/lib/halfmoon/js/halfmoon.js"></script>
@ -177,6 +171,7 @@
$.each(_localizationTmp.set, function (key, value) {
_localization[key] = value;
});
moment.locale('@Utilities.CurrentLocalization.LocalizationName');
</script>
@await RenderSectionAsync("scripts", required: false)
@Html.Raw(ViewBag.ScriptInjection)

View File

@ -10,7 +10,7 @@
<div class="pr-20 pl-20 mb-20 d-block d-lg-none">
<partial name="_SearchResourceForm"/>
</div>
<span class="sidebar-title ">Main</span>
<span class="sidebar-title">@ViewBag.Localization["WEBFRONT_NAV_TITLE_MAIN"]</span>
<div class="sidebar-divider"></div>
<!-- servers -->
<a asp-controller="Home" asp-action="Index" class="sidebar-link">
@ -47,7 +47,7 @@
<has-permission entity="ProfilePage" required-permission="Read">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@ViewBag.User.ClientId" class="sidebar-link">
<i class="oi oi-person mr-5"></i>
<span class="name">Profile</span>
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_TITLE_PROFILE"]</span>
</a>
</has-permission>
@ -55,13 +55,13 @@
{
<a href="#actionModal" class="profile-action sidebar-link" data-action="login">
<i class="oi oi-key mr-5"></i>
<span class="name">Login</span>
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_TITLE_LOGIN"]</span>
</a>
}
<br/>
<!-- stats -->
<div class="sidebar-title ">Stats</div>
<div class="sidebar-title ">@ViewBag.Localization["WEBFRONT_NAV_TITLE_STATS"]</div>
<div class="sidebar-divider"></div>
@foreach (Page pageLink in ViewBag.Pages)
@ -74,39 +74,41 @@
<!-- scoreboard -->
<a asp-controller="Server" asp-action="Scoreboard" class="sidebar-link">
<i class="oi oi-spreadsheet mr-5"></i>
<span class="name">Scoreboard</span>
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_TITLE_SCOREBOARD"]</span>
</a>
<br/>
<!-- socials -->
@if (ViewBag.CommunityInformation?.IsEnabled && ViewBag.CommunityInformation.SocialAccounts.Length > 0)
{
<span class="sidebar-title ">Socials</span>
<span class="sidebar-title">@ViewBag.Localization["WEBFRONT_NAV_TITLE_SOCIALS"]</span>
<div class="sidebar-divider"></div>
}
@foreach (var social in ViewBag.CommunityInformation?.SocialAccounts ?? Array.Empty<SocialAccountConfiguration>())
{
<a href="@social.Url" class="sidebar-link" target="_blank" title="@social.Title">
@if (!string.IsNullOrWhiteSpace(social.IconId))
{
<i class="oi @social.IconId mr-5"></i>
}
else if (!string.IsNullOrWhiteSpace(social.IconUrl))
{
var url = Uri.TryCreate(social.IconUrl, UriKind.Absolute, out Uri parsedUrl)
? parsedUrl.AbsoluteUri
: $"/images/community/{social.IconUrl}";
<img class="img-fluid social-icon" style="max-height: 1.2rem" src="@url" alt="@social.Title"/>
}
<span class="name text-truncate">@social.Title</span>
<div class="d-flex align-items-center">
@if (!string.IsNullOrWhiteSpace(social.IconId))
{
<i class="oi @social.IconId align-self-center"></i>
}
else if (!string.IsNullOrWhiteSpace(social.IconUrl))
{
var url = Uri.TryCreate(social.IconUrl, UriKind.Absolute, out Uri parsedUrl)
? parsedUrl.AbsoluteUri
: $"/images/community/{social.IconUrl}";
<img class="img-fluid social-icon align-self-center" src="@url" alt="@social.Title"/>
}
<div class="name text-truncate align-self-center">@social.Title</div>
</div>
</a>
}
<br/>
<!-- admin -->
<has-permission entity="AdminMenu" required-permission="Read">
<div class="sidebar-title ">Admin</div>
<div class="sidebar-title">@ViewBag.Localization["WEBFRONT_NAV_TITLE_ADMIN"]</div>
<div class="sidebar-divider"></div>
<has-permission entity="ConsolePage" required-permission="Read">
@ -118,14 +120,14 @@
<has-permission entity="Penalty" required-permission="Read"></has-permission>
<a asp-controller="Admin" asp-action="BanManagement" class="sidebar-link">
<i class="oi oi-ban mr-5"></i>
<span class="name">Ban Management</span>
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_TITLE_BAN_MANAGEMENT"]</span>
</a>
</has-permission>
@if (ViewBag.User.Level >= EFClient.Permission.Owner)
{
<a asp-controller="Configuration" asp-action="Edit" class="sidebar-link">
<i class="oi oi-cog mr-5"></i>
<span class="name">Configuration</span>
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_TITLE_CONFIGURATION"]</span>
</a>
}
<has-permission entity="AuditPage" required-permission="Read">
@ -140,7 +142,7 @@
<span class="name">@ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"]</span>
</a>
</has-permission>
@if (ViewBag.Authorized)
{
<a class="sidebar-link profile-action" href="#actionModal" data-action="GenerateLoginToken" data-response-duration="30000" title="@ViewBag.Localization["WEBFRONT_ACTION_TOKEN"]">
@ -158,11 +160,21 @@
<div class="sidebar-link font-size-12 font-weight-light">
@if (ViewBag.Authorized)
{
<span>Logged in as <color-code value="@ViewBag.User.Name"></color-code></span>
@foreach (var match in Utilities.SplitTranslationTokens(ViewBag.Localization["WEBFRONT_NAV_LOGIN_YES_FORMAT"]))
{
if (match.IsInterpolation && match.MatchValue == "username")
{
<color-code value="@ViewBag.User.Name"></color-code>
}
else
{
<span>@match.MatchValue</span>
}
}
}
else
{
<span>Not logged in</span>
<span>@ViewBag.Localization["WEBFRONT_NAV_LOGIN_NO_FORMAT"]</span>
}
</div>
<div class="sidebar-divider mt-0 mb-0"></div>

View File

@ -5,15 +5,15 @@
<form class="action-form" asp-action="Login" asp-controller="Account">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon-clientId">Client ID</span>
<span class="input-group-text" id="basic-addon-clientId">@ViewBag.Localization["WEBFRONT_LOGIN_MODAL_FORM_CLIENTID"]</span>
</div>
<input type="text" name="clientId" value="" class="form-control" aria-label="clientId" aria-describedby="basic-addon-clientId">
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon-Password">Token/Password</span>
<span class="input-group-text" id="basic-addon-Password">@ViewBag.Localization["WEBFRONT_LOGIN_MODAL_FORM_PASSWORD"]</span>
</div>
<input type="password" name="Password" value="" class="form-control" aria-label="Password" aria-describedby="basic-addon-Password">
</div>
<button type="submit" class="btn btn-block btn-primary">Login</button>
<button type="submit" class="btn btn-block btn-primary">@ViewBag.Localization["WEBFRONT_LOGIN_BUTTON_SUBMIT"]</button>
</form>

View File

@ -37,7 +37,7 @@
</div>
}
<hr/>
<a href="#" class="btn btn-lg btn-danger btn-block mt-15" role="button">Close</a>
<button class="btn btn-lg btn-danger btn-block mt-15" data-dismiss="modal" type="button">@ViewBag.Localization["WEBFRONT_CONTEXT_MENU_BUTTON_CLOSE"]</button>
</div>
</div>
</div>

View File

@ -12,7 +12,7 @@
"outputFileName": "wwwroot/js/global.min.js",
"inputFiles": [
"wwwroot/lib/jquery/dist/jquery.js",
"wwwroot/lib/moment.js/moment.min.js",
"wwwroot/lib/moment.js/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",

View File

@ -449,11 +449,10 @@ table.with-auto-width td {
}
img.social-icon {
max-height: 1.75rem;
margin-right: 0.6rem;
margin-top: 3px;
height: 1.6rem;
}
.sidebar-link .oi, .sidebar-link img {
min-width: 1.2rem;
margin-right: 0.75rem;
}

View File

@ -1 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><defs><style>.a{fill:#dadada;}</style></defs><title>discord-icon</title><path class="a" d="M184.74,204.24s-7.43-8.76-13.55-16.69c26.93-7.6,37.18-24.45,37.18-24.45a110.2,110.2,0,0,1-23.63,12.06A127.88,127.88,0,0,1,155,183.92a146.82,146.82,0,0,1-53.19-.17A191.59,191.59,0,0,1,71.58,175a119.6,119.6,0,0,1-15-6.94c-.66-.33-1.15-.66-1.82-1a1.64,1.64,0,0,1-.82-.66c-3.64-2-5.78-3.47-5.78-3.47s9.91,16.35,36,24.28c-6.11,7.77-13.71,17-13.71,17C25,202.75,7.81,173,7.81,173c0-66.08,29.57-119.77,29.57-119.77C67,31.27,95,31.76,95,31.76l2,2.48C60,45,43,61.17,43,61.17s4.47-2.48,12.06-6c22-9.58,39.49-12.39,46.59-12.88a24.1,24.1,0,0,1,3.47-.33,172.18,172.18,0,0,1,41.47-.33,167.5,167.5,0,0,1,61.79,19.65S192.18,46,157.15,35.23l2.81-3.3s28.09-.66,57.66,21.64c0,0,29.57,53.53,29.57,119.77C247.52,173,230.17,202.92,184.74,204.24ZM89.25,108.42c-11.73,0-21,10.24-21,22.8s9.42,22.8,21,22.8c11.73,0,21-10.25,21-22.8C110.4,118.66,101,108.42,89.25,108.42Zm75,0c-11.73,0-21,10.24-21,22.8s9.42,22.8,21,22.8c11.73,0,21-10.25,21-22.8C185.07,118.66,176,108.42,164.26,108.42Z"/></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="352"
height="352"
viewBox="0 0 352 352"
overflow="hidden"
version="1.1"
id="svg13"
sodipodi:docname="Discord_Canary_2021.svg"
inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)">
<metadata
id="metadata17">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1001"
id="namedview15"
showgrid="false"
inkscape:zoom="1.0665722"
inkscape:cx="53.327387"
inkscape:cy="169.20483"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="g11" />
<g
id="g11">
<path
d="M 0,176 C 0,78.798 78.798,0 176,0 273.202,0 352,78.798 352,176 352,273.202 273.202,352 176,352 78.798,352 0,273.202 0,176 Z"
fill-rule="evenodd"
id="path7"
style="fill:#117ac0;fill-opacity:1" />
<path
d="M 252.13119,104.48694 C 237.93314,98.045781 222.75448,93.373058 206.88505,90.72087 204.9364,94.131234 202.65828,98.727853 201.08931,102.36558 184.21995,99.915058 167.50615,99.915058 150.94499,102.36558 149.37697,98.727853 147.04767,94.131234 145.08065,90.72087 129.19576,93.373058 113.99778,98.071466 99.801667,104.53831 71.166938,146.4701 63.407091,187.39257 67.287014,227.70905 86.278932,241.47608 104.68345,249.81126 122.77785,255.29352 127.24421,249.33181 131.22943,242.99149 134.66302,236.29727 128.12434,233.89718 121.86488,230.9168 115.94644,227.48169 117.51832,226.34492 119.05058,225.18244 120.53549,223.97051 156.62092,240.31361 195.82978,240.31361 231.4843,223.97051 232.98372,225.18244 234.51985,226.34492 236.07048,227.48169 230.13754,230.94249 223.85875,233.92286 217.32103,236.32296 220.75463,242.99149 224.72149,249.33181 229.2062,255.29352 247.31798,249.83695 265.74086,241.47608 284.73278,227.70905 289.2861,180.97613 276.95265,140.43323 252.13119,104.48694 Z M 139.57861,202.92803 C 128.74748,202.92803 119.8621,193.10219 119.8621,181.17874 119.8621,169.23058 128.55523,159.4038 139.57861,159.4038 150.60104,159.4038 159.48352,169.20491 159.29512,181.17874 159.31251,193.10219 150.60104,202.92803 139.57861,202.92803 Z M 212.43829,202.92803 C 201.60715,202.92803 192.72469,193.10219 192.72469,181.17874 192.72469,169.23058 201.41876,159.4038 212.43829,159.4038 223.46168,159.4038 232.34609,169.20491 232.15479,181.17874 232.15479,193.10219 223.46168,202.92803 212.43829,202.92803 Z"
fill="#ffffff"
fill-rule="evenodd"
id="path9" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1 +1,15 @@
<svg role="img" fill="white" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="97px" height="97px" viewBox="0 0 97 97" enable-background="new 0 0 97 97" xml:space="preserve">
<g>
<path fill="#ff6060" d="M92.71,44.408L52.591,4.291c-2.31-2.311-6.057-2.311-8.369,0l-8.33,8.332L46.459,23.19
c2.456-0.83,5.272-0.273,7.229,1.685c1.969,1.97,2.521,4.81,1.67,7.275l10.186,10.185c2.465-0.85,5.307-0.3,7.275,1.671
c2.75,2.75,2.75,7.206,0,9.958c-2.752,2.751-7.208,2.751-9.961,0c-2.068-2.07-2.58-5.11-1.531-7.658l-9.5-9.499v24.997
c0.67,0.332,1.303,0.774,1.861,1.332c2.75,2.75,2.75,7.206,0,9.959c-2.75,2.749-7.209,2.749-9.957,0c-2.75-2.754-2.75-7.21,0-9.959
c0.68-0.679,1.467-1.193,2.307-1.537V36.369c-0.84-0.344-1.625-0.853-2.307-1.537c-2.083-2.082-2.584-5.14-1.516-7.698
L31.798,16.715L4.288,44.222c-2.311,2.313-2.311,6.06,0,8.371l40.121,40.118c2.31,2.311,6.056,2.311,8.369,0L92.71,52.779
C95.021,50.468,95.021,46.719,92.71,44.408z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 835 B

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -26,7 +26,7 @@ function getUrlParameter(sParam) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) {
return sParameterName[1] === undefined ? true : decodeURIComponent(sParameterName[1]);
return sParameterName[1] === undefined ? true : decodeURIComponent(unescape(sParameterName[1]));
}
}
return false;
@ -57,6 +57,15 @@ function escapeHtml (string) {
});
}
function buildToastUri(message, duration) {
let uri = '&';
if (window.location.href.toString().indexOf('?') <= 0) {
uri = '?';
}
uri += `toastMessage=${escape(message)}${duration ? `&duration=${duration}` : ''}`;
return uri;
}
$(document).ready(function () {
let toastMessage = getUrlParameter('toastMessage');
@ -70,7 +79,7 @@ $(document).ready(function () {
clearQueryString();
halfmoon.initStickyAlert({
content: toastMessage,
title: 'Success',
title: _localization['WEBFRONT_SCRIPT_ACTION_SUCCESS'],
alertType: 'alert-success',
fillType: 'filled',
timeShown: duration
@ -98,7 +107,7 @@ $(document).ready(function () {
.fail(function (jqxhr, textStatus, error) {
halfmoon.initStickyAlert({
content: jqxhr.responseText,
title: 'Error',
title: _localization['WEBFRONT_SCRIPT_ACTION_ERROR'],
alertType: 'alert-danger',
fillType: 'filled'
});
@ -130,13 +139,13 @@ $(document).ready(function () {
}
catch{}
if (shouldRefresh) {
window.location = `${window.location.href.replace('#', '')}?toastMessage=${escape(message)}${duration ? `&duration=${duration}` : ''}`;
window.location = `${window.location.href.replace('#', '')}${buildToastUri(message, duration)}`;
}
else {
modal.modal();
halfmoon.initStickyAlert({
content: escapeHtml(message),
title: 'Executed',
title: _localization['WEBFRONT_SCRIPT_ACTION_EXECUTED'],
alertType: 'alert-primary',
fillType: 'filled'
});
@ -165,7 +174,7 @@ $(document).ready(function () {
halfmoon.initStickyAlert({
content: message,
title: 'Error',
title: _localization['WEBFRONT_SCRIPT_ACTION_ERROR'],
alertType: 'alert-danger',
fillType: 'filled'
});

View File

@ -321,13 +321,16 @@ function renderPerformanceChart() {
}
const labels = [];
const values = [];
data.forEach(function (item, i) {
labels.push(i);
labels.push(item.OccurredAt);
values.push(item.Performance)
});
const padding = 4;
let dataMin = Math.min(...data);
const dataMax = Math.max(...data);
let dataMin = Math.min(...values);
const dataMax = Math.max(...values);
if (dataMax - dataMin === 0) {
dataMin = 0;
@ -341,7 +344,7 @@ function renderPerformanceChart() {
const chartData = {
labels: labels,
datasets: [{
data: data,
data: values,
pointBackgroundColor: 'rgba(255, 255, 255, 0)',
pointBorderColor: 'rgba(255, 255, 255, 0)',
pointHoverRadius: 5,
@ -356,8 +359,8 @@ function renderPerformanceChart() {
legend: false,
tooltips: {
callbacks: {
label: (tooltipItem) => Math.round(tooltipItem.yLabel) + ' ' + _localization["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
title: () => ''
label: context => moment.utc(context.label).local().calendar(),
title: items => Math.round(items[0].yLabel) + ' ' + _localization["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
},
mode: 'nearest',
intersect: false,
@ -390,6 +393,8 @@ function renderPerformanceChart() {
position: 'right',
ticks: {
precision: 0,
stepSize: 3,
callback: function (value, index, values) {
if (index === values.length - 1) {
return min;

View File

@ -29,7 +29,7 @@
$('#console_command_response').append(`<div class="text-danger">${escapeHtml(item)}</div>`);
})
} else {
$('#console_command_response').append(`<div class="text-danger">Could not execute command...</div>`);
$('#console_command_response').append(`<div class="text-danger">${_localization['WEBFRONT_SCRIPT_CONSOLE_ERROR']}</div>`);
}
});
}

View File

@ -73,7 +73,7 @@ function loadMoreItems() {
.fail(function () {
errorLoader();
halfmoon.initStickyAlert({
content: 'Could not load more items...',
content: _localization['WEBFRONT_SCRIPT_LOADER_ERROR'],
title: 'Error',
alertType: 'alert-danger',
fillType: 'filled'

View File

@ -88,7 +88,7 @@ function getPlayerHistoryChart(playerHistory, i, width, maxClients) {
callbacks: {
// todo: localization at some point
title: context => moment(context[0].label).local().calendar(),
label: context => context.datasetIndex !== 1 ? `${context.value} players on ${playerHistory[context.index].mapAlias}` : context.value === '0' ? '' : 'Server Unreachable!',
label: context => context.datasetIndex !== 1 ? `${context.value} ${_localization['WEBFRONT_SCRIPT_SERVER_PLAYERS']} | ${playerHistory[context.index].mapAlias}` : context.value === '0' ? '' : _localization['WEBFRONT_SCRIPT_SERVER_UNREACHABLE'],
},
mode: 'nearest',
intersect: false,

View File

@ -1,83 +1,127 @@
function getStatsChart(id, width, height) {
function getClosestMultiple(baseValue, value) {
return Math.round(value / baseValue) * baseValue;
}
function getStatsChart(id) {
const data = $('#' + id).data('history');
if (data === undefined) {
return;
}
let fixedData = [];
data.forEach(function (item, i) {
fixedData[i] = { x: i, y: Math.floor(item) };
});
if (data.length <= 1) {
// only 0 perf
return;
}
let dataMin = Math.min(...data);
const dataMax = Math.max(...data);
const labels = [];
const values = [];
data.forEach(function (item, i) {
labels.push(item.OccurredAt);
values.push(item.Performance)
});
const padding = 4;
let dataMin = Math.min(...values);
const dataMax = Math.max(...values);
if (dataMax - dataMin === 0) {
dataMin = 0;
}
const padding = (dataMax - dataMin) * 0.5;
const min = Math.max(0, dataMin - padding);
const max = dataMax + padding;
let interval = Math.floor((max - min) / 2);
dataMin = Math.max(0, dataMin);
if (interval < 1)
interval = 1;
const min = getClosestMultiple(padding, dataMin - padding);
const max = getClosestMultiple(padding, dataMax + padding);
return new CanvasJS.Chart(id, {
backgroundColor: 'transparent',
height: height,
width: width,
animationEnabled: false,
toolTip: {
contentFormatter: function (e) {
return `${_localization['WEBFRONT_ADV_STATS_RANKING_METRIC']} ${Math.round(e.entries[0].dataPoint.y, 1)}`;
const chartData = {
labels: labels,
datasets: [{
data: values,
pointBackgroundColor: 'rgba(255, 255, 255, 0)',
pointBorderColor: 'rgba(255, 255, 255, 0)',
pointHoverRadius: 5,
pointHoverBackgroundColor: 'rgba(255, 255, 255, 1)',
}]
};
const options = {
defaultFontFamily: "-apple-system, BlinkMacSystemFont, 'Open Sans', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'",
responsive: true,
maintainAspectRatio: false,
legend: false,
tooltips: {
callbacks: {
label: context => moment.utc(context.label).local().calendar(),
title: items => Math.round(items[0].yLabel) + ' ' + _localization['WEBFRONT_ADV_STATS_RANKING_METRIC']
},
mode: 'nearest',
intersect: false,
animationDuration: 0,
cornerRadius: 0,
displayColors: false
},
hover: {
mode: 'nearest',
intersect: false
},
elements: {
line: {
fill: false,
borderColor: halfmoon.getPreferredMode() === 'light-mode' ? 'rgba(0, 0, 0, 0.85)' : 'rgba(255, 255, 255, 0.75)',
borderWidth: 2
},
point: {
radius: 5
}
},
title: {
fontSize: 0
scales: {
xAxes: [{
display: false,
}],
yAxes: [{
gridLines: {
display: false
},
position: 'right',
ticks: {
precision: 0,
stepSize: 3,
callback: function (value, index, values) {
if (index === values.length - 1) {
return min;
} else if (index === 0) {
return max;
} else {
return '';
}
},
fontColor: 'rgba(255, 255, 255, 0.25)'
}
}]
},
axisX: {
gridThickness: 0,
lineThickness: 0,
tickThickness: 0,
margin: 0,
valueFormatString: ' '
layout: {
padding: {
left: 15
}
},
axisY: {
labelFontSize: 12,
interval: interval,
gridThickness: 0,
lineThickness: 0.5,
valueFormatString: '#,##0',
minimum: min,
maximum: max
},
legend: {
dockInsidePlotArea: true
},
data: [{
type: 'spline',
color: '#c0c0c0',
markerSize: 0,
dataPoints: fixedData,
lineThickness: 2
}]
});
};
new Chart(id, {
type: 'line',
data: chartData,
options: options
});
}
$(document).ready(function () {
$('.client-rating-graph').each(function (i, element) {
getStatsChart($(element).attr('id'), $(element).width(), $(element).height()).render();
});
$(window).resize(function () {
$('.client-rating-graph').each(function (index, element) {
getStatsChart($(element).attr('id'), $(element).width(), $(element).height()).render();
});
getStatsChart($(element).children('canvas').attr('id'));
});
$('.top-players-link').click(function (event) {
$($(this).attr('href')).html('');
initLoader('/Stats/GetTopPlayersAsync?serverId=' + $(this).data('serverid'), $(this).attr('href'), 10, 0);
@ -85,9 +129,9 @@ $(document).ready(function () {
});
});
$(document).on("loaderFinished", function (event, response) {
const ids = $.map($(response).find('.client-rating-graph'), function (elem) { return $(elem).attr('id'); });
$(document).on('loaderFinished', function (event, response) {
const ids = $.map($(response).find('.client-rating-graph'), function (elem) { return $(elem).children('canvas').attr('id'); });
ids.forEach(function (item, index) {
getStatsChart(item, $(item).width(), $(item).height()).render();
getStatsChart(item);
});
});