Compare commits

...

12 Commits

31 changed files with 321 additions and 213 deletions

View File

@ -92,7 +92,7 @@ namespace IW4MAdmin.Application.Commands
_logger.LogDebug("Changing map to {Map} and gametype {Gametype}", map, gametype); _logger.LogDebug("Changing map to {Map} and gametype {Gametype}", map, gametype);
await gameEvent.Owner.SetDvarAsync("g_gametype", gametype); await gameEvent.Owner.SetDvarAsync("g_gametype", gametype, gameEvent.Owner.Manager.CancellationToken);
gameEvent.Owner.Broadcast(_translationLookup["COMMANDS_MAP_SUCCESS"].FormatExt(map)); gameEvent.Owner.Broadcast(_translationLookup["COMMANDS_MAP_SUCCESS"].FormatExt(map));
await Task.Delay(gameEvent.Owner.Manager.GetApplicationSettings().Configuration().MapChangeDelaySeconds); await Task.Delay(gameEvent.Owner.Manager.GetApplicationSettings().Configuration().MapChangeDelaySeconds);

View File

@ -66,8 +66,8 @@ namespace IW4MAdmin.Application.IO
} }
return Task.FromResult(Enumerable.Empty<GameEvent>()); return Task.FromResult(Enumerable.Empty<GameEvent>());
} }
new Thread(() => ReadNetworkData(client)).Start(); Task.Run(async () => await ReadNetworkData(client, _token), _token);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -111,9 +111,9 @@ namespace IW4MAdmin.Application.IO
return Task.FromResult((IEnumerable<GameEvent>)events); return Task.FromResult((IEnumerable<GameEvent>)events);
} }
private void ReadNetworkData(UdpClient client) private async Task ReadNetworkData(UdpClient client, CancellationToken token)
{ {
while (!_token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
// get more data // get more data
IPEndPoint remoteEndpoint = null; IPEndPoint remoteEndpoint = null;
@ -127,7 +127,13 @@ namespace IW4MAdmin.Application.IO
try try
{ {
bufferedData = client.Receive(ref remoteEndpoint); var result = await client.ReceiveAsync(_token);
remoteEndpoint = result.RemoteEndPoint;
bufferedData = result.Buffer;
}
catch (OperationCanceledException)
{
_logger.LogDebug("Stopping network log receive");
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -321,7 +321,7 @@ namespace IW4MAdmin
if (!string.IsNullOrEmpty(CustomSayName)) if (!string.IsNullOrEmpty(CustomSayName))
{ {
await this.SetDvarAsync("sv_sayname", CustomSayName); await this.SetDvarAsync("sv_sayname", CustomSayName, Manager.CancellationToken);
} }
Throttled = false; Throttled = false;
@ -783,7 +783,7 @@ namespace IW4MAdmin
async Task<List<EFClient>[]> PollPlayersAsync() async Task<List<EFClient>[]> PollPlayersAsync()
{ {
var currentClients = GetClientsAsList(); var currentClients = GetClientsAsList();
var statusResponse = (await this.GetStatusAsync()); var statusResponse = await this.GetStatusAsync(Manager.CancellationToken);
var polledClients = statusResponse.Clients.AsEnumerable(); var polledClients = statusResponse.Clients.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots) if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
@ -1109,7 +1109,7 @@ namespace IW4MAdmin
RemoteConnection = RConConnectionFactory.CreateConnection(ResolvedIpEndPoint, Password, RconParser.RConEngine); RemoteConnection = RConConnectionFactory.CreateConnection(ResolvedIpEndPoint, Password, RconParser.RConEngine);
RemoteConnection.SetConfiguration(RconParser); RemoteConnection.SetConfiguration(RconParser);
var version = await this.GetMappedDvarValueOrDefaultAsync<string>("version"); var version = await this.GetMappedDvarValueOrDefaultAsync<string>("version", token: Manager.CancellationToken);
Version = version.Value; Version = version.Value;
GameName = Utilities.GetGame(version.Value ?? RconParser.Version); GameName = Utilities.GetGame(version.Value ?? RconParser.Version);
@ -1126,7 +1126,7 @@ namespace IW4MAdmin
Version = RconParser.Version; Version = RconParser.Version;
} }
var svRunning = await this.GetMappedDvarValueOrDefaultAsync<string>("sv_running"); var svRunning = await this.GetMappedDvarValueOrDefaultAsync<string>("sv_running", token: Manager.CancellationToken);
if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1") if (!string.IsNullOrEmpty(svRunning.Value) && svRunning.Value != "1")
{ {
@ -1135,27 +1135,28 @@ namespace IW4MAdmin
var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null; var infoResponse = RconParser.Configuration.CommandPrefixes.RConGetInfo != null ? await this.GetInfoAsync() : null;
string hostname = (await this.GetMappedDvarValueOrDefaultAsync<string>("sv_hostname", "hostname", infoResponse)).Value; var hostname = (await this.GetMappedDvarValueOrDefaultAsync<string>("sv_hostname", "hostname", infoResponse, token: Manager.CancellationToken)).Value;
string mapname = (await this.GetMappedDvarValueOrDefaultAsync<string>("mapname", infoResponse: infoResponse)).Value; var mapname = (await this.GetMappedDvarValueOrDefaultAsync<string>("mapname", infoResponse: infoResponse, token: Manager.CancellationToken)).Value;
int maxplayers = (await this.GetMappedDvarValueOrDefaultAsync<int>("sv_maxclients", infoResponse: infoResponse)).Value; var maxplayers = (await this.GetMappedDvarValueOrDefaultAsync<int>("sv_maxclients", infoResponse: infoResponse, token: Manager.CancellationToken)).Value;
string gametype = (await this.GetMappedDvarValueOrDefaultAsync<string>("g_gametype", "gametype", infoResponse)).Value; var gametype = (await this.GetMappedDvarValueOrDefaultAsync<string>("g_gametype", "gametype", infoResponse, token: Manager.CancellationToken)).Value;
var basepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basepath"); var basepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basepath", token: Manager.CancellationToken);
var basegame = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basegame"); var basegame = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_basegame", token: Manager.CancellationToken);
var homepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_homepath"); var homepath = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_homepath", token: Manager.CancellationToken);
var game = (await this.GetMappedDvarValueOrDefaultAsync<string>("fs_game", infoResponse: infoResponse)); var game = await this.GetMappedDvarValueOrDefaultAsync<string>("fs_game", infoResponse: infoResponse, token: Manager.CancellationToken);
var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log"); var logfile = await this.GetMappedDvarValueOrDefaultAsync<string>("g_log", token: Manager.CancellationToken);
var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync"); var logsync = await this.GetMappedDvarValueOrDefaultAsync<int>("g_logsync", token: Manager.CancellationToken);
var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip"); var ip = await this.GetMappedDvarValueOrDefaultAsync<string>("net_ip", token: Manager.CancellationToken);
var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: ""); var gamePassword = await this.GetMappedDvarValueOrDefaultAsync("g_password", overrideDefault: "", token: Manager.CancellationToken);
if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName) if (Manager.GetApplicationSettings().Configuration().EnableCustomSayName)
{ {
await this.SetDvarAsync("sv_sayname", Manager.GetApplicationSettings().Configuration().CustomSayName); await this.SetDvarAsync("sv_sayname", Manager.GetApplicationSettings().Configuration().CustomSayName,
Manager.CancellationToken);
} }
try try
{ {
var website = await this.GetMappedDvarValueOrDefaultAsync<string>("_website"); var website = await this.GetMappedDvarValueOrDefaultAsync<string>("_website", token: Manager.CancellationToken);
// this occurs for games that don't give us anything back when // this occurs for games that don't give us anything back when
// the dvar is not set // the dvar is not set
@ -1201,14 +1202,14 @@ namespace IW4MAdmin
if (logsync.Value == 0) if (logsync.Value == 0)
{ {
await this.SetDvarAsync("g_logsync", 2); // set to 2 for continous in other games, clamps to 1 for IW4 await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4
needsRestart = true; needsRestart = true;
} }
if (string.IsNullOrWhiteSpace(logfile.Value)) if (string.IsNullOrWhiteSpace(logfile.Value))
{ {
logfile.Value = "games_mp.log"; logfile.Value = "games_mp.log";
await this.SetDvarAsync("g_log", logfile.Value); await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken);
needsRestart = true; needsRestart = true;
} }
@ -1220,7 +1221,7 @@ namespace IW4MAdmin
} }
// this DVAR isn't set until the a map is loaded // this DVAR isn't set until the a map is loaded
await this.SetDvarAsync("logfile", 2); await this.SetDvarAsync("logfile", 2, Manager.CancellationToken);
} }
CustomCallback = await ScriptLoaded(); CustomCallback = await ScriptLoaded();

