Compare commits
28 Commits
2022.06.16
...
2022.07.13
Author | SHA1 | Date | |
---|---|---|---|
0a55c54c42 | |||
f43f7b5040 | |||
540cf7489d | |||
1a72faee60 | |||
4e44bb5ea1 | |||
9e17bcc38f | |||
4b33b33d01 | |||
6f1bc7ab90 | |||
63e1774cb6 | |||
61df873bb1 | |||
052eeb0615 | |||
88e67747fe | |||
5db94723aa | |||
ea8216ecdf | |||
6abbcbe464 | |||
57484690b6 | |||
7a022a1973 | |||
7108e23a03 | |||
77d25890da | |||
2fca68a7ea | |||
a6c0a94f6c | |||
71abaac9e1 | |||
e07651b931 | |||
5a2ee36df9 | |||
2daa4991d1 | |||
775c0a91b5 | |||
55bccc7d3d | |||
4322e8d882 |
@ -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]
|
||||
|
@ -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": [
|
||||
|
@ -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());
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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()));
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 )
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
@ -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) {}
|
||||
};
|
||||
};
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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()))
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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]
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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"]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">–</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>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -41,7 +41,6 @@
|
||||
{
|
||||
<color-code value="@Model.Offense"></color-code>
|
||||
}
|
||||
|
||||
</span>
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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" : "")">
|
||||
|
@ -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 }),
|
||||
|
@ -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"/>
|
||||
|
@ -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>
|
||||
— <span class="text-primary">@ViewBag.TotalRankedClients.ToString("#,##0")</span> Ranked Players
|
||||
— <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 }),
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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"]
|
||||
|
@ -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>
|
||||
|
@ -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 }),
|
||||
|
@ -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>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 })
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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 }),
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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 -->
|
||||
|
@ -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"/>
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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">•</span>
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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">
|
||||
—
|
||||
</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>
|
||||
}
|
||||
|
@ -52,7 +52,7 @@
|
||||
<span aria-hidden="true">×</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)
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 |
@ -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 |
@ -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'
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user