View File

@ -153,6 +153,8 @@ namespace IW4MAdmin.Application
{ {
Console.WriteLine(e.Message); Console.WriteLine(e.Message);
} }
_serverManager?.Stop();
Console.WriteLine(exitMessage); Console.WriteLine(exitMessage);
await Console.In.ReadAsync(new char[1], 0, 1); await Console.In.ReadAsync(new char[1], 0, 1);

View File

@ -88,7 +88,7 @@ namespace IW4MAdmin.Application.Migration
public static void RemoveObsoletePlugins20210322() public static void RemoveObsoletePlugins20210322()
{ {
var files = new[] {"StatsWeb.dll", "StatsWeb.Views.dll"}; var files = new[] {"StatsWeb.dll", "StatsWeb.Views.dll", "IW4ScriptCommands.dll"};
foreach (var file in files) foreach (var file in files)
{ {

View File

@ -27,6 +27,7 @@ namespace IW4MAdmin.Application.Misc
_serializerOptions = new JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
{ {
WriteIndented = true, WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}; };
_serializerOptions.Converters.Add(new JsonStringEnumConverter()); _serializerOptions.Converters.Add(new JsonStringEnumConverter());
_onSaving = new SemaphoreSlim(1, 1); _onSaving = new SemaphoreSlim(1, 1);

View File

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models; using Data.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -77,19 +78,19 @@ namespace IW4MAdmin.Application.RConParsers
public string RConEngine { get; set; } = "COD"; public string RConEngine { get; set; } = "COD";
public bool IsOneLog { get; set; } public bool IsOneLog { get; set; }
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command) public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default)
{ {
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command); var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token);
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray(); return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
} }
public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default) public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default)
{ {
string[] lineSplit; string[] lineSplit;
try try
{ {
lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName); lineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.GET_DVAR, dvarName, token);
} }
catch catch
{ {
@ -98,10 +99,10 @@ namespace IW4MAdmin.Application.RConParsers
throw; throw;
} }
lineSplit = new string[0]; lineSplit = Array.Empty<string>();
} }
string response = string.Join('\n', lineSplit).TrimEnd('\0'); var response = string.Join('\n', lineSplit).TrimEnd('\0');
var match = Regex.Match(response, Configuration.Dvar.Pattern); var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (response.Contains("Unknown command") || if (response.Contains("Unknown command") ||
@ -109,7 +110,7 @@ namespace IW4MAdmin.Application.RConParsers
{ {
if (fallbackValue != null) if (fallbackValue != null)
{ {
return new Dvar<T>() return new Dvar<T>
{ {
Name = dvarName, Name = dvarName,
Value = fallbackValue Value = fallbackValue
@ -119,17 +120,17 @@ namespace IW4MAdmin.Application.RConParsers
throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName)); throw new DvarException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_DVAR"].FormatExt(dvarName));
} }
string value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value; var value = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarValue]].Value;
string defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value; var defaultValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarDefaultValue]].Value;
string latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value; var latchedValue = match.Groups[Configuration.Dvar.GroupMapping[ParserRegex.GroupType.RConDvarLatchedValue]].Value;
string removeTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", ""); string RemoveTrailingColorCode(string input) => Regex.Replace(input, @"\^7$", "");
value = removeTrailingColorCode(value); value = RemoveTrailingColorCode(value);
defaultValue = removeTrailingColorCode(defaultValue); defaultValue = RemoveTrailingColorCode(defaultValue);
latchedValue = removeTrailingColorCode(latchedValue); latchedValue = RemoveTrailingColorCode(latchedValue);
return new Dvar<T>() return new Dvar<T>
{ {
Name = dvarName, Name = dvarName,
Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)), Value = string.IsNullOrEmpty(value) ? default : (T)Convert.ChangeType(value, typeof(T)),
@ -139,10 +140,12 @@ namespace IW4MAdmin.Application.RConParsers
}; };
} }
public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection) public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default)
{ {
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS); var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token);
_logger.LogDebug("Status Response {response}", string.Join(Environment.NewLine, response));
_logger.LogDebug("Status Response {Response}", string.Join(Environment.NewLine, response));
return new StatusResponse return new StatusResponse
{ {
Clients = ClientsFromStatus(response).ToArray(), Clients = ClientsFromStatus(response).ToArray(),
@ -183,13 +186,13 @@ namespace IW4MAdmin.Application.RConParsers
return (T)Convert.ChangeType(value, typeof(T)); return (T)Convert.ChangeType(value, typeof(T));
} }
public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue) public async Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default)
{ {
string dvarString = (dvarValue is string str) var dvarString = (dvarValue is string str)
? $"{dvarName} \"{str}\"" ? $"{dvarName} \"{str}\""
: $"{dvarName} {dvarValue}"; : $"{dvarName} {dvarValue}";
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString)).Length > 0; return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0;
} }
private List<EFClient> ClientsFromStatus(string[] Status) private List<EFClient> ClientsFromStatus(string[] Status)

View File

@ -66,7 +66,12 @@ OnPlayerConnect()
player thread OnPlayerSpawned(); player thread OnPlayerSpawned();
player thread PlayerTrackingOnInterval(); player thread PlayerTrackingOnInterval();
player ToggleNightMode();
// only toggle if it's enabled
if ( IsDefined( level.nightModeEnabled ) && level.nightModeEnabled )
{
player ToggleNightMode();
}
} }
} }
@ -496,7 +501,7 @@ OnExecuteCommand( event )
} }
else else
{ {
response = self GotoImpl( event.data ); response = self GotoImpl( data );
} }
break; break;
case "Kill": case "Kill":
@ -505,6 +510,9 @@ OnExecuteCommand( event )
case "NightMode": case "NightMode":
NightModeImpl(); NightModeImpl();
break; break;
case "SetSpectator":
response = event.target SetSpectatorImpl();
break;
} }
// send back the response to the origin, but only if they're not the target // send back the response to the origin, but only if they're not the target
@ -590,8 +598,8 @@ HideImpl()
} }
self SetClientDvar( "sv_cheats", 1 ); self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 ); self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 ); self SetClientDvar( "sv_cheats", 0 );
self.savedHealth = self.health; self.savedHealth = self.health;
self.health = 9999; self.health = 9999;
@ -610,9 +618,9 @@ UnhideImpl()
return; return;
} }
self SetClientDvar( "sv_cheats", 1 ); self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 ); self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 ); self SetClientDvar( "sv_cheats", 0 );
self.health = self.savedHealth; self.health = self.savedHealth;
self.isHidden = false; self.isHidden = false;
@ -707,3 +715,16 @@ ToggleNightMode()
self SetClientDvar( "fx_draw", fxDraw ); self SetClientDvar( "fx_draw", fxDraw );
self SetClientDvar( "sv_cheats", 0 ); self SetClientDvar( "sv_cheats", 0 );
} }
SetSpectatorImpl()
{
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}

View File

@ -23,7 +23,7 @@ namespace Integrations.Cod
/// </summary> /// </summary>
public class CodRConConnection : IRConConnection public class CodRConConnection : IRConConnection
{ {
static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new ConcurrentDictionary<EndPoint, ConnectionState>(); private static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new();
public IPEndPoint Endpoint { get; } public IPEndPoint Endpoint { get; }
public string RConPassword { get; } public string RConPassword { get; }
@ -48,7 +48,40 @@ namespace Integrations.Cod
config = parser.Configuration; config = parser.Configuration;
} }
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "",
CancellationToken token = default)
{
try
{
return await SendQueryAsyncInternal(type, parameters, token);
}
catch (OperationCanceledException)
{
_log.LogWarning("Timed out waiting for RCon response");
throw new RConException("Did not received RCon response in allocated time frame");
}
finally
{
var state = ActiveQueries[Endpoint];
if (state.OnComplete.CurrentCount == 0)
{
state.OnComplete.Release(1);
state.ConnectionAttempts = 0;
}
if (state.OnReceivedData.CurrentCount == 0)
{
state.OnReceivedData.Release(1);
}
if (state.OnSentData.CurrentCount == 0)
{
state.OnSentData.Release(1);
}
}
}
private async Task<string[]> SendQueryAsyncInternal(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default)
{ {
if (!ActiveQueries.ContainsKey(this.Endpoint)) if (!ActiveQueries.ContainsKey(this.Endpoint))
{ {
@ -57,36 +90,36 @@ namespace Integrations.Cod
var connectionState = ActiveQueries[this.Endpoint]; var connectionState = ActiveQueries[this.Endpoint];
_log.LogDebug("Waiting for semaphore to be released [{endpoint}]", Endpoint); _log.LogDebug("Waiting for semaphore to be released [{Endpoint}]", Endpoint);
// enter the semaphore so only one query is sent at a time per server. // enter the semaphore so only one query is sent at a time per server.
await connectionState.OnComplete.WaitAsync(); await connectionState.OnComplete.WaitAsync(token);
var timeSinceLastQuery = (DateTime.Now - connectionState.LastQuery).TotalMilliseconds; var timeSinceLastQuery = (DateTime.Now - connectionState.LastQuery).TotalMilliseconds;
if (timeSinceLastQuery < config.FloodProtectInterval) if (timeSinceLastQuery < config.FloodProtectInterval)
{ {
await Task.Delay(config.FloodProtectInterval - (int)timeSinceLastQuery); await Task.Delay(config.FloodProtectInterval - (int)timeSinceLastQuery, token);
} }
connectionState.LastQuery = DateTime.Now; connectionState.LastQuery = DateTime.Now;
_log.LogDebug("Semaphore has been released [{endpoint}]", Endpoint); _log.LogDebug("Semaphore has been released [{Endpoint}]", Endpoint);
_log.LogDebug("Query {@queryInfo}", new { endpoint=Endpoint.ToString(), type, parameters }); _log.LogDebug("Query {@QueryInfo}", new { endpoint=Endpoint.ToString(), type, parameters });
byte[] payload = null; byte[] payload = null;
bool waitForResponse = config.WaitForResponse; var waitForResponse = config.WaitForResponse;
string convertEncoding(string text) string ConvertEncoding(string text)
{ {
byte[] convertedBytes = Utilities.EncodingType.GetBytes(text); var convertedBytes = Utilities.EncodingType.GetBytes(text);
return _gameEncoding.GetString(convertedBytes); return _gameEncoding.GetString(convertedBytes);
} }
try try
{ {
string convertedRConPassword = convertEncoding(RConPassword); var convertedRConPassword = ConvertEncoding(RConPassword);
string convertedParameters = convertEncoding(parameters); var convertedParameters = ConvertEncoding(parameters);
switch (type) switch (type)
{ {
@ -137,7 +170,7 @@ namespace Integrations.Cod
using (LogContext.PushProperty("Server", Endpoint.ToString())) using (LogContext.PushProperty("Server", Endpoint.ToString()))
{ {
_log.LogInformation( _log.LogInformation(
"Retrying RCon message ({connectionAttempts}/{allowedConnectionFailures} attempts) with parameters {payload}", "Retrying RCon message ({ConnectionAttempts}/{AllowedConnectionFailures} attempts) with parameters {payload}",
connectionState.ConnectionAttempts, connectionState.ConnectionAttempts,
_retryAttempts, parameters); _retryAttempts, parameters);
} }
@ -151,20 +184,27 @@ namespace Integrations.Cod
{ {
connectionState.SendEventArgs.UserToken = socket; connectionState.SendEventArgs.UserToken = socket;
connectionState.ConnectionAttempts++; connectionState.ConnectionAttempts++;
await connectionState.OnSentData.WaitAsync(); await connectionState.OnSentData.WaitAsync(token);
await connectionState.OnReceivedData.WaitAsync(); await connectionState.OnReceivedData.WaitAsync(token);
connectionState.BytesReadPerSegment.Clear(); connectionState.BytesReadPerSegment.Clear();
bool exceptionCaught = false; var exceptionCaught = false;
_log.LogDebug("Sending {payloadLength} bytes to [{endpoint}] ({connectionAttempts}/{allowedConnectionFailures})", _log.LogDebug("Sending {PayloadLength} bytes to [{Endpoint}] ({ConnectionAttempts}/{AllowedConnectionFailures})",
payload.Length, Endpoint, connectionState.ConnectionAttempts, _retryAttempts); payload.Length, Endpoint, connectionState.ConnectionAttempts, _retryAttempts);
try try
{ {
try
response = await SendPayloadAsync(payload, waitForResponse, parser.OverrideTimeoutForCommand(parameters)); {
response = await SendPayloadAsync(payload, waitForResponse,
if ((response.Length == 0 || response[0].Length == 0) && waitForResponse) parser.OverrideTimeoutForCommand(parameters), token);
}
catch (OperationCanceledException)
{
// ignored
}
if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse)
{ {
throw new RConException("Expected response but got 0 bytes back"); throw new RConException("Expected response but got 0 bytes back");
} }
@ -178,14 +218,14 @@ namespace Integrations.Cod
if (connectionState.ConnectionAttempts < _retryAttempts) if (connectionState.ConnectionAttempts < _retryAttempts)
{ {
exceptionCaught = true; exceptionCaught = true;
await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts)); await Task.Delay(StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts), token);
goto retrySend; goto retrySend;
} }
using (LogContext.PushProperty("Server", Endpoint.ToString())) using (LogContext.PushProperty("Server", Endpoint.ToString()))
{ {
_log.LogWarning( _log.LogWarning(
"Made {connectionAttempts} attempts to send RCon data to server, but received no response", "Made {ConnectionAttempts} attempts to send RCon data to server, but received no response",
connectionState.ConnectionAttempts); connectionState.ConnectionAttempts);
} }
connectionState.ConnectionAttempts = 0; connectionState.ConnectionAttempts = 0;
@ -214,14 +254,15 @@ namespace Integrations.Cod
if (response.Length == 0) if (response.Length == 0)
{ {
_log.LogDebug("Received empty response for RCon request {@query}", new { endpoint=Endpoint.ToString(), type, parameters }); _log.LogDebug("Received empty response for RCon request {@Query}",
return new string[0]; new { endpoint = Endpoint.ToString(), type, parameters });
return Array.Empty<string>();
} }
string responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ? var responseString = type == StaticHelpers.QueryType.COMMAND_STATUS ?
ReassembleSegmentedStatus(response) : RecombineMessages(response); ReassembleSegmentedStatus(response) : RecombineMessages(response);
// note: not all games respond if the pasword is wrong or not set // note: not all games respond if the password is wrong or not set
if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword")) if (responseString.Contains("Invalid password") || responseString.Contains("rconpassword"))
{ {
throw new RConException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]); throw new RConException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_RCON_INVALID"]);
@ -237,21 +278,21 @@ namespace Integrations.Cod
throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString())); throw new ServerException(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_NOT_RUNNING"].FormatExt(Endpoint.ToString()));
} }
string responseHeaderMatch = Regex.Match(responseString, config.CommandPrefixes.RConResponse).Value; var responseHeaderMatch = Regex.Match(responseString, config.CommandPrefixes.RConResponse).Value;
string[] headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : responseHeaderMatch); var headerSplit = responseString.Split(type == StaticHelpers.QueryType.GET_INFO ? config.CommandPrefixes.RconGetInfoResponseHeader : responseHeaderMatch);
if (headerSplit.Length != 2) if (headerSplit.Length != 2)
{ {
using (LogContext.PushProperty("Server", Endpoint.ToString())) using (LogContext.PushProperty("Server", Endpoint.ToString()))
{ {
_log.LogWarning("Invalid response header from server. Expected {expected}, but got {response}", _log.LogWarning("Invalid response header from server. Expected {Expected}, but got {Response}",
config.CommandPrefixes.RConResponse, headerSplit.FirstOrDefault()); config.CommandPrefixes.RConResponse, headerSplit.FirstOrDefault());
} }
throw new RConException("Unexpected response header from server"); throw new RConException("Unexpected response header from server");
} }
string[] splitResponse = headerSplit.Last().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); var splitResponse = headerSplit.Last().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
return splitResponse; return splitResponse;
} }
@ -312,7 +353,7 @@ namespace Integrations.Cod
} }
} }
private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse, TimeSpan overrideTimeout) private async Task<byte[][]> SendPayloadAsync(byte[] payload, bool waitForResponse, TimeSpan overrideTimeout, CancellationToken token = default)
{ {
var connectionState = ActiveQueries[this.Endpoint]; var connectionState = ActiveQueries[this.Endpoint];
var rconSocket = (Socket)connectionState.SendEventArgs.UserToken; var rconSocket = (Socket)connectionState.SendEventArgs.UserToken;
@ -332,18 +373,27 @@ namespace Integrations.Cod
connectionState.SendEventArgs.SetBuffer(payload); connectionState.SendEventArgs.SetBuffer(payload);
// send the data to the server // send the data to the server
bool sendDataPending = rconSocket.SendToAsync(connectionState.SendEventArgs); var sendDataPending = rconSocket.SendToAsync(connectionState.SendEventArgs);
if (sendDataPending) if (sendDataPending)
{ {
// the send has not been completed asynchronously // the send has not been completed asynchronously
// this really shouldn't ever happen because it's UDP // this really shouldn't ever happen because it's UDP
var complete = false;
try
{
complete = await connectionState.OnSentData.WaitAsync(StaticHelpers.SocketTimeout(1), token);
}
catch (OperationCanceledException)
{
// ignored
}
if(!await connectionState.OnSentData.WaitAsync(StaticHelpers.SocketTimeout(1))) if(!complete)
{ {
using(LogContext.PushProperty("Server", Endpoint.ToString())) using(LogContext.PushProperty("Server", Endpoint.ToString()))
{ {
_log.LogWarning("Socket timed out while sending RCon data on attempt {attempt}", _log.LogWarning("Socket timed out while sending RCon data on attempt {Attempt}",
connectionState.ConnectionAttempts); connectionState.ConnectionAttempts);
} }
rconSocket.Close(); rconSocket.Close();
@ -359,17 +409,29 @@ namespace Integrations.Cod
connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer); connectionState.ReceiveEventArgs.SetBuffer(connectionState.ReceiveBuffer);
// get our response back // get our response back
bool receiveDataPending = rconSocket.ReceiveFromAsync(connectionState.ReceiveEventArgs); var receiveDataPending = rconSocket.ReceiveFromAsync(connectionState.ReceiveEventArgs);
if (receiveDataPending) if (receiveDataPending)
{ {
_log.LogDebug("Waiting to asynchronously receive data on attempt #{connectionAttempts}", connectionState.ConnectionAttempts); _log.LogDebug("Waiting to asynchronously receive data on attempt #{ConnectionAttempts}", connectionState.ConnectionAttempts);
if (!await connectionState.OnReceivedData.WaitAsync(
new[] var completed = false;
{
StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts), try
overrideTimeout {
}.Max())) completed = await connectionState.OnReceivedData.WaitAsync(
new[]
{
StaticHelpers.SocketTimeout(connectionState.ConnectionAttempts),
overrideTimeout
}.Max(), token);
}
catch (OperationCanceledException)
{
// ignored
}
if (!completed)
{ {
if (connectionState.ConnectionAttempts > 1) // this reduces some spam for unstable connections if (connectionState.ConnectionAttempts > 1) // this reduces some spam for unstable connections
{ {
@ -388,16 +450,15 @@ namespace Integrations.Cod
} }
rconSocket.Close(); rconSocket.Close();
return GetResponseData(connectionState); return GetResponseData(connectionState);
} }
private byte[][] GetResponseData(ConnectionState connectionState) private byte[][] GetResponseData(ConnectionState connectionState)
{ {
var responseList = new List<byte[]>(); var responseList = new List<byte[]>();
int totalBytesRead = 0; var totalBytesRead = 0;
foreach (int bytesRead in connectionState.BytesReadPerSegment) foreach (var bytesRead in connectionState.BytesReadPerSegment)
{ {
responseList.Add(connectionState.ReceiveBuffer responseList.Add(connectionState.ReceiveBuffer
.Skip(totalBytesRead) .Skip(totalBytesRead)

View File

@ -48,12 +48,12 @@ namespace Integrations.Source
_activeQuery.Dispose(); _activeQuery.Dispose();
} }
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "") public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default)
{ {
try try
{ {
await _activeQuery.WaitAsync(); await _activeQuery.WaitAsync(token);
await WaitForAvailable(); await WaitForAvailable(token);
if (_needNewSocket) if (_needNewSocket)
{ {
@ -66,7 +66,7 @@ namespace Integrations.Source
// ignored // ignored
} }
await Task.Delay(ConnectionTimeout); await Task.Delay(ConnectionTimeout, token);
_rconClient = _rconClientFactory.CreateClient(_ipEndPoint); _rconClient = _rconClientFactory.CreateClient(_ipEndPoint);
_authenticated = false; _authenticated = false;
_needNewSocket = false; _needNewSocket = false;
@ -147,12 +147,12 @@ namespace Integrations.Source
} }
} }
private async Task WaitForAvailable() private async Task WaitForAvailable(CancellationToken token)
{ {
var diff = DateTime.Now - _lastQuery; var diff = DateTime.Now - _lastQuery;
if (diff < FloodDelay) if (diff < FloodDelay)
{ {
await Task.Delay(FloodDelay - diff); await Task.Delay(FloodDelay - diff, token);
} }
} }

View File

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" /> <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -23,7 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,7 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -271,6 +271,24 @@ let commands = [{
} }
sendScriptCommand(gameEvent.Owner, 'NightMode', gameEvent.Origin, undefined, undefined); sendScriptCommand(gameEvent.Owner, 'NightMode', gameEvent.Origin, undefined, undefined);
} }
},
{
name: 'setspectator',
description: 'sets a player as spectator',
alias: 'spec',
permission: 'Administrator',
targetRequired: true,
arguments: [{
name: 'player',
required: true
}],
supportedGames: ['IW4'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
}
sendScriptCommand(gameEvent.Owner, 'SetSpectator', gameEvent.Origin, gameEvent.Target, undefined);
}
}]; }];
const sendScriptCommand = (server, command, origin, target, data) => { const sendScriptCommand = (server, command, origin, target, data) => {
@ -289,6 +307,11 @@ const sendEvent = (server, responseExpected, event, subtype, origin, target, dat
const start = new Date(); const start = new Date();
while (pendingOut && pendingCheckCount <= 10) { while (pendingOut && pendingCheckCount <= 10) {
if (server.Throttled) {
logger.WriteWarning('Server is throttled, so we are not attempting to send data');
return;
}
try { try {
const out = server.GetServerDvar(outDvar); const out = server.GetServerDvar(outDvar);
pendingOut = !(out == null || out === '' || out === 'null'); pendingOut = !(out == null || out === '' || out === 'null');
@ -300,6 +323,7 @@ const sendEvent = (server, responseExpected, event, subtype, origin, target, dat
logger.WriteDebug('Waiting for event bus to be cleared'); logger.WriteDebug('Waiting for event bus to be cleared');
System.Threading.Tasks.Task.Delay(1000).Wait(); System.Threading.Tasks.Task.Delay(1000).Wait();
} }
pendingCheckCount++; pendingCheckCount++;
} }
@ -379,6 +403,10 @@ const initialize = (server) => {
}; };
const pollForEvents = server => { const pollForEvents = server => {
if (server.Throttled) {
return;
}
const logger = _serviceResolver.ResolveService('ILogger'); const logger = _serviceResolver.ResolveService('ILogger');
let input; let input;

View File

@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -20,7 +20,7 @@
</Target> </Target>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.22.1" PrivateAssets="All" /> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.2.28.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1190,7 +1190,7 @@ namespace SharedLibraryCore.Commands
public static async Task<string> GetNextMap(Server s, ITranslationLookup lookup) public static async Task<string> GetNextMap(Server s, ITranslationLookup lookup)
{ {
var mapRotation = (await s.GetDvarAsync<string>("sv_mapRotation")).Value?.ToLower() ?? ""; var mapRotation = (await s.GetDvarAsync<string>("sv_mapRotation", token: s.Manager.CancellationToken)).Value?.ToLower() ?? "";
var regexMatches = Regex.Matches(mapRotation, var regexMatches = Regex.Matches(mapRotation,
@"((?:gametype|exec) +(?:([a-z]{1,4})(?:.cfg)?))? *map ([a-z|_|\d]+)", RegexOptions.IgnoreCase) @"((?:gametype|exec) +(?:([a-z]{1,4})(?:.cfg)?))? *map ([a-z|_|\d]+)", RegexOptions.IgnoreCase)
.ToList(); .ToList();

View File

@ -20,6 +20,7 @@ namespace SharedLibraryCore.Interfaces
public enum MetaType public enum MetaType
{ {
All = -1,
Other, Other,
Information, Information,
AliasUpdate, AliasUpdate,

View File

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading;
using System.Threading.Tasks;
using SharedLibraryCore.RCon; using SharedLibraryCore.RCon;
namespace SharedLibraryCore.Interfaces namespace SharedLibraryCore.Interfaces
@ -14,7 +15,7 @@ namespace SharedLibraryCore.Interfaces
/// <param name="type">type of RCon query to perform</param> /// <param name="type">type of RCon query to perform</param>
/// <param name="parameters">optional parameter list</param> /// <param name="parameters">optional parameter list</param>
/// <returns></returns> /// <returns></returns>
Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = ""); Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", CancellationToken token = default);
/// <summary> /// <summary>
/// sets the rcon parser /// sets the rcon parser
@ -22,4 +23,4 @@ namespace SharedLibraryCore.Interfaces
/// <param name="config">parser</param> /// <param name="config">parser</param>
void SetConfiguration(IRConParser config); void SetConfiguration(IRConParser config);
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
@ -52,7 +53,7 @@ namespace SharedLibraryCore.Interfaces
/// <param name="dvarName">name of DVAR</param> /// <param name="dvarName">name of DVAR</param>
/// <param name="fallbackValue">default value to return if dvar retrieval fails</param> /// <param name="fallbackValue">default value to return if dvar retrieval fails</param>
/// <returns></returns> /// <returns></returns>
Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default); Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default);
/// <summary> /// <summary>
/// set value of DVAR by name /// set value of DVAR by name
@ -61,7 +62,7 @@ namespace SharedLibraryCore.Interfaces
/// <param name="dvarName">name of DVAR to set</param> /// <param name="dvarName">name of DVAR to set</param>
/// <param name="dvarValue">value to set DVAR to</param> /// <param name="dvarValue">value to set DVAR to</param>
/// <returns></returns> /// <returns></returns>
Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue); Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default);
/// <summary> /// <summary>
/// executes a console command on the server /// executes a console command on the server
@ -69,8 +70,8 @@ namespace SharedLibraryCore.Interfaces
/// <param name="connection">RCon connection to use</param> /// <param name="connection">RCon connection to use</param>
/// <param name="command">console command to execute</param> /// <param name="command">console command to execute</param>
/// <returns></returns> /// <returns></returns>
Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command); Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default);
/// <summary> /// <summary>
/// get the list of connected clients from status response /// get the list of connected clients from status response
/// </summary> /// </summary>
@ -78,7 +79,7 @@ namespace SharedLibraryCore.Interfaces
/// <returns> /// <returns>
/// <see cref="IStatusResponse" /> /// <see cref="IStatusResponse" />
/// </returns> /// </returns>
Task<IStatusResponse> GetStatusAsync(IRConConnection connection); Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default);
/// <summary> /// <summary>
/// retrieves the value of given dvar key if it exists in the override dict /// retrieves the value of given dvar key if it exists in the override dict
@ -103,4 +104,4 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns> /// <returns></returns>
TimeSpan OverrideTimeoutForCommand(string command); TimeSpan OverrideTimeoutForCommand(string command);
} }
} }

View File

@ -377,7 +377,7 @@ namespace SharedLibraryCore
{ {
try try
{ {
return (await this.GetDvarAsync("sv_customcallbacks", "0")).Value == "1"; return (await this.GetDvarAsync("sv_customcallbacks", "0", Manager.CancellationToken)).Value == "1";
} }
catch (DvarException) catch (DvarException)
@ -391,11 +391,11 @@ namespace SharedLibraryCore
public string[] ExecuteServerCommand(string command) public string[] ExecuteServerCommand(string command)
{ {
var tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromMilliseconds(400)); tokenSource.CancelAfter(TimeSpan.FromSeconds(0.5));
try try
{ {
return this.ExecuteCommandAsync(command).WithWaitCancellation(tokenSource.Token).GetAwaiter().GetResult(); return this.ExecuteCommandAsync(command, tokenSource.Token).GetAwaiter().GetResult();
} }
catch catch
{ {
@ -406,11 +406,10 @@ namespace SharedLibraryCore
public string GetServerDvar(string dvarName) public string GetServerDvar(string dvarName)
{ {
var tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromMilliseconds(400)); tokenSource.CancelAfter(TimeSpan.FromSeconds(0.5));
try try
{ {
return this.GetDvarAsync<string>(dvarName).WithWaitCancellation(tokenSource.Token).GetAwaiter() return this.GetDvarAsync<string>(dvarName, token: tokenSource.Token).GetAwaiter().GetResult().Value;
.GetResult()?.Value;
} }
catch catch
{ {
@ -421,12 +420,11 @@ namespace SharedLibraryCore
public bool SetServerDvar(string dvarName, string dvarValue) public bool SetServerDvar(string dvarName, string dvarValue)
{ {
var tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromMilliseconds(400)); tokenSource.CancelAfter(TimeSpan.FromSeconds(0.5));
try try
{ {
this.SetDvarAsync(dvarName, dvarValue).WithWaitCancellation(tokenSource.Token).GetAwaiter().GetResult(); this.SetDvarAsync(dvarName, dvarValue, tokenSource.Token).GetAwaiter().GetResult();
return true; return true;
} }
catch catch
{ {

View File

@ -597,51 +597,16 @@ namespace SharedLibraryCore.Services
/// <returns></returns> /// <returns></returns>
public virtual async Task UpdateLevel(Permission newPermission, EFClient temporalClient, EFClient origin) public virtual async Task UpdateLevel(Permission newPermission, EFClient temporalClient, EFClient origin)
{ {
await using var ctx = _contextFactory.CreateContext(); await using var context = _contextFactory.CreateContext();
var entity = await ctx.Clients var entity = await context.Clients
.Where(_client => _client.ClientId == temporalClient.ClientId) .Where(client => client.ClientId == temporalClient.ClientId)
.FirstAsync(); .FirstAsync();
var oldPermission = entity.Level; _logger.LogInformation("Updating {ClientId} from {OldPermission} to {NewPermission} ",
temporalClient.ClientId, entity.Level, newPermission);
entity.Level = newPermission; entity.Level = newPermission;
await ctx.SaveChangesAsync(); await context.SaveChangesAsync();
using (LogContext.PushProperty("Server", temporalClient?.CurrentServer?.ToString()))
{
_logger.LogInformation("Updated {clientId} to {newPermission}", temporalClient.ClientId, newPermission);
var linkedPermissionSet = new[] { Permission.Banned, Permission.Flagged };
// if their permission level has been changed to level that needs to be updated on all accounts
if (linkedPermissionSet.Contains(newPermission) || linkedPermissionSet.Contains(oldPermission))
{
//get all clients that have the same linkId
var iqMatchingClients = ctx.Clients
.Where(_client => _client.AliasLinkId == entity.AliasLinkId);
var iqLinkClients = new List<Data.Models.Client.EFClient>().AsQueryable();
if (!_appConfig.EnableImplicitAccountLinking)
{
var linkIds = await ctx.Aliases.Where(alias =>
alias.IPAddress != null && alias.IPAddress == temporalClient.IPAddress)
.Select(alias => alias.LinkId)
.ToListAsync();
iqLinkClients = ctx.Clients.Where(client => linkIds.Contains(client.AliasLinkId));
}
// this updates the level for all the clients with the same LinkId
// only if their new level is flagged or banned
await iqMatchingClients.Union(iqLinkClients).ForEachAsync(_client =>
{
_client.Level = newPermission;
_logger.LogInformation("Updated linked {clientId} to {newPermission}", _client.ClientId,
newPermission);
});
await ctx.SaveChangesAsync();
}
}
temporalClient.Level = newPermission; temporalClient.Level = newPermission;
} }

View File

@ -116,7 +116,7 @@ namespace SharedLibraryCore.Services
} }
private static readonly EFPenalty.PenaltyType[] LinkedPenalties = private static readonly EFPenalty.PenaltyType[] LinkedPenalties =
{ EFPenalty.PenaltyType.Ban, EFPenalty.PenaltyType.Flag }; { EFPenalty.PenaltyType.Ban, EFPenalty.PenaltyType.Flag, EFPenalty.PenaltyType.TempBan };
private static readonly Expression<Func<EFPenalty, bool>> Filter = p => private static readonly Expression<Func<EFPenalty, bool>> Filter = p =>
LinkedPenalties.Contains(p.Type) && p.Active && (p.Expires == null || p.Expires > DateTime.UtcNow); LinkedPenalties.Contains(p.Type) && p.Active && (p.Expires == null || p.Expires > DateTime.UtcNow);

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId> <PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2022.2.22.1</Version> <Version>2022.2.28.1</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description> <Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2022.2.22.1</PackageVersion> <PackageVersion>2022.2.28.1</PackageVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">

View File

@ -723,14 +723,21 @@ namespace SharedLibraryCore
.Replace('/', '_'); .Replace('/', '_');
} }
public static Task<Dvar<T>> GetDvarAsync<T>(this Server server, string dvarName, T fallbackValue = default) public static async Task<Dvar<T>> GetDvarAsync<T>(this Server server, string dvarName,
T fallbackValue = default, CancellationToken token = default)
{ {
return server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue); return await server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue, token);
}
public static async Task<Dvar<T>> GetDvarAsync<T>(this Server server, string dvarName,
T fallbackValue = default)
{
return await GetDvarAsync(server, dvarName, fallbackValue, default);
} }
public static async Task<Dvar<T>> GetMappedDvarValueOrDefaultAsync<T>(this Server server, string dvarName, public static async Task<Dvar<T>> GetMappedDvarValueOrDefaultAsync<T>(this Server server, string dvarName,
string infoResponseName = null, IDictionary<string, string> infoResponse = null, string infoResponseName = null, IDictionary<string, string> infoResponse = null,
T overrideDefault = default) T overrideDefault = default, CancellationToken token = default)
{ {
// todo: unit test this // todo: unit test this
var mappedKey = server.RconParser.GetOverrideDvarName(dvarName); var mappedKey = server.RconParser.GetOverrideDvarName(dvarName);
@ -749,22 +756,32 @@ namespace SharedLibraryCore
}; };
} }
return await server.GetDvarAsync(mappedKey, defaultValue); return await server.GetDvarAsync(mappedKey, defaultValue, token: token);
} }
public static Task SetDvarAsync(this Server server, string dvarName, object dvarValue) public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue, CancellationToken token = default)
{ {
return server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue); await server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue, token);
}
public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue)
{
await SetDvarAsync(server, dvarName, dvarValue, default);
} }
public static async Task<string[]> ExecuteCommandAsync(this Server server, string commandName, CancellationToken token = default)
{
return await server.RconParser.ExecuteCommandAsync(server.RemoteConnection, commandName, token);
}
public static async Task<string[]> ExecuteCommandAsync(this Server server, string commandName) public static async Task<string[]> ExecuteCommandAsync(this Server server, string commandName)
{ {
return await server.RconParser.ExecuteCommandAsync(server.RemoteConnection, commandName); return await ExecuteCommandAsync(server, commandName, default);
} }
public static Task<IStatusResponse> GetStatusAsync(this Server server) public static Task<IStatusResponse> GetStatusAsync(this Server server, CancellationToken token)
{ {
return server.RconParser.GetStatusAsync(server.RemoteConnection); return server.RconParser.GetStatusAsync(server.RemoteConnection, token);
} }
/// <summary> /// <summary>

View File

@ -53,6 +53,14 @@ namespace WebfrontCore.Controllers
client.SetAdditionalProperty(EFMeta.ClientTag, tag.LinkedMeta.Value); client.SetAdditionalProperty(EFMeta.ClientTag, tag.LinkedMeta.Value);
} }
// even though we haven't set their level to "banned" yet
// (ie they haven't reconnected with the infringing player identifier)
// we want to show them as banned as to not confuse people.
if (activePenalties.Any(penalty => penalty.Type == EFPenalty.PenaltyType.Ban))
{
client.Level = Data.Models.Client.EFClient.Permission.Banned;
}
var displayLevelInt = (int)client.Level; var displayLevelInt = (int)client.Level;
var displayLevel = client.Level.ToLocalizedLevelName(); var displayLevel = client.Level.ToLocalizedLevelName();

View File

@ -38,11 +38,12 @@ namespace WebfrontCore.ViewComponents
return View("_List", meta); return View("_List", meta);
} }
public static async Task<IEnumerable<IClientMeta>> GetClientMeta(IMetaService metaService, MetaType? metaType, EFClient.Permission level, ClientPaginationRequest request) public static async Task<IEnumerable<IClientMeta>> GetClientMeta(IMetaService metaService, MetaType? metaType,
EFClient.Permission level, ClientPaginationRequest request)
{ {
IEnumerable<IClientMeta> meta = null; IEnumerable<IClientMeta> meta = null;
if (metaType == null) // all types if (metaType is null or MetaType.All)
{ {
meta = await metaService.GetRuntimeMeta(request); meta = await metaService.GetRuntimeMeta(request);
} }

View File

@ -146,23 +146,23 @@
<partial name="Meta/_Information.cshtml" model="@Model.Meta"/> <partial name="Meta/_Information.cshtml" model="@Model.Meta"/>
</div> </div>
<div class="row border-bottom bg-dark"> <div class="row border-bottom">
<div class="d-md-flex flex-fill align-items-center bg-dark"> <div class="d-md-flex flex-fill">
<div class="text-center bg-dark p-2 pl-3 pr-4 text-muted" id="filter_meta_container_button"> <div class="bg-dark p-2 pl-3 pr-3 text-center text-muted border-0 align-self-stretch align-middle" id="filter_meta_container_button">
<span class="oi oi-sort-ascending"></span> <span class="text-primary" id="meta_filter_dropdown_icon"></span>
<a>@ViewBag.Localization["WEBFRONT_CLIENT_META_FILTER"]</a> <a>@ViewBag.Localization["WEBFRONT_CLIENT_META_FILTER"]</a>
</div> </div>
<div id="filter_meta_container" class="d-none d-md-flex flex-md-fill flex-md-wrap"> <div id="filter_meta_container" class="d-none d-md-flex flex-md-fill flex-md-wrap">
<a asp-action="ProfileAsync" asp-controller="Client" @{
class="nav-link p-2 pl-3 pr-3 text-center col-12 col-md-auto text-md-left @(!Model.MetaFilterType.HasValue ? "btn-primary text-white" : "text-muted")" const int defaultTabCount = 5;
asp-route-id="@Model.ClientId"> var metaTypes = Enum.GetValues(typeof(MetaType))
@ViewBag.Localization["META_TYPE_ALL_NAME"]
</a>
@{ var metaTypes = Enum.GetValues(typeof(MetaType))
.Cast<MetaType>() .Cast<MetaType>()
.Where(type => !ignoredMetaTypes.Contains(type)) .Where(type => !ignoredMetaTypes.Contains(type))
.ToList(); } .OrderByDescending(type => type == MetaType.All)
@foreach (var type in metaTypes.Take(4)) .ToList();
var selectedMeta = metaTypes.FirstOrDefault(meta => metaTypes.IndexOf(Model.MetaFilterType ?? MetaType.All) >= defaultTabCount && meta != MetaType.All && meta == Model.MetaFilterType);
}
@foreach (var type in metaTypes.Take(defaultTabCount - 1).Append(selectedMeta == MetaType.Other ? metaTypes[defaultTabCount - 1] : selectedMeta))
{ {
<a asp-action="ProfileAsync" asp-controller="Client" <a asp-action="ProfileAsync" asp-controller="Client"
class="meta-filter nav-link p-2 pl-3 pr-3 text-center @(Model.MetaFilterType.HasValue && Model.MetaFilterType.Value.ToString() == type.ToString() ? "btn-primary text-white" : "text-muted")" class="meta-filter nav-link p-2 pl-3 pr-3 text-center @(Model.MetaFilterType.HasValue && Model.MetaFilterType.Value.ToString() == type.ToString() ? "btn-primary text-white" : "text-muted")"
@ -172,9 +172,8 @@
@type.ToTranslatedName() @type.ToTranslatedName()
</a> </a>
} }
<a href="#" class="nav-link p-2 pl-3 pr-3 text-center text-muted d-none d-md-block" id="expand-meta-filters">...</a> <div class="d-md-none" id="additional_meta_filter">
<div class="d-block d-md-none" id="additional-meta-filter"> @foreach (var type in (selectedMeta == MetaType.Other ? metaTypes.Skip(defaultTabCount) : metaTypes.Skip(defaultTabCount).Append(metaTypes[defaultTabCount - 1])).Where(meta => selectedMeta == MetaType.Other || meta != selectedMeta))
@foreach (var type in metaTypes.Skip(4))
{ {
<a asp-action="ProfileAsync" asp-controller="Client" <a asp-action="ProfileAsync" asp-controller="Client"
class="meta-filter nav-link p-2 pl-3 pr-3 text-center @(Model.MetaFilterType.HasValue && Model.MetaFilterType.Value.ToString() == type.ToString() ? "btn-primary text-white" : "text-muted")" class="meta-filter nav-link p-2 pl-3 pr-3 text-center @(Model.MetaFilterType.HasValue && Model.MetaFilterType.Value.ToString() == type.ToString() ? "btn-primary text-white" : "text-muted")"

View File

@ -35,15 +35,9 @@
startAt = $('.loader-data-time').last().data('time'); startAt = $('.loader-data-time').last().data('time');
$('#filter_meta_container_button').click(function () { $('#filter_meta_container_button').click(function () {
$('#filter_meta_container').hide().removeClass('d-none').addClass('d-block').slideDown(); $('#filter_meta_container').removeClass('d-none').addClass('flex-md-column');
$('#additional-meta-filter').removeClass('d-md-none').addClass('d-flex').slideDown(); $('#additional_meta_filter').removeClass('d-md-none');
$('#expand-meta-filters').removeClass('d-md-block');
}); });
$('#expand-meta-filters').click(function () {
$('#additional-meta-filter').removeClass('d-md-none').addClass('d-flex').slideDown();
$('#expand-meta-filters').removeClass('d-md-block');
})
/* /*
* load context of chat * load context of chat

View File

@ -15,6 +15,8 @@ $(document).ready(() => {
return 0; return 0;
} }
setInterval(refreshScoreboard, 5000);
$(window.location.hash).tab('show'); $(window.location.hash).tab('show');
$(`${window.location.hash}_nav`).addClass('active'); $(`${window.location.hash}_nav`).addClass('active');
@ -32,5 +34,3 @@ function setupDataSorting() {
refreshScoreboard(); refreshScoreboard();
}) })
} }
setInterval(refreshScoreboard, 5000);