Compare commits
74 Commits
2022.06.03
...
2022.07.23
Author | SHA1 | Date | |
---|---|---|---|
444c06e65e | |||
561909158f | |||
cd12c3f26e | |||
c817f9a810 | |||
b27ae1517e | |||
507688a175 | |||
d2cfd50e39 | |||
51e8b31e42 | |||
fa1567d3f5 | |||
f97e266c24 | |||
506b17dbb3 | |||
bef8c08d90 | |||
b78c467539 | |||
c3e042521a | |||
cb5f490d3b | |||
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 | |||
a92f9fc29c | |||
fbf424c77d | |||
b8e001fcfe | |||
5ab5b73ecf | |||
4534d24fe6 | |||
73c8d0da33 | |||
16d75470b5 | |||
f02552faa1 | |||
a4923d03f9 | |||
8ae6561f4e | |||
deeb1dea87 | |||
9ab34614c5 | |||
2cff25d6b3 | |||
df3e226dc9 | |||
ef3db63ba7 | |||
49fe4520ff | |||
6587187a34 | |||
b337e232a2 | |||
a44b4e9475 | |||
ffb0e5cac1 | |||
ecc2b5bf54 | |||
2ac9cc4379 | |||
215037095f | |||
5433d7d1d2 | |||
0446fe1ec5 | |||
cf2a00e5b3 | |||
ab494a22cb | |||
b690579154 | |||
acc967e50a | |||
c493fbe13d | |||
ee56a5db1f |
55
Application/Alerts/AlertExtensions.cs
Normal file
55
Application/Alerts/AlertExtensions.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Alerts;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
|
||||
namespace IW4MAdmin.Application.Alerts;
|
||||
|
||||
public static class AlertExtensions
|
||||
{
|
||||
public static Alert.AlertState BuildAlert(this EFClient client, Alert.AlertCategory? type = null)
|
||||
{
|
||||
return new Alert.AlertState
|
||||
{
|
||||
RecipientId = client.ClientId,
|
||||
Category = type ?? Alert.AlertCategory.Information
|
||||
};
|
||||
}
|
||||
|
||||
public static Alert.AlertState WithCategory(this Alert.AlertState state, Alert.AlertCategory category)
|
||||
{
|
||||
state.Category = category;
|
||||
return state;
|
||||
}
|
||||
|
||||
public static Alert.AlertState OfType(this Alert.AlertState state, string type)
|
||||
{
|
||||
state.Type = type;
|
||||
return state;
|
||||
}
|
||||
|
||||
public static Alert.AlertState WithMessage(this Alert.AlertState state, string message)
|
||||
{
|
||||
state.Message = message;
|
||||
return state;
|
||||
}
|
||||
|
||||
public static Alert.AlertState ExpiresIn(this Alert.AlertState state, TimeSpan expiration)
|
||||
{
|
||||
state.ExpiresAt = DateTime.Now.Add(expiration);
|
||||
return state;
|
||||
}
|
||||
|
||||
public static Alert.AlertState FromSource(this Alert.AlertState state, string source)
|
||||
{
|
||||
state.Source = source;
|
||||
return state;
|
||||
}
|
||||
|
||||
public static Alert.AlertState FromClient(this Alert.AlertState state, EFClient client)
|
||||
{
|
||||
state.Source = client.Name.StripColors();
|
||||
state.SourceId = client.ClientId;
|
||||
return state;
|
||||
}
|
||||
}
|
137
Application/Alerts/AlertManager.cs
Normal file
137
Application/Alerts/AlertManager.cs
Normal file
@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharedLibraryCore.Alerts;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Application.Alerts;
|
||||
|
||||
public class AlertManager : IAlertManager
|
||||
{
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
private readonly ConcurrentDictionary<int, List<Alert.AlertState>> _states = new();
|
||||
private readonly List<Func<Task<IEnumerable<Alert.AlertState>>>> _staticSources = new();
|
||||
|
||||
public AlertManager(ApplicationConfiguration appConfig)
|
||||
{
|
||||
_appConfig = appConfig;
|
||||
_states.TryAdd(0, new List<Alert.AlertState>());
|
||||
}
|
||||
|
||||
public EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
|
||||
|
||||
public async Task Initialize()
|
||||
{
|
||||
foreach (var source in _staticSources)
|
||||
{
|
||||
var alerts = await source();
|
||||
foreach (var alert in alerts)
|
||||
{
|
||||
AddAlert(alert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client)
|
||||
{
|
||||
lock (_states)
|
||||
{
|
||||
var alerts = Enumerable.Empty<Alert.AlertState>();
|
||||
if (client.Level > Data.Models.Client.EFClient.Permission.Trusted)
|
||||
{
|
||||
alerts = alerts.Concat(_states[0].Where(alert =>
|
||||
alert.MinimumPermission is null || alert.MinimumPermission <= client.Level));
|
||||
}
|
||||
|
||||
if (_states.ContainsKey(client.ClientId))
|
||||
{
|
||||
alerts = alerts.Concat(_states[client.ClientId].AsReadOnly());
|
||||
}
|
||||
|
||||
return alerts.OrderByDescending(alert => alert.OccuredAt);
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkAlertAsRead(Guid alertId)
|
||||
{
|
||||
lock (_states)
|
||||
{
|
||||
foreach (var items in _states.Values)
|
||||
{
|
||||
var matchingEvent = items.FirstOrDefault(item => item.AlertId == alertId);
|
||||
|
||||
if (matchingEvent is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
items.Remove(matchingEvent);
|
||||
OnAlertConsumed?.Invoke(this, matchingEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkAllAlertsAsRead(int recipientId)
|
||||
{
|
||||
lock (_states)
|
||||
{
|
||||
foreach (var items in _states.Values)
|
||||
{
|
||||
items.RemoveAll(item =>
|
||||
{
|
||||
if (item.RecipientId != null && item.RecipientId != recipientId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OnAlertConsumed?.Invoke(this, item);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddAlert(Alert.AlertState alert)
|
||||
{
|
||||
lock (_states)
|
||||
{
|
||||
if (alert.RecipientId is null)
|
||||
{
|
||||
_states[0].Add(alert);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_states.ContainsKey(alert.RecipientId.Value))
|
||||
{
|
||||
_states[alert.RecipientId.Value] = new List<Alert.AlertState>();
|
||||
}
|
||||
|
||||
if (_appConfig.MinimumAlertPermissions.ContainsKey(alert.Type))
|
||||
{
|
||||
alert.MinimumPermission = _appConfig.MinimumAlertPermissions[alert.Type];
|
||||
}
|
||||
|
||||
_states[alert.RecipientId.Value].Add(alert);
|
||||
|
||||
PruneOldAlerts();
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource)
|
||||
{
|
||||
_staticSources.Add(alertSource);
|
||||
}
|
||||
|
||||
|
||||
private void PruneOldAlerts()
|
||||
{
|
||||
foreach (var value in _states.Values)
|
||||
{
|
||||
value.RemoveAll(item => item.ExpiresAt < DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
}
|
@ -57,10 +57,11 @@ namespace IW4MAdmin.Application
|
||||
private readonly List<MessageToken> MessageTokens;
|
||||
private readonly ClientService ClientSvc;
|
||||
readonly PenaltyService PenaltySvc;
|
||||
private readonly IAlertManager _alertManager;
|
||||
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;
|
||||
@ -82,18 +83,19 @@ namespace IW4MAdmin.Application
|
||||
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
|
||||
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
|
||||
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
|
||||
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
|
||||
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
|
||||
{
|
||||
MiddlewareActionHandler = actionHandler;
|
||||
_servers = new ConcurrentBag<Server>();
|
||||
MessageTokens = new List<MessageToken>();
|
||||
ClientSvc = clientService;
|
||||
PenaltySvc = penaltyService;
|
||||
_alertManager = alertManager;
|
||||
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();
|
||||
@ -508,6 +510,7 @@ namespace IW4MAdmin.Application
|
||||
#endregion
|
||||
|
||||
_metaRegistration.Register();
|
||||
await _alertManager.Initialize();
|
||||
|
||||
#region CUSTOM_EVENTS
|
||||
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
|
||||
@ -610,6 +613,7 @@ namespace IW4MAdmin.Application
|
||||
{
|
||||
IsRestartRequested = true;
|
||||
Stop().GetAwaiter().GetResult();
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
@ -629,9 +633,9 @@ namespace IW4MAdmin.Application
|
||||
return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList();
|
||||
}
|
||||
|
||||
public EFClient FindActiveClient(EFClient client) =>client.ClientNumber < 0 ?
|
||||
public EFClient FindActiveClient(EFClient client) => client.ClientNumber < 0 ?
|
||||
GetActiveClients()
|
||||
.FirstOrDefault(c => c.NetworkId == client.NetworkId) ?? client :
|
||||
.FirstOrDefault(c => c.NetworkId == client.NetworkId && c.GameName == client.GameName) ?? client :
|
||||
client;
|
||||
|
||||
public ClientService GetClientService()
|
||||
@ -697,5 +701,6 @@ namespace IW4MAdmin.Application
|
||||
}
|
||||
|
||||
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
|
||||
public IAlertManager AlertManager => _alertManager;
|
||||
}
|
||||
}
|
||||
|
52
Application/Commands/AddClientNoteCommand.cs
Normal file
52
Application/Commands/AddClientNoteCommand.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models.Client;
|
||||
using IW4MAdmin.Application.Meta;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Dtos.Meta.Responses;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Application.Commands;
|
||||
|
||||
public class AddClientNoteCommand : Command
|
||||
{
|
||||
private readonly IMetaServiceV2 _metaService;
|
||||
|
||||
public AddClientNoteCommand(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) : base(config, layout)
|
||||
{
|
||||
Name = "addnote";
|
||||
Description = _translationLookup["COMMANDS_ADD_CLIENT_NOTE_DESCRIPTION"];
|
||||
Alias = "an";
|
||||
Permission = EFClient.Permission.Moderator;
|
||||
RequiresTarget = true;
|
||||
Arguments = new[]
|
||||
{
|
||||
new CommandArgument
|
||||
{
|
||||
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
|
||||
Required = true
|
||||
},
|
||||
new CommandArgument
|
||||
{
|
||||
Name = _translationLookup["COMMANDS_ARGS_NOTE"],
|
||||
Required = false
|
||||
}
|
||||
};
|
||||
|
||||
_metaService = metaService;
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||
{
|
||||
var note = new ClientNoteMetaResponse
|
||||
{
|
||||
Note = gameEvent.Data?.Trim(),
|
||||
OriginEntityId = gameEvent.Origin.ClientId,
|
||||
ModifiedDate = DateTime.UtcNow
|
||||
};
|
||||
await _metaService.SetPersistentMetaValue("ClientNotes", note, gameEvent.Target.ClientId);
|
||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_ADD_CLIENT_NOTE_SUCCESS"]);
|
||||
}
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models.Client;
|
||||
using Data.Models.Misc;
|
||||
using IW4MAdmin.Application.Alerts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Alerts;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
@ -16,19 +19,66 @@ namespace IW4MAdmin.Application.Commands
|
||||
{
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IAlertManager _alertManager;
|
||||
private const short MaxLength = 1024;
|
||||
|
||||
|
||||
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
|
||||
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout)
|
||||
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger, IAlertManager alertManager)
|
||||
: base(config, layout)
|
||||
{
|
||||
Name = "offlinemessage";
|
||||
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
|
||||
Alias = "om";
|
||||
Permission = EFClient.Permission.Moderator;
|
||||
RequiresTarget = true;
|
||||
|
||||
|
||||
_contextFactory = contextFactory;
|
||||
_logger = logger;
|
||||
_alertManager = alertManager;
|
||||
|
||||
_alertManager.RegisterStaticAlertSource(async () =>
|
||||
{
|
||||
var context = contextFactory.CreateContext(false);
|
||||
return await context.InboxMessages.Where(message => !message.IsDelivered)
|
||||
.Where(message => message.CreatedDateTime >= DateTime.UtcNow.AddDays(-7))
|
||||
.Where(message => message.DestinationClient.Level > EFClient.Permission.User)
|
||||
.Select(message => new Alert.AlertState
|
||||
{
|
||||
OccuredAt = message.CreatedDateTime,
|
||||
Message = message.Message,
|
||||
ExpiresAt = DateTime.UtcNow.AddDays(7),
|
||||
Category = Alert.AlertCategory.Message,
|
||||
Source = message.SourceClient.CurrentAlias.Name.StripColors(),
|
||||
SourceId = message.SourceClientId,
|
||||
RecipientId = message.DestinationClientId,
|
||||
ReferenceId = message.InboxMessageId,
|
||||
Type = nameof(EFInboxMessage)
|
||||
}).ToListAsync();
|
||||
});
|
||||
|
||||
_alertManager.OnAlertConsumed += (_, state) =>
|
||||
{
|
||||
if (state.Category != Alert.AlertCategory.Message || state.ReferenceId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var context = contextFactory.CreateContext(true);
|
||||
foreach (var message in context.InboxMessages
|
||||
.Where(message => message.InboxMessageId == state.ReferenceId.Value).ToList())
|
||||
{
|
||||
message.IsDelivered = true;
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not update message state for alert {@Alert}", state);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||
@ -38,23 +88,24 @@ namespace IW4MAdmin.Application.Commands
|
||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
|
||||
{
|
||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (gameEvent.Target.IsIngame)
|
||||
{
|
||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"].FormatExt(gameEvent.Target.Name));
|
||||
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"]
|
||||
.FormatExt(gameEvent.Target.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
|
||||
|
||||
var newMessage = new EFInboxMessage()
|
||||
var newMessage = new EFInboxMessage
|
||||
{
|
||||
SourceClientId = gameEvent.Origin.ClientId,
|
||||
DestinationClientId = gameEvent.Target.ClientId,
|
||||
@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands
|
||||
Message = gameEvent.Data,
|
||||
};
|
||||
|
||||
_alertManager.AddAlert(gameEvent.Target.BuildAlert(Alert.AlertCategory.Message)
|
||||
.WithMessage(gameEvent.Data.Trim())
|
||||
.FromClient(gameEvent.Origin)
|
||||
.OfType(nameof(EFInboxMessage))
|
||||
.ExpiresIn(TimeSpan.FromDays(7)));
|
||||
|
||||
try
|
||||
{
|
||||
context.Set<EFInboxMessage>().Add(newMessage);
|
||||
@ -75,4 +132,4 @@ namespace IW4MAdmin.Application.Commands
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ namespace IW4MAdmin.Application.Commands
|
||||
Name = "readmessage";
|
||||
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
|
||||
Alias = "rm";
|
||||
Permission = EFClient.Permission.Flagged;
|
||||
Permission = EFClient.Permission.User;
|
||||
|
||||
_contextFactory = contextFactory;
|
||||
_logger = logger;
|
||||
@ -76,4 +76,4 @@ namespace IW4MAdmin.Application.Commands
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": [
|
||||
|
@ -9,6 +9,7 @@ using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@ -24,8 +25,10 @@ using Serilog.Context;
|
||||
using static SharedLibraryCore.Database.Models.EFClient;
|
||||
using Data.Models;
|
||||
using Data.Models.Server;
|
||||
using IW4MAdmin.Application.Alerts;
|
||||
using IW4MAdmin.Application.Commands;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore.Alerts;
|
||||
using static Data.Models.Client.EFClient;
|
||||
|
||||
namespace IW4MAdmin
|
||||
@ -73,7 +76,7 @@ namespace IW4MAdmin
|
||||
{
|
||||
ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber);
|
||||
|
||||
EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId);
|
||||
var client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId, GameName);
|
||||
|
||||
// first time client is connecting to server
|
||||
if (client == null)
|
||||
@ -116,7 +119,7 @@ namespace IW4MAdmin
|
||||
|
||||
public override async Task OnClientDisconnected(EFClient client)
|
||||
{
|
||||
if (!GetClientsAsList().Any(_client => _client.NetworkId == client.NetworkId))
|
||||
if (GetClientsAsList().All(eachClient => eachClient.NetworkId != client.NetworkId))
|
||||
{
|
||||
using (LogContext.PushProperty("Server", ToString()))
|
||||
{
|
||||
@ -152,10 +155,10 @@ namespace IW4MAdmin
|
||||
{
|
||||
if (E.IsBlocking)
|
||||
{
|
||||
await E.Origin?.Lock();
|
||||
await E.Origin.Lock();
|
||||
}
|
||||
|
||||
bool canExecuteCommand = true;
|
||||
var canExecuteCommand = true;
|
||||
|
||||
try
|
||||
{
|
||||
@ -164,30 +167,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)
|
||||
{
|
||||
@ -202,15 +205,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);
|
||||
@ -306,8 +309,16 @@ namespace IW4MAdmin
|
||||
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
||||
{
|
||||
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
|
||||
|
||||
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
|
||||
.WithCategory(Alert.AlertCategory.Error)
|
||||
.FromSource("System")
|
||||
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"))
|
||||
.ExpiresIn(TimeSpan.FromDays(1));
|
||||
|
||||
Manager.AlertManager.AddAlert(alert);
|
||||
}
|
||||
|
||||
|
||||
Throttled = true;
|
||||
}
|
||||
|
||||
@ -318,7 +329,15 @@ namespace IW4MAdmin
|
||||
|
||||
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
|
||||
{
|
||||
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
|
||||
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"));
|
||||
|
||||
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
|
||||
.WithCategory(Alert.AlertCategory.Information)
|
||||
.FromSource("System")
|
||||
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"))
|
||||
.ExpiresIn(TimeSpan.FromDays(1));
|
||||
|
||||
Manager.AlertManager.AddAlert(alert);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(CustomSayName))
|
||||
@ -355,9 +374,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
|
||||
@ -431,7 +450,7 @@ namespace IW4MAdmin
|
||||
Clients[E.Origin.ClientNumber] = E.Origin;
|
||||
try
|
||||
{
|
||||
E.Origin.GameName = (Reference.Game?)GameName;
|
||||
E.Origin.GameName = (Reference.Game)GameName;
|
||||
E.Origin = await OnClientConnected(E.Origin);
|
||||
E.Target = E.Origin;
|
||||
}
|
||||
@ -499,7 +518,7 @@ namespace IW4MAdmin
|
||||
|
||||
E.Target.SetLevel(Permission.User, E.Origin);
|
||||
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
|
||||
E.Target.CurrentAlias?.IPAddress);
|
||||
E.Target.GameName, E.Target.CurrentAlias?.IPAddress);
|
||||
await Manager.GetPenaltyService().Create(unflagPenalty);
|
||||
}
|
||||
|
||||
@ -671,23 +690,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)
|
||||
@ -723,6 +769,34 @@ namespace IW4MAdmin
|
||||
{
|
||||
E.Origin.UpdateTeam(E.Extra as string);
|
||||
}
|
||||
|
||||
else if (E.Type == GameEvent.EventType.MetaUpdated)
|
||||
{
|
||||
if (E.Extra is "PersistentClientGuid")
|
||||
{
|
||||
var parts = E.Data.Split(",");
|
||||
|
||||
if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
|
||||
int.TryParse(parts[1], out var low))
|
||||
{
|
||||
var guid = long.Parse(high.ToString("X") + low.ToString("X"), NumberStyles.HexNumber);
|
||||
|
||||
var penalties = await Manager.GetPenaltyService()
|
||||
.GetActivePenaltiesByIdentifier(null, guid, (Reference.Game)GameName);
|
||||
var banPenalty =
|
||||
penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
|
||||
|
||||
if (banPenalty is not null && E.Origin.Level != Permission.Banned)
|
||||
{
|
||||
ServerLogger.LogInformation(
|
||||
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
|
||||
E.Origin.ToString(), guid);
|
||||
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(guid),
|
||||
Utilities.IW4MAdminClient(this), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (ChatHistory)
|
||||
{
|
||||
@ -745,7 +819,7 @@ namespace IW4MAdmin
|
||||
|
||||
private async Task OnClientUpdate(EFClient origin)
|
||||
{
|
||||
var client = Manager.GetActiveClients().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
|
||||
var client = GetClientsAsList().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
|
||||
|
||||
if (client == null)
|
||||
{
|
||||
@ -790,12 +864,10 @@ namespace IW4MAdmin
|
||||
/// array index 2 = updated clients
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
async Task<List<EFClient>[]> PollPlayersAsync()
|
||||
async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token)
|
||||
{
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var currentClients = GetClientsAsList();
|
||||
var statusResponse = await this.GetStatusAsync(tokenSource.Token);
|
||||
var statusResponse = await this.GetStatusAsync(token);
|
||||
|
||||
if (statusResponse is null)
|
||||
{
|
||||
@ -918,11 +990,11 @@ namespace IW4MAdmin
|
||||
private DateTime _lastMessageSent = DateTime.Now;
|
||||
private DateTime _lastPlayerCount = DateTime.Now;
|
||||
|
||||
public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
||||
public override async Task<bool> ProcessUpdatesAsync(CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cts.IsCancellationRequested)
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
await ShutdownInternal();
|
||||
return true;
|
||||
@ -936,7 +1008,7 @@ namespace IW4MAdmin
|
||||
return true;
|
||||
}
|
||||
|
||||
var polledClients = await PollPlayersAsync();
|
||||
var polledClients = await PollPlayersAsync(token);
|
||||
|
||||
if (polledClients is null)
|
||||
{
|
||||
@ -947,7 +1019,7 @@ namespace IW4MAdmin
|
||||
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
|
||||
{
|
||||
disconnectingClient.CurrentServer = this;
|
||||
var e = new GameEvent()
|
||||
var e = new GameEvent
|
||||
{
|
||||
Type = GameEvent.EventType.PreDisconnect,
|
||||
Origin = disconnectingClient,
|
||||
@ -964,7 +1036,7 @@ namespace IW4MAdmin
|
||||
!string.IsNullOrEmpty(client.Name) && (client.Ping != 999 || client.IsBot)))
|
||||
{
|
||||
client.CurrentServer = this;
|
||||
client.GameName = (Reference.Game?)GameName;
|
||||
client.GameName = (Reference.Game)GameName;
|
||||
|
||||
var e = new GameEvent
|
||||
{
|
||||
@ -1229,28 +1301,17 @@ namespace IW4MAdmin
|
||||
this.GamePassword = gamePassword.Value;
|
||||
UpdateMap(mapname);
|
||||
|
||||
if (RconParser.CanGenerateLogPath)
|
||||
if (RconParser.CanGenerateLogPath && string.IsNullOrEmpty(ServerConfig.ManualLogPath))
|
||||
{
|
||||
bool needsRestart = false;
|
||||
|
||||
if (logsync.Value == 0)
|
||||
{
|
||||
await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4
|
||||
needsRestart = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(logfile.Value))
|
||||
{
|
||||
logfile.Value = "games_mp.log";
|
||||
await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken);
|
||||
needsRestart = true;
|
||||
}
|
||||
|
||||
if (needsRestart)
|
||||
{
|
||||
// disabling this for the time being
|
||||
/*Logger.WriteWarning("Game log file not properly initialized, restarting map...");
|
||||
await this.ExecuteCommandAsync("map_restart");*/
|
||||
}
|
||||
|
||||
// this DVAR isn't set until the a map is loaded
|
||||
@ -1456,6 +1517,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)
|
||||
{
|
||||
@ -1486,6 +1552,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());
|
||||
@ -1514,7 +1585,7 @@ namespace IW4MAdmin
|
||||
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString());
|
||||
targetClient.SetLevel(Permission.User, originClient);
|
||||
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
|
||||
targetClient.NetworkId, targetClient.CurrentAlias?.IPAddress);
|
||||
targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
|
||||
await Manager.GetPenaltyService().Create(unbanPenalty);
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Helpers;
|
||||
using Integrations.Source.Extensions;
|
||||
using IW4MAdmin.Application.Alerts;
|
||||
using IW4MAdmin.Application.Extensions;
|
||||
using IW4MAdmin.Application.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -448,6 +449,7 @@ namespace IW4MAdmin.Application
|
||||
.AddSingleton<IServerDataCollector, ServerDataCollector>()
|
||||
.AddSingleton<IEventPublisher, EventPublisher>()
|
||||
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
|
||||
.AddSingleton<IAlertManager, AlertManager>()
|
||||
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
|
||||
.AddSingleton(translationLookup)
|
||||
.AddDatabaseContextOptions(appConfig);
|
||||
|
@ -33,7 +33,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
builder.Append(header);
|
||||
builder.Append(config.NoticeLineSeparator);
|
||||
// build the reason
|
||||
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense);
|
||||
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense.FormatMessageForEngine(config));
|
||||
|
||||
if (isNewLineSeparator)
|
||||
{
|
||||
@ -117,4 +117,4 @@ namespace IW4MAdmin.Application.Misc
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
@ -29,10 +30,15 @@ namespace IW4MAdmin.Application.Misc
|
||||
OnClientConnect?.Invoke(this, gameEvent);
|
||||
}
|
||||
|
||||
if (gameEvent.Type == GameEvent.EventType.Disconnect)
|
||||
if (gameEvent.Type == GameEvent.EventType.Disconnect && gameEvent.Origin.ClientId != 0)
|
||||
{
|
||||
OnClientDisconnect?.Invoke(this, gameEvent);
|
||||
}
|
||||
|
||||
if (gameEvent.Type == GameEvent.EventType.MetaUpdated)
|
||||
{
|
||||
OnClientMetaUpdated?.Invoke(this, gameEvent);
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
@ -41,4 +47,4 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()));
|
||||
|
@ -5,6 +5,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models.Client;
|
||||
using Data.Models.Client.Stats;
|
||||
using Data.Models.Server;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -22,18 +23,20 @@ namespace IW4MAdmin.Application.Misc
|
||||
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
|
||||
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
|
||||
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
|
||||
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
|
||||
|
||||
private readonly TimeSpan? _cacheTimeSpan =
|
||||
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
|
||||
|
||||
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
|
||||
IDataValueCache<EFClient, (int, int)> serverStatsCache,
|
||||
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache)
|
||||
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
|
||||
{
|
||||
_logger = logger;
|
||||
_snapshotCache = snapshotCache;
|
||||
_serverStatsCache = serverStatsCache;
|
||||
_clientHistoryCache = clientHistoryCache;
|
||||
_rankedClientsCache = rankedClientsCache;
|
||||
}
|
||||
|
||||
public async Task<(int?, DateTime?)>
|
||||
@ -160,5 +163,30 @@ namespace IW4MAdmin.Application.Misc
|
||||
return Enumerable.Empty<ClientHistoryInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
|
||||
{
|
||||
_rankedClientsCache.SetCacheItem(async (set, cancellationToken) =>
|
||||
{
|
||||
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
|
||||
return await set
|
||||
.Where(rating => rating.Newest)
|
||||
.Where(rating => rating.ServerId == serverId)
|
||||
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
|
||||
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
|
||||
.Where(rating => rating.Ranking != null)
|
||||
.CountAsync(cancellationToken);
|
||||
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
|
||||
|
||||
try
|
||||
{
|
||||
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(RankedClientsCountAsync));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,40 +9,41 @@ namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
internal class TokenAuthentication : ITokenAuthentication
|
||||
{
|
||||
private readonly ConcurrentDictionary<long, TokenState> _tokens;
|
||||
private readonly ConcurrentDictionary<int, TokenState> _tokens;
|
||||
private readonly RandomNumberGenerator _random;
|
||||
private static readonly TimeSpan TimeoutPeriod = new TimeSpan(0, 0, 120);
|
||||
private static readonly TimeSpan TimeoutPeriod = new(0, 0, 120);
|
||||
private const short TokenLength = 4;
|
||||
|
||||
public TokenAuthentication()
|
||||
{
|
||||
_tokens = new ConcurrentDictionary<long, TokenState>();
|
||||
_tokens = new ConcurrentDictionary<int, TokenState>();
|
||||
_random = RandomNumberGenerator.Create();
|
||||
}
|
||||
|
||||
public bool AuthorizeToken(long networkId, string token)
|
||||
public bool AuthorizeToken(ITokenIdentifier authInfo)
|
||||
{
|
||||
var authorizeSuccessful = _tokens.ContainsKey(networkId) && _tokens[networkId].Token == token;
|
||||
var authorizeSuccessful = _tokens.ContainsKey(authInfo.ClientId) &&
|
||||
_tokens[authInfo.ClientId].Token == authInfo.Token;
|
||||
|
||||
if (authorizeSuccessful)
|
||||
{
|
||||
_tokens.TryRemove(networkId, out _);
|
||||
_tokens.TryRemove(authInfo.ClientId, out _);
|
||||
}
|
||||
|
||||
return authorizeSuccessful;
|
||||
}
|
||||
|
||||
public TokenState GenerateNextToken(long networkId)
|
||||
public TokenState GenerateNextToken(ITokenIdentifier authInfo)
|
||||
{
|
||||
TokenState state;
|
||||
|
||||
if (_tokens.ContainsKey(networkId))
|
||||
if (_tokens.ContainsKey(authInfo.ClientId))
|
||||
{
|
||||
state = _tokens[networkId];
|
||||
state = _tokens[authInfo.ClientId];
|
||||
|
||||
if ((DateTime.Now - state.RequestTime) > TimeoutPeriod)
|
||||
if (DateTime.Now - state.RequestTime > TimeoutPeriod)
|
||||
{
|
||||
_tokens.TryRemove(networkId, out _);
|
||||
_tokens.TryRemove(authInfo.ClientId, out _);
|
||||
}
|
||||
|
||||
else
|
||||
@ -53,17 +54,16 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
state = new TokenState
|
||||
{
|
||||
NetworkId = networkId,
|
||||
Token = _generateToken(),
|
||||
TokenDuration = TimeoutPeriod
|
||||
};
|
||||
|
||||
_tokens.TryAdd(networkId, state);
|
||||
_tokens.TryAdd(authInfo.ClientId, state);
|
||||
|
||||
// perform some housekeeping so we don't have built up tokens if they're not ever used
|
||||
foreach (var (key, value) in _tokens)
|
||||
{
|
||||
if ((DateTime.Now - value.RequestTime) > TimeoutPeriod)
|
||||
if (DateTime.Now - value.RequestTime > TimeoutPeriod)
|
||||
{
|
||||
_tokens.TryRemove(key, out _);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ namespace IW4MAdmin.Application.RConParsers
|
||||
public class BaseRConParser : IRConParser
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private static string _botIpIndicator = "00000000.";
|
||||
|
||||
public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory)
|
||||
{
|
||||
@ -52,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);
|
||||
@ -81,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();
|
||||
}
|
||||
@ -104,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") ||
|
||||
@ -290,8 +291,15 @@ namespace IW4MAdmin.Application.RConParsers
|
||||
long networkId;
|
||||
var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
|
||||
string networkIdString;
|
||||
|
||||
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
|
||||
|
||||
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]]
|
||||
.Contains(_botIpIndicator))
|
||||
{
|
||||
ip = System.Net.IPAddress.Broadcast.ToString().ConvertToIP();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
|
||||
@ -306,9 +314,9 @@ namespace IW4MAdmin.Application.RConParsers
|
||||
continue;
|
||||
}
|
||||
|
||||
var client = new EFClient()
|
||||
var client = new EFClient
|
||||
{
|
||||
CurrentAlias = new EFAlias()
|
||||
CurrentAlias = new EFAlias
|
||||
{
|
||||
Name = name,
|
||||
IPAddress = ip
|
||||
@ -360,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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -9,6 +10,11 @@ namespace Data.Abstractions
|
||||
{
|
||||
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
|
||||
TimeSpan? expirationTime = null, bool autoRefresh = false);
|
||||
|
||||
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
|
||||
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false);
|
||||
|
||||
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
|
||||
Task<TReturnType> GetCacheItem(string keyName, object id = null, CancellationToken token = default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,15 @@ namespace Data.Context
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
// make network id unique
|
||||
modelBuilder.Entity<EFClient>(entity => { entity.HasIndex(e => e.NetworkId).IsUnique(); });
|
||||
modelBuilder.Entity<EFClient>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.NetworkId);
|
||||
entity.HasAlternateKey(client => new
|
||||
{
|
||||
client.NetworkId,
|
||||
client.GameName
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity<EFPenalty>(entity =>
|
||||
{
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
@ -15,8 +17,8 @@ namespace Data.Helpers
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
|
||||
private readonly ConcurrentDictionary<string, CacheState<TReturnType>> _cacheStates =
|
||||
new ConcurrentDictionary<string, CacheState<TReturnType>>();
|
||||
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
|
||||
private readonly object _defaultKey = new();
|
||||
|
||||
private bool _autoRefresh;
|
||||
private const int DefaultExpireMinutes = 15;
|
||||
@ -51,41 +53,61 @@ namespace Data.Helpers
|
||||
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||
TimeSpan? expirationTime = null, bool autoRefresh = false)
|
||||
{
|
||||
if (_cacheStates.ContainsKey(key))
|
||||
{
|
||||
_logger.LogDebug("Cache key {Key} is already added", key);
|
||||
return;
|
||||
}
|
||||
|
||||
var state = new CacheState<TReturnType>
|
||||
{
|
||||
Key = key,
|
||||
Getter = getter,
|
||||
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
|
||||
};
|
||||
|
||||
_autoRefresh = autoRefresh;
|
||||
|
||||
_cacheStates.TryAdd(key, state);
|
||||
|
||||
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
|
||||
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
|
||||
_timer.Start();
|
||||
SetCacheItem(getter, key, null, expirationTime, autoRefresh);
|
||||
}
|
||||
|
||||
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default)
|
||||
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
|
||||
{
|
||||
ids ??= new[] { _defaultKey };
|
||||
|
||||
if (!_cacheStates.ContainsKey(key))
|
||||
{
|
||||
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
|
||||
}
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
if (_cacheStates[key].ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var state = new CacheState<TReturnType>
|
||||
{
|
||||
Key = key,
|
||||
Getter = getter,
|
||||
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
|
||||
};
|
||||
|
||||
_cacheStates[key].Add(id, state);
|
||||
|
||||
_autoRefresh = autoRefresh;
|
||||
|
||||
|
||||
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
|
||||
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
|
||||
_timer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
|
||||
await GetCacheItem(keyName, null, cancellationToken);
|
||||
|
||||
public async Task<TReturnType> GetCacheItem(string keyName, object id = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_cacheStates.ContainsKey(keyName))
|
||||
{
|
||||
throw new ArgumentException("No cache found for key {key}", keyName);
|
||||
}
|
||||
|
||||
var state = _cacheStates[keyName];
|
||||
var state = id is null ? _cacheStates[keyName].Values.First() : _cacheStates[keyName][id];
|
||||
|
||||
// when auto refresh is off we want to check the expiration and value
|
||||
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
|
||||
@ -115,4 +137,4 @@ namespace Data.Helpers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1638
Data/Migrations/MySql/20220609135128_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
1638
Data/Migrations/MySql/20220609135128_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.MySql
|
||||
{
|
||||
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory",
|
||||
column: "CreatedDateTime");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory");
|
||||
}
|
||||
}
|
||||
}
|
1639
Data/Migrations/MySql/20220613192602_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1639
Data/Migrations/MySql/20220613192602_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,63 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.MySql
|
||||
{
|
||||
public partial class AddAlternateKeyToEFClients : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.Sql("UPDATE `EFClients` set `GameName` = 0 WHERE `GameName` IS NULL");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients",
|
||||
columns: new[] { "NetworkId", "GameName" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "int",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
1639
Data/Migrations/MySql/20220616224602_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1639
Data/Migrations/MySql/20220616224602_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.MySql
|
||||
{
|
||||
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
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;");
|
||||
}
|
||||
}
|
||||
}
|
@ -64,7 +64,7 @@ namespace Data.Migrations.MySql
|
||||
b.Property<DateTime>("FirstConnection")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("GameName")
|
||||
b.Property<int>("GameName")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("LastConnection")
|
||||
@ -90,12 +90,13 @@ namespace Data.Migrations.MySql
|
||||
|
||||
b.HasKey("ClientId");
|
||||
|
||||
b.HasAlternateKey("NetworkId", "GameName");
|
||||
|
||||
b.HasIndex("AliasLinkId");
|
||||
|
||||
b.HasIndex("CurrentAliasId");
|
||||
|
||||
b.HasIndex("NetworkId")
|
||||
.IsUnique();
|
||||
b.HasIndex("NetworkId");
|
||||
|
||||
b.ToTable("EFClients", (string)null);
|
||||
});
|
||||
@ -456,6 +457,8 @@ namespace Data.Migrations.MySql
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("CreatedDateTime");
|
||||
|
||||
b.HasIndex("Ranking");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
1695
Data/Migrations/Postgresql/20220609135210_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
1695
Data/Migrations/Postgresql/20220609135210_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Postgresql
|
||||
{
|
||||
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory",
|
||||
column: "CreatedDateTime");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory");
|
||||
}
|
||||
}
|
||||
}
|
1696
Data/Migrations/Postgresql/20220613181913_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1696
Data/Migrations/Postgresql/20220613181913_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,63 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Postgresql
|
||||
{
|
||||
public partial class AddAlternateKeyToEFClients : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.Sql("UPDATE \"EFClients\" SET \"GameName\" = 0 WHERE \"GameName\" IS NULL");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "integer",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients",
|
||||
columns: new[] { "NetworkId", "GameName" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "integer",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "integer");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
1696
Data/Migrations/Postgresql/20220616224145_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1696
Data/Migrations/Postgresql/20220616224145_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Postgresql
|
||||
{
|
||||
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(
|
||||
@"CREATE INDEX""IX_EFClientMessages_TimeSentDesc""
|
||||
ON public.""EFClientMessages"" USING btree
|
||||
(""TimeSent"" DESC NULLS LAST)
|
||||
TABLESPACE pg_default;"
|
||||
);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"DROP INDEX public.""IX_EFClientMessages_TimeSentDesc""");
|
||||
}
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ namespace Data.Migrations.Postgresql
|
||||
b.Property<DateTime>("FirstConnection")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int?>("GameName")
|
||||
b.Property<int>("GameName")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("LastConnection")
|
||||
@ -97,12 +97,13 @@ namespace Data.Migrations.Postgresql
|
||||
|
||||
b.HasKey("ClientId");
|
||||
|
||||
b.HasAlternateKey("NetworkId", "GameName");
|
||||
|
||||
b.HasIndex("AliasLinkId");
|
||||
|
||||
b.HasIndex("CurrentAliasId");
|
||||
|
||||
b.HasIndex("NetworkId")
|
||||
.IsUnique();
|
||||
b.HasIndex("NetworkId");
|
||||
|
||||
b.ToTable("EFClients", (string)null);
|
||||
});
|
||||
@ -475,6 +476,8 @@ namespace Data.Migrations.Postgresql
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("CreatedDateTime");
|
||||
|
||||
b.HasIndex("Ranking");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
1636
Data/Migrations/Sqlite/20220609022511_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
1636
Data/Migrations/Sqlite/20220609022511_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Sqlite
|
||||
{
|
||||
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory",
|
||||
column: "CreatedDateTime");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory");
|
||||
}
|
||||
}
|
||||
}
|
1637
Data/Migrations/Sqlite/20220613160952_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1637
Data/Migrations/Sqlite/20220613160952_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Sqlite
|
||||
{
|
||||
public partial class AddAlternateKeyToEFClients : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients",
|
||||
columns: new[] { "NetworkId", "GameName" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "INTEGER",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
1637
Data/Migrations/Sqlite/20220616225008_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1637
Data/Migrations/Sqlite/20220616225008_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,19 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Sqlite
|
||||
{
|
||||
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -62,7 +62,7 @@ namespace Data.Migrations.Sqlite
|
||||
b.Property<DateTime>("FirstConnection")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("GameName")
|
||||
b.Property<int>("GameName")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastConnection")
|
||||
@ -88,12 +88,13 @@ namespace Data.Migrations.Sqlite
|
||||
|
||||
b.HasKey("ClientId");
|
||||
|
||||
b.HasAlternateKey("NetworkId", "GameName");
|
||||
|
||||
b.HasIndex("AliasLinkId");
|
||||
|
||||
b.HasIndex("CurrentAliasId");
|
||||
|
||||
b.HasIndex("NetworkId")
|
||||
.IsUnique();
|
||||
b.HasIndex("NetworkId");
|
||||
|
||||
b.ToTable("EFClients", (string)null);
|
||||
});
|
||||
@ -454,6 +455,8 @@ namespace Data.Migrations.Sqlite
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("CreatedDateTime");
|
||||
|
||||
b.HasIndex("Ranking");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
@ -63,7 +63,7 @@ namespace Data.Models.Client
|
||||
public DateTime FirstConnection { get; set; }
|
||||
[Required]
|
||||
public DateTime LastConnection { get; set; }
|
||||
public Reference.Game? GameName { get; set; } = Reference.Game.UKN;
|
||||
public Reference.Game GameName { get; set; } = Reference.Game.UKN;
|
||||
public bool Masked { get; set; }
|
||||
[Required]
|
||||
public int AliasLinkId { get; set; }
|
||||
|
@ -7,8 +7,6 @@ namespace Data.Models.Client.Stats
|
||||
{
|
||||
public class EFClientRankingHistory: AuditFields
|
||||
{
|
||||
public const int MaxRankingCount = 30;
|
||||
|
||||
[Key]
|
||||
public long ClientRankingHistoryId { get; set; }
|
||||
|
||||
@ -28,4 +26,4 @@ namespace Data.Models.Client.Stats
|
||||
public double? ZScore { get; set; }
|
||||
public double? PerformanceMetric { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,8 @@ namespace Data.Models.Configuration
|
||||
entity.HasIndex(ranking => ranking.Ranking);
|
||||
entity.HasIndex(ranking => ranking.ZScore);
|
||||
entity.HasIndex(ranking => ranking.UpdatedDateTime);
|
||||
entity.HasIndex(ranking => ranking.CreatedDateTime);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,8 @@
|
||||
T6 = 7,
|
||||
T7 = 8,
|
||||
SHG1 = 9,
|
||||
CSGO = 10
|
||||
CSGO = 10,
|
||||
H1 = 11
|
||||
}
|
||||
|
||||
public enum ConnectionType
|
||||
@ -24,4 +25,4 @@
|
||||
Disconnect
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,9 @@ onPlayerConnect( player )
|
||||
for( ;; )
|
||||
{
|
||||
level waittill( "connected", player );
|
||||
player setClientDvar("cl_autorecord", 1);
|
||||
player setClientDvar("cl_demosKeep", 200);
|
||||
player setClientDvars( "cl_autorecord", 1,
|
||||
"cl_demosKeep", 200 );
|
||||
|
||||
player thread waitForFrameThread();
|
||||
player thread waitForAttack();
|
||||
}
|
||||
@ -60,7 +61,7 @@ getHttpString( url )
|
||||
|
||||
runRadarUpdates()
|
||||
{
|
||||
interval = int(getDvar("sv_printradar_updateinterval"));
|
||||
interval = getDvarInt( "sv_printradar_updateinterval", 500 );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -191,7 +192,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
|
||||
i++;
|
||||
}
|
||||
|
||||
lastAttack = int(getTime()) - int(self.lastAttackTime);
|
||||
lastAttack = getTime() - self.lastAttackTime;
|
||||
isAlive = isAlive(self);
|
||||
|
||||
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
||||
|
@ -53,7 +53,7 @@ waitForAttack()
|
||||
|
||||
runRadarUpdates()
|
||||
{
|
||||
interval = int(getDvar("sv_printradar_updateinterval"));
|
||||
interval = getDvarInt( "sv_printradar_updateinterval", 500 );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -183,7 +183,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
|
||||
i++;
|
||||
}
|
||||
|
||||
lastAttack = int(getTime()) - int(self.lastAttackTime);
|
||||
lastAttack = getTime() - self.lastAttackTime;
|
||||
isAlive = isAlive(self);
|
||||
|
||||
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
||||
|
@ -60,7 +60,7 @@ waitForAttack()
|
||||
|
||||
runRadarUpdates()
|
||||
{
|
||||
interval = int(getDvar("sv_printradar_updateinterval"));
|
||||
interval = getDvarInt( "sv_printradar_updateinterval" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -190,7 +190,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
|
||||
i++;
|
||||
}
|
||||
|
||||
lastAttack = int(getTime()) - int(self.lastAttackTime);
|
||||
lastAttack = getTime() - self.lastAttackTime;
|
||||
isAlive = isAlive(self);
|
||||
|
||||
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );
|
||||
|
@ -53,15 +53,13 @@ init()
|
||||
level thread OnPlayerConnect();
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////
|
||||
// Client Methods
|
||||
//////////////////////////////////
|
||||
|
||||
OnPlayerConnect()
|
||||
{
|
||||
level endon ( "disconnect" );
|
||||
level endon ( "game_ended" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -69,7 +67,7 @@ OnPlayerConnect()
|
||||
|
||||
level.iw4adminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" );
|
||||
|
||||
if ( isDefined(player.pers["isBot"]) && player.pers["isBot"] )
|
||||
if ( isDefined( player.pers["isBot"] ) && player.pers["isBot"] )
|
||||
{
|
||||
// we don't want to track bots
|
||||
continue;
|
||||
@ -106,7 +104,7 @@ OnPlayerSpawned()
|
||||
|
||||
OnPlayerDisconnect()
|
||||
{
|
||||
level endon ( "disconnect" );
|
||||
self endon ( "disconnect" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -141,8 +139,6 @@ OnPlayerJoinedSpectators()
|
||||
|
||||
OnGameEnded()
|
||||
{
|
||||
level endon ( "disconnect" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
level waittill( "game_ended" );
|
||||
@ -167,6 +163,33 @@ DisplayWelcomeData()
|
||||
self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection );
|
||||
}
|
||||
|
||||
SetPersistentData()
|
||||
{
|
||||
guidHigh = self GetPlayerData( "bests", "none" );
|
||||
guidLow = self GetPlayerData( "awards", "none" );
|
||||
persistentGuid = guidHigh + "," + guidLow;
|
||||
|
||||
if ( guidHigh != 0 && guidLow != 0)
|
||||
{
|
||||
if ( level.iw4adminIntegrationDebug == 1 )
|
||||
{
|
||||
IPrintLn( "Uploading persistent guid " + persistentGuid );
|
||||
}
|
||||
|
||||
SetClientMeta( "PersistentClientGuid", persistentGuid );
|
||||
}
|
||||
|
||||
if ( level.iw4adminIntegrationDebug == 1 )
|
||||
{
|
||||
IPrintLn( "Persisting client guid " + persistentGuid );
|
||||
}
|
||||
|
||||
guid = self SplitGuid();
|
||||
|
||||
self SetPlayerData( "bests", "none", guid["high"] );
|
||||
self SetPlayerData( "awards", "none", guid["low"] );
|
||||
}
|
||||
|
||||
PlayerConnectEvents()
|
||||
{
|
||||
self endon( "disconnect" );
|
||||
@ -208,8 +231,7 @@ PlayerTrackingOnInterval()
|
||||
|
||||
MonitorClientEvents()
|
||||
{
|
||||
level endon( "disconnect" );
|
||||
self endon( "disconnect" );
|
||||
level endon( "game_ended" );
|
||||
|
||||
for ( ;; )
|
||||
{
|
||||
@ -304,6 +326,107 @@ DecrementClientMeta( metaKey, decrementValue, clientId )
|
||||
SetClientMeta( metaKey, decrementValue, clientId, "decrement" );
|
||||
}
|
||||
|
||||
SplitGuid()
|
||||
{
|
||||
guid = self GetGuid();
|
||||
|
||||
if ( isDefined( self.guid ) )
|
||||
{
|
||||
guid = self.guid;
|
||||
}
|
||||
|
||||
firstPart = 0;
|
||||
secondPart = 0;
|
||||
stringLength = 17;
|
||||
firstPartExp = 0;
|
||||
secondPartExp = 0;
|
||||
|
||||
for ( i = stringLength - 1; i > 0; i-- )
|
||||
{
|
||||
char = GetSubStr( guid, i - 1, i );
|
||||
if ( char == "" )
|
||||
{
|
||||
char = "0";
|
||||
}
|
||||
|
||||
if ( i > stringLength / 2 )
|
||||
{
|
||||
value = GetIntForHexChar( char );
|
||||
power = Pow( 16, secondPartExp );
|
||||
secondPart = secondPart + ( value * power );
|
||||
secondPartExp++;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = GetIntForHexChar( char );
|
||||
power = Pow( 16, firstPartExp );
|
||||
firstPart = firstPart + ( value * power );
|
||||
firstPartExp++;
|
||||
}
|
||||
}
|
||||
|
||||
split = [];
|
||||
split["low"] = int( secondPart );
|
||||
split["high"] = int( firstPart );
|
||||
|
||||
return split;
|
||||
}
|
||||
|
||||
Pow( num, exponent )
|
||||
{
|
||||
result = 1;
|
||||
while( exponent != 0 )
|
||||
{
|
||||
result = result * num;
|
||||
exponent--;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
GetIntForHexChar( char )
|
||||
{
|
||||
char = ToLower( char );
|
||||
// generated by co-pilot because I can't be bothered to make it more "elegant"
|
||||
switch( char )
|
||||
{
|
||||
case "0":
|
||||
return 0;
|
||||
case "1":
|
||||
return 1;
|
||||
case "2":
|
||||
return 2;
|
||||
case "3":
|
||||
return 3;
|
||||
case "4":
|
||||
return 4;
|
||||
case "5":
|
||||
return 5;
|
||||
case "6":
|
||||
return 6;
|
||||
case "7":
|
||||
return 7;
|
||||
case "8":
|
||||
return 8;
|
||||
case "9":
|
||||
return 9;
|
||||
case "a":
|
||||
return 10;
|
||||
case "b":
|
||||
return 11;
|
||||
case "c":
|
||||
return 12;
|
||||
case "d":
|
||||
return 13;
|
||||
case "e":
|
||||
return 14;
|
||||
case "f":
|
||||
return 15;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
GenerateJoinTeamString( isSpectator )
|
||||
{
|
||||
team = self.team;
|
||||
@ -456,7 +579,7 @@ MonitorBus()
|
||||
|
||||
QueueEvent( request, eventType, notifyEntity )
|
||||
{
|
||||
level endon( "disconnect" );
|
||||
level endon( "game_ended" );
|
||||
|
||||
start = GetTime();
|
||||
maxWait = level.eventBus.timeout * 1000; // 30 seconds
|
||||
@ -490,6 +613,8 @@ QueueEvent( request, eventType, notifyEntity )
|
||||
{
|
||||
notifyEntity NotifyClientEventTimeout( eventType );
|
||||
}
|
||||
|
||||
SetDvar( level.eventBus.inVar, "" );
|
||||
|
||||
return;
|
||||
}
|
||||
@ -643,6 +768,7 @@ OnClientDataReceived( event )
|
||||
self.persistentClientId = event.data["clientId"];
|
||||
|
||||
self thread DisplayWelcomeData();
|
||||
self setPersistentData();
|
||||
}
|
||||
|
||||
OnExecuteCommand( event )
|
||||
@ -902,7 +1028,7 @@ GotoCoordImpl( data )
|
||||
return;
|
||||
}
|
||||
|
||||
position = ( int(data["x"]), int(data["y"]), int(data["z"]) );
|
||||
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
|
||||
self SetOrigin( position );
|
||||
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -4,6 +4,7 @@ using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Threading.Tasks;
|
||||
using SharedLibraryCore.Helpers;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Login.Commands
|
||||
{
|
||||
@ -18,7 +19,7 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
||||
RequiresTarget = false;
|
||||
Arguments = new CommandArgument[]
|
||||
{
|
||||
new CommandArgument()
|
||||
new()
|
||||
{
|
||||
Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
|
||||
Required = true
|
||||
@ -26,24 +27,28 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||
{
|
||||
bool success = E.Owner.Manager.TokenAuthenticator.AuthorizeToken(E.Origin.NetworkId, E.Data);
|
||||
var success = gameEvent.Owner.Manager.TokenAuthenticator.AuthorizeToken(new TokenIdentifier
|
||||
{
|
||||
ClientId = gameEvent.Origin.ClientId,
|
||||
Token = gameEvent.Data
|
||||
});
|
||||
|
||||
if (!success)
|
||||
{
|
||||
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, E.Origin.PasswordSalt));
|
||||
success = hashedPassword[0] == E.Origin.Password;
|
||||
var hashedPassword = await Task.FromResult(Hashing.Hash(gameEvent.Data, gameEvent.Origin.PasswordSalt));
|
||||
success = hashedPassword[0] == gameEvent.Origin.Password;
|
||||
}
|
||||
|
||||
if (success)
|
||||
{
|
||||
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
|
||||
Plugin.AuthorizedClients[gameEvent.Origin.ClientId] = true;
|
||||
}
|
||||
|
||||
_ = success ?
|
||||
E.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
|
||||
E.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
||||
gameEvent.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
|
||||
gameEvent.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -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'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult()
|
||||
: metaService.DecrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult();
|
||||
} else {
|
||||
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId).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,
|
||||
|
||||
@ -16,13 +16,18 @@ var plugin = {
|
||||
|
||||
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t5';
|
||||
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
|
||||
rconParser.Configuration.Dvar.Pattern = '^(?:\\^7)?\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$';
|
||||
rconParser.Configuration.Dvar.Pattern = '^(?:\\^7)?\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n)?\\w*(.+)*$';
|
||||
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
|
||||
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
|
||||
rconParser.Configuration.GuidNumberStyle = 7; // Integer
|
||||
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) {}
|
||||
};
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ var eventParser;
|
||||
|
||||
var plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 0.2,
|
||||
version: 0.3,
|
||||
name: 'Call of Duty 5: World at War Parser',
|
||||
isParser: true,
|
||||
|
||||
@ -17,6 +17,7 @@ var plugin = {
|
||||
rconParser.Configuration.GuidNumberStyle = 7; // Integer
|
||||
rconParser.Configuration.DefaultRConPort = 28960;
|
||||
rconParser.Version = 'Call of Duty Multiplayer COD_WaW MP build 1.7.1263 CL(350073) JADAMS2 Thu Oct 29 15:43:55 2009 win-x86';
|
||||
rconParser.GameName = 5; // T4
|
||||
|
||||
eventParser.Configuration.GuidNumberStyle = 7; // Integer
|
||||
eventParser.GameName = 5; // T4
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models.Client;
|
||||
@ -88,8 +89,8 @@ namespace Stats.Client
|
||||
return zScore ?? 0;
|
||||
}, MaxZScoreCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30));
|
||||
|
||||
await _distributionCache.GetCacheItem(DistributionCacheKey);
|
||||
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey);
|
||||
await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
||||
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
|
||||
|
||||
/*foreach (var serverId in _serverIds)
|
||||
{
|
||||
@ -132,7 +133,7 @@ namespace Stats.Client
|
||||
|
||||
public async Task<double> GetZScoreForServer(long serverId, double value)
|
||||
{
|
||||
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey);
|
||||
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
||||
if (!serverParams.ContainsKey(serverId))
|
||||
{
|
||||
return 0.0;
|
||||
@ -150,7 +151,7 @@ namespace Stats.Client
|
||||
|
||||
public async Task<double?> GetRatingForZScore(double? value)
|
||||
{
|
||||
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey);
|
||||
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
|
||||
return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore);
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
}
|
||||
else
|
||||
{
|
||||
gameEvent.Owner.Broadcast(topStats);
|
||||
await gameEvent.Owner.BroadcastAsync(topStats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ namespace Stats.Dtos
|
||||
public EFClient.Permission Level { get; set; }
|
||||
public double? Performance { get; set; }
|
||||
public int? Ranking { get; set; }
|
||||
public int TotalRankedClients { get; set; }
|
||||
public double? ZScore { get; set; }
|
||||
public double? Rating { get; set; }
|
||||
public List<ServerInfo> Servers { get; set; }
|
||||
@ -25,4 +26,4 @@ namespace Stats.Dtos
|
||||
public List<EFClientRankingHistory> Ratings { get; set; }
|
||||
public List<EFClientStatistics> LegacyStats { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ namespace Stats.Dtos
|
||||
/// <summary>
|
||||
/// only look for messages sent after this date
|
||||
/// </summary>
|
||||
public DateTime SentAfter { get; set; } = DateTime.UtcNow.AddYears(-100);
|
||||
public DateTime? SentAfter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// only look for messages sent before this date0
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models;
|
||||
using Data.Models.Client;
|
||||
using Data.Models.Client.Stats;
|
||||
using IW4MAdmin.Plugins.Stats;
|
||||
@ -12,7 +13,6 @@ using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using Stats.Client.Abstractions;
|
||||
using Stats.Dtos;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
@ -50,7 +50,8 @@ namespace Stats.Helpers
|
||||
{
|
||||
client.ClientId,
|
||||
client.CurrentAlias.Name,
|
||||
client.Level
|
||||
client.Level,
|
||||
client.GameName
|
||||
}).FirstOrDefaultAsync(client => client.ClientId == query.ClientId);
|
||||
|
||||
if (clientInfo == null)
|
||||
@ -77,7 +78,8 @@ namespace Stats.Helpers
|
||||
.Where(r => r.ClientId == clientInfo.ClientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
.Where(r => r.Ranking != null)
|
||||
.OrderByDescending(r => r.UpdatedDateTime)
|
||||
.OrderByDescending(r => r.CreatedDateTime)
|
||||
.Take(250)
|
||||
.ToListAsync();
|
||||
|
||||
var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest);
|
||||
@ -111,8 +113,9 @@ namespace Stats.Helpers
|
||||
Rating = mostRecentRanking?.PerformanceMetric,
|
||||
All = hitStats,
|
||||
Servers = _manager.GetServers()
|
||||
.Select(server => new ServerInfo()
|
||||
{Name = server.Hostname, IPAddress = server.IP, Port = server.Port})
|
||||
.Select(server => new ServerInfo
|
||||
{Name = server.Hostname, IPAddress = server.IP, Port = server.Port, Game = (Reference.Game)server.GameName})
|
||||
.Where(server => server.Game == clientInfo.GameName)
|
||||
.ToList(),
|
||||
Aggregate = hitStats.FirstOrDefault(hit =>
|
||||
hit.HitLocationId == null && hit.ServerId == serverId && hit.WeaponId == null &&
|
||||
@ -153,4 +156,4 @@ namespace Stats.Helpers
|
||||
&& (zScore == null || stats.ZScore > zScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,58 +45,53 @@ namespace Stats.Helpers
|
||||
var result = new ResourceQueryHelperResult<MessageResponse>();
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
if (serverCache == null)
|
||||
{
|
||||
serverCache = await context.Set<EFServer>().ToListAsync();
|
||||
}
|
||||
serverCache ??= await context.Set<EFServer>().ToListAsync();
|
||||
|
||||
if (int.TryParse(query.ServerId, out int serverId))
|
||||
if (int.TryParse(query.ServerId, out var serverId))
|
||||
{
|
||||
query.ServerId = serverCache.FirstOrDefault(_server => _server.ServerId == serverId)?.EndPoint ?? query.ServerId;
|
||||
query.ServerId = serverCache.FirstOrDefault(server => server.ServerId == serverId)?.EndPoint ?? query.ServerId;
|
||||
}
|
||||
|
||||
var iqMessages = context.Set<EFClientMessage>()
|
||||
.Where(_message => _message.TimeSent >= query.SentAfter)
|
||||
.Where(_message => _message.TimeSent < query.SentBefore);
|
||||
.Where(message => message.TimeSent < query.SentBefore);
|
||||
|
||||
if (query.ClientId != null)
|
||||
if (query.SentAfter is not null)
|
||||
{
|
||||
iqMessages = iqMessages.Where(_message => _message.ClientId == query.ClientId.Value);
|
||||
iqMessages = iqMessages.Where(message => message.TimeSent >= query.SentAfter);
|
||||
}
|
||||
|
||||
if (query.ServerId != null)
|
||||
if (query.ClientId is not null)
|
||||
{
|
||||
iqMessages = iqMessages.Where(_message => _message.Server.EndPoint == query.ServerId);
|
||||
iqMessages = iqMessages.Where(message => message.ClientId == query.ClientId.Value);
|
||||
}
|
||||
|
||||
if (query.ServerId is not null)
|
||||
{
|
||||
iqMessages = iqMessages.Where(message => message.Server.EndPoint == query.ServerId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(query.MessageContains))
|
||||
{
|
||||
iqMessages = iqMessages.Where(_message => EF.Functions.Like(_message.Message.ToLower(), $"%{query.MessageContains.ToLower()}%"));
|
||||
iqMessages = iqMessages.Where(message => EF.Functions.Like(message.Message.ToLower(), $"%{query.MessageContains.ToLower()}%"));
|
||||
}
|
||||
|
||||
var iqResponse = iqMessages
|
||||
.Select(_message => new MessageResponse
|
||||
.Select(message => new MessageResponse
|
||||
{
|
||||
ClientId = _message.ClientId,
|
||||
ClientName = query.IsProfileMeta ? "" : _message.Client.CurrentAlias.Name,
|
||||
ServerId = _message.ServerId,
|
||||
When = _message.TimeSent,
|
||||
Message = _message.Message,
|
||||
ServerName = query.IsProfileMeta ? "" : _message.Server.HostName,
|
||||
GameName = _message.Server.GameName == null ? Server.Game.IW4 : (Server.Game)_message.Server.GameName.Value,
|
||||
SentIngame = _message.SentIngame
|
||||
ClientId = message.ClientId,
|
||||
ClientName = query.IsProfileMeta ? "" : message.Client.CurrentAlias.Name,
|
||||
ServerId = message.ServerId,
|
||||
When = message.TimeSent,
|
||||
Message = message.Message,
|
||||
ServerName = query.IsProfileMeta ? "" : message.Server.HostName,
|
||||
GameName = message.Server.GameName == null ? Server.Game.IW4 : (Server.Game)message.Server.GameName.Value,
|
||||
SentIngame = message.SentIngame
|
||||
});
|
||||
|
||||
if (query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending)
|
||||
{
|
||||
iqResponse = iqResponse.OrderByDescending(_message => _message.When);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
iqResponse = iqResponse.OrderBy(_message => _message.When);
|
||||
}
|
||||
|
||||
iqResponse = query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending
|
||||
? iqResponse.OrderByDescending(message => message.When)
|
||||
: iqResponse.OrderBy(message => message.When);
|
||||
|
||||
var resultList = await iqResponse
|
||||
.Skip(query.Offset)
|
||||
.Take(query.Count)
|
||||
@ -115,13 +110,13 @@ namespace Stats.Helpers
|
||||
{
|
||||
var quickMessages = _defaultSettings
|
||||
.QuickMessages
|
||||
.First(_qm => _qm.Game == message.GameName);
|
||||
.First(qm => qm.Game == message.GameName);
|
||||
message.Message = quickMessages.Messages[message.Message.Substring(1)];
|
||||
message.IsQuickMessage = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
message.Message = message.Message.Substring(1);
|
||||
message.Message = message.Message[1..];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,7 +86,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
public async Task<int> GetClientOverallRanking(int clientId, long? serverId = null)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
|
||||
if (_config.EnableAdvancedMetrics)
|
||||
{
|
||||
var clientRanking = await context.Set<EFClientRankingHistory>()
|
||||
@ -117,7 +117,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null, long? serverId = null)
|
||||
public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null,
|
||||
long? serverId = null)
|
||||
{
|
||||
return (ranking) => ranking.ServerId == serverId
|
||||
&& ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned
|
||||
@ -138,6 +139,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
public class RankingSnapshot
|
||||
{
|
||||
public int ClientId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public DateTime LastConnection { get; set; }
|
||||
public double? PerformanceMetric { get; set; }
|
||||
public double? ZScore { get; set; }
|
||||
public int? Ranking { get; set; }
|
||||
public DateTime CreatedDateTime { get; set; }
|
||||
}
|
||||
|
||||
public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
@ -150,24 +162,38 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.Take(count)
|
||||
.ToListAsync();
|
||||
|
||||
var rankings = await context.Set<EFClientRankingHistory>()
|
||||
.Where(ranking => clientIdsList.Contains(ranking.ClientId))
|
||||
.Where(ranking => ranking.ServerId == serverId)
|
||||
.Select(ranking => new
|
||||
{
|
||||
ranking.ClientId,
|
||||
ranking.Client.CurrentAlias.Name,
|
||||
ranking.Client.LastConnection,
|
||||
ranking.PerformanceMetric,
|
||||
ranking.ZScore,
|
||||
ranking.Ranking,
|
||||
ranking.CreatedDateTime
|
||||
})
|
||||
.ToListAsync();
|
||||
var rankingsDict = new Dictionary<int, List<RankingSnapshot>>();
|
||||
|
||||
foreach (var clientId in clientIdsList)
|
||||
{
|
||||
var eachRank = await context.Set<EFClientRankingHistory>()
|
||||
.Where(ranking => ranking.ClientId == clientId)
|
||||
.Where(ranking => ranking.ServerId == serverId)
|
||||
.OrderByDescending(ranking => ranking.CreatedDateTime)
|
||||
.Select(ranking => new RankingSnapshot
|
||||
{
|
||||
ClientId = ranking.ClientId,
|
||||
Name = ranking.Client.CurrentAlias.Name,
|
||||
LastConnection = ranking.Client.LastConnection,
|
||||
PerformanceMetric = ranking.PerformanceMetric,
|
||||
ZScore = ranking.ZScore,
|
||||
Ranking = ranking.Ranking,
|
||||
CreatedDateTime = ranking.CreatedDateTime
|
||||
})
|
||||
.Take(60)
|
||||
.ToListAsync();
|
||||
|
||||
if (rankingsDict.ContainsKey(clientId))
|
||||
{
|
||||
rankingsDict[clientId] = rankingsDict[clientId].Concat(eachRank).Distinct()
|
||||
.OrderByDescending(ranking => ranking.CreatedDateTime).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
rankingsDict.Add(clientId, eachRank);
|
||||
}
|
||||
}
|
||||
|
||||
var rankingsDict = rankings.GroupBy(rank => rank.ClientId)
|
||||
.ToDictionary(rank => rank.Key, rank => rank.OrderBy(r => r.CreatedDateTime).ToList());
|
||||
|
||||
var statsInfo = await context.Set<EFClientStatistics>()
|
||||
.Where(stat => clientIdsList.Contains(stat.ClientId))
|
||||
.Where(stat => stat.TimePlayed > 0)
|
||||
@ -179,7 +205,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ClientId = s.Key,
|
||||
Kills = s.Sum(c => c.Kills),
|
||||
Deaths = s.Sum(c => c.Deaths),
|
||||
KDR = s.Sum(c => (c.Kills / (double) (c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
|
||||
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
|
||||
s.Sum(c => c.TimePlayed),
|
||||
TotalTimePlayed = s.Sum(c => c.TimePlayed),
|
||||
UpdatedAt = s.Max(c => c.UpdatedAt)
|
||||
@ -187,30 +213,32 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.ToListAsync();
|
||||
|
||||
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();
|
||||
.OrderByDescending(stat => rankingsDict[stat.ClientId].First().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].First().LastConnection))
|
||||
.HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false),
|
||||
LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].First().LastConnection),
|
||||
Name = rankingsDict[s.ClientId].First().Name,
|
||||
Performance = Math.Round(rankingsDict[s.ClientId].First().PerformanceMetric ?? 0, 2),
|
||||
RatingChange = (rankingsDict[s.ClientId].Last().Ranking -
|
||||
rankingsDict[s.ClientId].First().Ranking) ?? 0,
|
||||
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => new PerformanceHistory
|
||||
{ Performance = ranking.PerformanceMetric ?? 0, OccurredAt = ranking.CreatedDateTime })
|
||||
.ToList(),
|
||||
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
|
||||
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed),
|
||||
Ranking = index + start + 1,
|
||||
ZScore = rankingsDict[s.ClientId].First().ZScore,
|
||||
ServerId = serverId
|
||||
})
|
||||
.OrderBy(r => r.Ranking)
|
||||
.ToList();
|
||||
|
||||
return finished;
|
||||
}
|
||||
@ -221,7 +249,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
return await GetNewTopStats(start, count, serverId);
|
||||
}
|
||||
|
||||
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
// setup the query for the clients within the given rating range
|
||||
var iqClientRatings = (from rating in context.Set<EFRating>()
|
||||
@ -264,7 +292,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.Select(grp => new
|
||||
{
|
||||
grp.Key,
|
||||
Ratings = grp.Select(r => new {r.Performance, r.Ranking, r.When})
|
||||
Ratings = grp.Select(r => new { r.Performance, r.Ranking, r.When })
|
||||
});
|
||||
|
||||
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
|
||||
@ -278,7 +306,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ClientId = s.Key,
|
||||
Kills = s.Sum(c => c.Kills),
|
||||
Deaths = s.Sum(c => c.Deaths),
|
||||
KDR = s.Sum(c => (c.Kills / (double) (c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
|
||||
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
|
||||
s.Sum(c => c.TimePlayed),
|
||||
TotalTimePlayed = s.Sum(c => c.TimePlayed),
|
||||
});
|
||||
@ -289,7 +317,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 +330,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)
|
||||
})
|
||||
@ -366,7 +404,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
Port = sv.Port,
|
||||
EndPoint = sv.ToString(),
|
||||
ServerId = serverId,
|
||||
GameName = (Reference.Game?) sv.GameName,
|
||||
GameName = (Reference.Game?)sv.GameName,
|
||||
HostName = sv.Hostname
|
||||
};
|
||||
|
||||
@ -376,9 +414,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
// we want to set the gamename up if it's never been set, or it changed
|
||||
else if (!server.GameName.HasValue || server.GameName.Value != (Reference.Game) sv.GameName)
|
||||
else if (!server.GameName.HasValue || server.GameName.Value != (Reference.Game)sv.GameName)
|
||||
{
|
||||
server.GameName = (Reference.Game) sv.GameName;
|
||||
server.GameName = (Reference.Game)sv.GameName;
|
||||
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
@ -469,7 +507,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
Active = true,
|
||||
HitCount = 0,
|
||||
Location = (int) hl
|
||||
Location = (int)hl
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
@ -489,7 +527,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
Active = true,
|
||||
HitCount = 0,
|
||||
Location = (int) hl
|
||||
Location = (int)hl
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@ -521,9 +559,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
catch (DbUpdateException updateException) when (
|
||||
updateException.InnerException is PostgresException {SqlState: "23503"}
|
||||
|| updateException.InnerException is SqliteException {SqliteErrorCode: 787}
|
||||
|| updateException.InnerException is MySqlException {SqlState: "23503"})
|
||||
updateException.InnerException is PostgresException { SqlState: "23503" }
|
||||
|| updateException.InnerException is SqliteException { SqliteErrorCode: 787 }
|
||||
|| updateException.InnerException is MySqlException { SqlState: "23503" })
|
||||
{
|
||||
_log.LogWarning("Trying to add {Client} to stats before they have been added to the database",
|
||||
pl.ToString());
|
||||
@ -644,9 +682,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
ServerId = serverId,
|
||||
DeathOrigin = vDeathOrigin,
|
||||
KillOrigin = vKillOrigin,
|
||||
DeathType = (int) ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
|
||||
DeathType = (int)ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
|
||||
Damage = int.Parse(damage),
|
||||
HitLoc = (int) ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
|
||||
HitLoc = (int)ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
|
||||
WeaponReference = weapon,
|
||||
ViewAngles = vViewAngles,
|
||||
TimeOffset = long.Parse(offset),
|
||||
@ -660,21 +698,21 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
AnglesList = snapshotAngles,
|
||||
IsAlive = isAlive == "1",
|
||||
TimeSinceLastAttack = long.Parse(lastAttackTime),
|
||||
GameName = (int) attacker.CurrentServer.GameName
|
||||
GameName = (int)attacker.CurrentServer.GameName
|
||||
};
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Could not parse script hit data. Damage={Damage}, TimeOffset={Offset}, TimeSinceLastAttack={LastAttackTime}",
|
||||
_log.LogError(ex,
|
||||
"Could not parse script hit data. Damage={Damage}, TimeOffset={Offset}, TimeSinceLastAttack={LastAttackTime}",
|
||||
damage, offset, lastAttackTime);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
hit.SetAdditionalProperty("HitLocationReference", hitLoc);
|
||||
|
||||
if (hit.HitLoc == (int) IW4Info.HitLocation.shield)
|
||||
if (hit.HitLoc == (int)IW4Info.HitLocation.shield)
|
||||
{
|
||||
// we don't care about shield hits
|
||||
return;
|
||||
@ -693,9 +731,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
await waiter.WaitAsync(Utilities.DefaultCommandTimeout, Plugin.ServerManager.CancellationToken);
|
||||
|
||||
// increment their hit count
|
||||
if (hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
|
||||
hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
|
||||
hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
|
||||
if (hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
|
||||
hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
|
||||
hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
|
||||
{
|
||||
clientStats.HitLocations.First(hl => hl.Location == hit.HitLoc).HitCount += 1;
|
||||
}
|
||||
@ -838,7 +876,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
if (!gameDetectionTypes[server.GameName].Contains(detectionType))
|
||||
@ -870,7 +908,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
new EFPenalty()
|
||||
{
|
||||
AutomatedOffense = penalty.Type == Detection.DetectionType.Bone
|
||||
? $"{penalty.Type}-{(int) penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
|
||||
? $"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
|
||||
: $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
|
||||
}
|
||||
};
|
||||
@ -887,7 +925,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
string flagReason = penalty.Type == Cheat.Detection.DetectionType.Bone
|
||||
? $"{penalty.Type}-{(int) penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
|
||||
? $"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
|
||||
: $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}";
|
||||
|
||||
penaltyClient.AdministeredPenalties = new List<EFPenalty>()
|
||||
@ -926,19 +964,19 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
// update the total stats
|
||||
_servers[serverId].ServerStatistics.TotalKills += 1;
|
||||
|
||||
|
||||
if (attackerStats == null)
|
||||
{
|
||||
_log.LogWarning("Stats for {Client} are not yet initialized", attacker.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (victimStats == null)
|
||||
{
|
||||
_log.LogWarning("Stats for {Client} are not yet initialized", victim.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// this happens when the round has changed
|
||||
if (attackerStats.SessionScore == 0)
|
||||
{
|
||||
@ -951,10 +989,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
}
|
||||
|
||||
var estimatedAttackerScore = attacker.CurrentServer.GameName != Server.Game.CSGO
|
||||
? attacker.Score
|
||||
? attacker.Score
|
||||
: attackerStats.SessionKills * 50;
|
||||
var estimatedVictimScore = attacker.CurrentServer.GameName != Server.Game.CSGO
|
||||
? victim.Score
|
||||
var estimatedVictimScore = attacker.CurrentServer.GameName != Server.Game.CSGO
|
||||
? victim.Score
|
||||
: victimStats.SessionKills * 50;
|
||||
|
||||
attackerStats.SessionScore = estimatedAttackerScore;
|
||||
@ -1042,7 +1080,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
/// <returns></returns>
|
||||
public async Task UpdateStatHistory(EFClient client, EFClientStatistics clientStats)
|
||||
{
|
||||
int currentSessionTime = (int) (DateTime.UtcNow - client.LastConnection).TotalSeconds;
|
||||
int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds;
|
||||
|
||||
// don't update their stat history if they haven't played long
|
||||
if (currentSessionTime < 60)
|
||||
@ -1215,7 +1253,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext();
|
||||
var minPlayTime = _config.TopPlayersMinPlayTime;
|
||||
|
||||
|
||||
var performances = await context.Set<EFClientStatistics>()
|
||||
.AsNoTracking()
|
||||
.Where(stat => stat.ClientId == clientId)
|
||||
@ -1223,7 +1261,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.Where(stats => stats.UpdatedAt >= Extensions.FifteenDaysAgo())
|
||||
.Where(stats => stats.TimePlayed >= minPlayTime)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
if (clientStats.TimePlayed >= minPlayTime)
|
||||
{
|
||||
clientStats.ZScore = await _serverDistributionCalculator.GetZScoreForServer(serverId,
|
||||
@ -1254,8 +1292,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
if (performances.Any(performance => performance.TimePlayed >= minPlayTime))
|
||||
{
|
||||
var aggregateZScore = performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
|
||||
|
||||
var aggregateZScore =
|
||||
performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
|
||||
|
||||
int? aggregateRanking = await context.Set<EFClientStatistics>()
|
||||
.Where(stat => stat.ClientId != clientId)
|
||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime))
|
||||
@ -1274,7 +1313,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
clientStats.Client?.ToString(), aggregateZScore);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var aggregateRankingSnapshot = new EFClientRankingHistory
|
||||
{
|
||||
ClientId = clientId,
|
||||
@ -1297,7 +1336,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
.Where(r => r.ClientId == clientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
.CountAsync();
|
||||
|
||||
|
||||
var mostRecent = await context.Set<EFClientRankingHistory>()
|
||||
.Where(r => r.ClientId == clientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
@ -1309,14 +1348,20 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
context.Update(mostRecent);
|
||||
}
|
||||
|
||||
if (totalRankingEntries > EFClientRankingHistory.MaxRankingCount)
|
||||
const int maxRankingCount = 1728; // 60 / 2.5 * 24 * 3 ( 3 days at sample every 2.5 minutes)
|
||||
|
||||
if (totalRankingEntries > maxRankingCount)
|
||||
{
|
||||
var lastRating = await context.Set<EFClientRankingHistory>()
|
||||
.Where(r => r.ClientId == clientId)
|
||||
.Where(r => r.ServerId == serverId)
|
||||
.OrderBy(r => r.CreatedDateTime)
|
||||
.FirstOrDefaultAsync();
|
||||
context.Remove(lastRating);
|
||||
|
||||
if (lastRating is not null)
|
||||
{
|
||||
context.Remove(lastRating);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1325,7 +1370,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
/// </summary>
|
||||
/// <param name="attackerStats">Stats of the attacker</param>
|
||||
/// <param name="victimStats">Stats of the victim</param>
|
||||
public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats,
|
||||
public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats,
|
||||
EFClient attacker, EFClient victim)
|
||||
{
|
||||
bool suicide = attackerStats.ClientId == victimStats.ClientId;
|
||||
@ -1351,7 +1396,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
// calculate elo
|
||||
var attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) -
|
||||
Math.Log(Math.Max(1, attackerStats.EloRating));
|
||||
Math.Log(Math.Max(1, attackerStats.EloRating));
|
||||
var winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E));
|
||||
|
||||
attackerStats.EloRating += 6.0 * (1 - winPercentage);
|
||||
@ -1361,8 +1406,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2));
|
||||
|
||||
// update after calculation
|
||||
attackerStats.TimePlayed += (int) (DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
|
||||
victimStats.TimePlayed += (int) (DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
|
||||
attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
|
||||
victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
|
||||
attackerStats.LastActive = DateTime.UtcNow;
|
||||
victimStats.LastActive = DateTime.UtcNow;
|
||||
}
|
||||
@ -1400,11 +1445,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
var killSpm = scoreDifference / timeSinceLastCalc;
|
||||
var spmMultiplier = 2.934 *
|
||||
Math.Pow(
|
||||
_servers[clientStats.ServerId]
|
||||
.TeamCount((IW4Info.Team) clientStats.Team == IW4Info.Team.Allies
|
||||
? IW4Info.Team.Axis
|
||||
: IW4Info.Team.Allies), -0.454);
|
||||
Math.Pow(
|
||||
_servers[clientStats.ServerId]
|
||||
.TeamCount((IW4Info.Team)clientStats.Team == IW4Info.Team.Allies
|
||||
? IW4Info.Team.Axis
|
||||
: IW4Info.Team.Allies), -0.454);
|
||||
killSpm *= Math.Max(1, spmMultiplier);
|
||||
|
||||
// update this for ac tracking
|
||||
@ -1421,8 +1466,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
// calculate the weight of the new play time against last 10 hours of gameplay
|
||||
int totalPlayTime = (clientStats.TimePlayed == 0)
|
||||
? (int) (DateTime.UtcNow - clientStats.LastActive).TotalSeconds
|
||||
: clientStats.TimePlayed + (int) (DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
|
||||
? (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds
|
||||
: clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
|
||||
|
||||
double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0));
|
||||
|
||||
@ -1442,7 +1487,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
|
||||
{
|
||||
_log.LogWarning("clientStats SPM/Skill NaN {@killInfo}",
|
||||
new {killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference});
|
||||
new
|
||||
{
|
||||
killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference
|
||||
});
|
||||
clientStats.SPM = 0;
|
||||
clientStats.Skill = 0;
|
||||
}
|
||||
@ -1483,11 +1531,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
public void ResetKillstreaks(Server sv)
|
||||
{
|
||||
foreach (var session in sv.GetClientsAsList()
|
||||
.Select(_client => new
|
||||
{
|
||||
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
|
||||
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
|
||||
}))
|
||||
.Select(_client => new
|
||||
{
|
||||
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
|
||||
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
|
||||
}))
|
||||
{
|
||||
session.stat?.StartNewSession();
|
||||
session.detection?.OnMapChange();
|
||||
@ -1549,8 +1597,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
foreach (var stats in sv.GetClientsAsList()
|
||||
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))
|
||||
.Where(_stats => _stats != null))
|
||||
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))
|
||||
.Where(_stats => _stats != null))
|
||||
{
|
||||
await SaveClientStats(stats);
|
||||
}
|
||||
|
@ -42,10 +42,11 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
private readonly ILogger<Plugin> _logger;
|
||||
private readonly List<IClientStatisticCalculator> _statCalculators;
|
||||
private readonly IServerDistributionCalculator _serverDistributionCalculator;
|
||||
private readonly IServerDataViewer _serverDataViewer;
|
||||
|
||||
public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory,
|
||||
ITranslationLookup translationLookup, IMetaServiceV2 metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper, ILogger<StatManager> managerLogger,
|
||||
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator)
|
||||
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator, IServerDataViewer serverDataViewer)
|
||||
{
|
||||
Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
|
||||
_databaseContextFactory = databaseContextFactory;
|
||||
@ -56,6 +57,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
_logger = logger;
|
||||
_statCalculators = statCalculators.ToList();
|
||||
_serverDistributionCalculator = serverDistributionCalculator;
|
||||
_serverDataViewer = serverDataViewer;
|
||||
}
|
||||
|
||||
public async Task OnEventAsync(GameEvent gameEvent, Server server)
|
||||
@ -201,13 +203,17 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
var performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed);
|
||||
var performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
|
||||
var spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Count(c => c.SPM > 0), 1);
|
||||
var overallRanking = await Manager.GetClientOverallRanking(request.ClientId);
|
||||
|
||||
return new List<InformationResponse>
|
||||
{
|
||||
new InformationResponse
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
|
||||
Value = "#" + (await Manager.GetClientOverallRanking(request.ClientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Value = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING_FORMAT"].FormatExt((overallRanking == 0 ? "--" :
|
||||
overallRanking.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))),
|
||||
(await _serverDataViewer.RankedClientsCountAsync(token: token)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))
|
||||
),
|
||||
Column = 0,
|
||||
Order = 0,
|
||||
Type = MetaType.Information
|
||||
|
@ -17,7 +17,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -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();
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
33
SharedLibraryCore/Alerts/Alert.cs
Normal file
33
SharedLibraryCore/Alerts/Alert.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Data.Models.Client;
|
||||
|
||||
namespace SharedLibraryCore.Alerts;
|
||||
|
||||
public class Alert
|
||||
{
|
||||
public enum AlertCategory
|
||||
{
|
||||
Information,
|
||||
Warning,
|
||||
Error,
|
||||
Message,
|
||||
}
|
||||
|
||||
public class AlertState
|
||||
{
|
||||
public Guid AlertId { get; } = Guid.NewGuid();
|
||||
public AlertCategory Category { get; set; }
|
||||
public DateTime OccuredAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string Source { get; set; }
|
||||
public int? RecipientId { get; set; }
|
||||
public int? SourceId { get; set; }
|
||||
public int? ReferenceId { get; set; }
|
||||
public bool? Delivered { get; set; }
|
||||
public bool? Consumed { get; set; }
|
||||
public EFClient.Permission? MinimumPermission { get; set; }
|
||||
public string Type { get; set; }
|
||||
public static AlertState Build() => new();
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Context;
|
||||
using Data.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
@ -20,28 +19,37 @@ namespace SharedLibraryCore
|
||||
{
|
||||
public class BaseController : Controller
|
||||
{
|
||||
protected readonly IAlertManager AlertManager;
|
||||
|
||||
/// <summary>
|
||||
/// life span in months
|
||||
/// </summary>
|
||||
private const int COOKIE_LIFESPAN = 3;
|
||||
private const int CookieLifespan = 3;
|
||||
|
||||
private static readonly byte[] LocalHost = { 127, 0, 0, 1 };
|
||||
private static string SocialLink;
|
||||
private static string SocialTitle;
|
||||
protected readonly DatabaseContext Context;
|
||||
private static string _socialLink;
|
||||
private static string _socialTitle;
|
||||
|
||||
protected List<Page> Pages;
|
||||
protected List<string> PermissionsSet;
|
||||
protected bool Authorized { get; set; }
|
||||
protected TranslationLookup Localization { get; }
|
||||
protected EFClient Client { get; }
|
||||
protected ApplicationConfiguration AppConfig { get; }
|
||||
|
||||
public IManager Manager { get; }
|
||||
|
||||
public BaseController(IManager manager)
|
||||
{
|
||||
AlertManager = manager.AlertManager;
|
||||
Manager = manager;
|
||||
Localization ??= Utilities.CurrentLocalization.LocalizationIndex;
|
||||
Localization = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
AppConfig = Manager.GetApplicationSettings().Configuration();
|
||||
|
||||
if (AppConfig.EnableSocialLink && SocialLink == null)
|
||||
if (AppConfig.EnableSocialLink && _socialLink == null)
|
||||
{
|
||||
SocialLink = AppConfig.SocialLinkAddress;
|
||||
SocialTitle = AppConfig.SocialLinkTitle;
|
||||
_socialLink = AppConfig.SocialLinkAddress;
|
||||
_socialTitle = AppConfig.SocialLinkTitle;
|
||||
}
|
||||
|
||||
Pages = Manager.GetPageList().Pages
|
||||
@ -56,7 +64,7 @@ namespace SharedLibraryCore
|
||||
ViewBag.EnableColorCodes = AppConfig.EnableColorCodes;
|
||||
ViewBag.Language = Utilities.CurrentLocalization.Culture.TwoLetterISOLanguageName;
|
||||
|
||||
Client ??= new EFClient
|
||||
Client = new EFClient
|
||||
{
|
||||
ClientId = -1,
|
||||
Level = Data.Models.Client.EFClient.Permission.Banned,
|
||||
@ -64,11 +72,7 @@ namespace SharedLibraryCore
|
||||
};
|
||||
}
|
||||
|
||||
public IManager Manager { get; }
|
||||
protected bool Authorized { get; set; }
|
||||
protected TranslationLookup Localization { get; }
|
||||
protected EFClient Client { get; }
|
||||
protected ApplicationConfiguration AppConfig { get; }
|
||||
|
||||
|
||||
protected async Task SignInAsync(ClaimsPrincipal claimsPrinciple)
|
||||
{
|
||||
@ -76,7 +80,7 @@ namespace SharedLibraryCore
|
||||
new AuthenticationProperties
|
||||
{
|
||||
AllowRefresh = true,
|
||||
ExpiresUtc = DateTime.UtcNow.AddMonths(COOKIE_LIFESPAN),
|
||||
ExpiresUtc = DateTime.UtcNow.AddMonths(CookieLifespan),
|
||||
IsPersistent = true,
|
||||
IssuedUtc = DateTime.UtcNow
|
||||
});
|
||||
@ -96,7 +100,7 @@ namespace SharedLibraryCore
|
||||
Client.ClientId = clientId;
|
||||
Client.NetworkId = clientId == 1
|
||||
? 0
|
||||
: User.Claims.First(_claim => _claim.Type == ClaimTypes.PrimarySid).Value
|
||||
: User.Claims.First(claim => claim.Type == ClaimTypes.PrimarySid).Value
|
||||
.ConvertGuidToLong(NumberStyles.HexNumber);
|
||||
Client.Level = (Data.Models.Client.EFClient.Permission)Enum.Parse(
|
||||
typeof(Data.Models.Client.EFClient.Permission),
|
||||
@ -104,6 +108,9 @@ namespace SharedLibraryCore
|
||||
Client.CurrentAlias = new EFAlias
|
||||
{ Name = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value };
|
||||
Authorized = Client.ClientId >= 0;
|
||||
Client.GameName =
|
||||
Enum.Parse<Reference.Game>(User.Claims
|
||||
.First(claim => claim.Type == ClaimTypes.PrimaryGroupSid).Value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +138,7 @@ namespace SharedLibraryCore
|
||||
new Claim(ClaimTypes.Role, Client.Level.ToString()),
|
||||
new Claim(ClaimTypes.Sid, Client.ClientId.ToString()),
|
||||
new Claim(ClaimTypes.PrimarySid, Client.NetworkId.ToString("X")),
|
||||
new Claim(ClaimTypes.PrimaryGroupSid, Client.GameName.ToString())
|
||||
};
|
||||
var claimsIdentity = new ClaimsIdentity(claims, "login");
|
||||
SignInAsync(new ClaimsPrincipal(claimsIdentity)).Wait();
|
||||
@ -150,8 +158,8 @@ namespace SharedLibraryCore
|
||||
ViewBag.Url = AppConfig.WebfrontUrl;
|
||||
ViewBag.User = Client;
|
||||
ViewBag.Version = Manager.Version;
|
||||
ViewBag.SocialLink = SocialLink ?? "";
|
||||
ViewBag.SocialTitle = SocialTitle;
|
||||
ViewBag.SocialLink = _socialLink ?? "";
|
||||
ViewBag.SocialTitle = _socialTitle;
|
||||
ViewBag.Pages = Pages;
|
||||
ViewBag.Localization = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
ViewBag.CustomBranding = shouldUseCommunityName
|
||||
@ -169,6 +177,7 @@ namespace SharedLibraryCore
|
||||
ViewBag.ReportCount = Manager.GetServers().Sum(server =>
|
||||
server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24)));
|
||||
ViewBag.PermissionsSet = PermissionsSet;
|
||||
ViewBag.Alerts = AlertManager.RetrieveAlerts(Client).ToList();
|
||||
|
||||
base.OnActionExecuting(context);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -381,7 +381,7 @@ namespace SharedLibraryCore.Commands
|
||||
{
|
||||
// todo: don't do the lookup here
|
||||
var penalties = await gameEvent.Owner.Manager.GetPenaltyService().GetActivePenaltiesAsync(gameEvent.Target.AliasLinkId,
|
||||
gameEvent.Target.CurrentAliasId, gameEvent.Target.NetworkId, gameEvent.Target.CurrentAlias.IPAddress);
|
||||
gameEvent.Target.CurrentAliasId, gameEvent.Target.NetworkId, gameEvent.Target.GameName, gameEvent.Target.CurrentAlias.IPAddress);
|
||||
|
||||
if (penalties
|
||||
.FirstOrDefault(p =>
|
||||
@ -897,7 +897,7 @@ namespace SharedLibraryCore.Commands
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
var existingPenalties = await E.Owner.Manager.GetPenaltyService()
|
||||
.GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.CurrentAliasId, E.Target.NetworkId, E.Target.IPAddress);
|
||||
.GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.CurrentAliasId, E.Target.NetworkId, E.Target.GameName, E.Target.IPAddress);
|
||||
var penalty = existingPenalties.FirstOrDefault(b => b.Type > EFPenalty.PenaltyType.Kick);
|
||||
|
||||
if (penalty == null)
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models.Client;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace SharedLibraryCore.Commands
|
||||
@ -19,13 +20,16 @@ namespace SharedLibraryCore.Commands
|
||||
RequiresTarget = false;
|
||||
}
|
||||
|
||||
public override Task ExecuteAsync(GameEvent E)
|
||||
public override Task ExecuteAsync(GameEvent gameEvent)
|
||||
{
|
||||
var state = E.Owner.Manager.TokenAuthenticator.GenerateNextToken(E.Origin.NetworkId);
|
||||
E.Origin.Tell(string.Format(_translationLookup["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token,
|
||||
$"{state.RemainingTime} {_translationLookup["GLOBAL_MINUTES"]}", E.Origin.ClientId));
|
||||
var state = gameEvent.Owner.Manager.TokenAuthenticator.GenerateNextToken(new TokenIdentifier
|
||||
{
|
||||
ClientId = gameEvent.Origin.ClientId
|
||||
});
|
||||
gameEvent.Origin.Tell(string.Format(_translationLookup["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token,
|
||||
$"{state.RemainingTime} {_translationLookup["GLOBAL_MINUTES"]}", gameEvent.Origin.ClientId));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Data.Models.Misc;
|
||||
using Newtonsoft.Json;
|
||||
using SharedLibraryCore.Configuration.Attributes;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
@ -154,6 +155,13 @@ namespace SharedLibraryCore.Configuration
|
||||
{ Permission.Console.ToString(), new List<string> { "*" } }
|
||||
};
|
||||
|
||||
public Dictionary<string, Permission> MinimumAlertPermissions { get; set; } = new()
|
||||
{
|
||||
{ nameof(EFInboxMessage), Permission.Trusted },
|
||||
{ GameEvent.EventType.ConnectionLost.ToString(), Permission.Administrator },
|
||||
{ GameEvent.EventType.ConnectionRestored.ToString(), Permission.Administrator }
|
||||
};
|
||||
|
||||
[ConfigurationIgnore]
|
||||
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")]
|
||||
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new()
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Data.Models;
|
||||
using Data.Models.Client;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
@ -10,6 +11,7 @@ namespace SharedLibraryCore.Dtos
|
||||
public int LinkId { get; set; }
|
||||
public EFClient.Permission Level { get; set; }
|
||||
public DateTime LastConnection { get; set; }
|
||||
public Reference.Game Game { get; set; }
|
||||
public bool IsMasked { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SharedLibraryCore.Dtos.Meta.Responses;
|
||||
|
||||
public class ClientNoteMetaResponse
|
||||
{
|
||||
public string Note { get; set; }
|
||||
public int OriginEntityId { get; set; }
|
||||
[JsonIgnore]
|
||||
public string OriginEntityName { get; set; }
|
||||
public DateTime ModifiedDate { get; set; }
|
||||
}
|
@ -9,6 +9,7 @@ namespace SharedLibraryCore.Dtos
|
||||
public class PlayerInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public Reference.Game Game { get; set; }
|
||||
public int ClientId { get; set; }
|
||||
public string Level { get; set; }
|
||||
public string Tag { get; set; }
|
||||
@ -32,5 +33,6 @@ namespace SharedLibraryCore.Dtos
|
||||
public string ConnectProtocolUrl { get;set; }
|
||||
public string CurrentServerName { get; set; }
|
||||
public IGeoLocationResult GeoLocationInfo { get; set; }
|
||||
public ClientNoteMetaResponse NoteMeta { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Data.Models;
|
||||
using SharedLibraryCore.Helpers;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
@ -40,5 +41,6 @@ namespace SharedLibraryCore.Dtos
|
||||
return Math.Round(valid.Select(player => player.ZScore.Value).Average(), 2);
|
||||
}
|
||||
}
|
||||
public Reference.Game Game { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
9
SharedLibraryCore/Helpers/TokenIdentifier.cs
Normal file
9
SharedLibraryCore/Helpers/TokenIdentifier.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace SharedLibraryCore.Helpers;
|
||||
|
||||
public class TokenIdentifier : ITokenIdentifier
|
||||
{
|
||||
public int ClientId { get; set; }
|
||||
public string Token { get; set; }
|
||||
}
|
@ -4,7 +4,6 @@ namespace SharedLibraryCore.Helpers
|
||||
{
|
||||
public sealed class TokenState
|
||||
{
|
||||
public long NetworkId { get; set; }
|
||||
public DateTime RequestTime { get; set; } = DateTime.Now;
|
||||
public TimeSpan TokenDuration { get; set; }
|
||||
public string Token { get; set; }
|
||||
@ -12,4 +11,4 @@ namespace SharedLibraryCore.Helpers
|
||||
public string RemainingTime => Math.Round(-(DateTime.Now - RequestTime).Subtract(TokenDuration).TotalMinutes, 1)
|
||||
.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
54
SharedLibraryCore/Interfaces/IAlertManager.cs
Normal file
54
SharedLibraryCore/Interfaces/IAlertManager.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using SharedLibraryCore.Alerts;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces;
|
||||
|
||||
public interface IAlertManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the manager
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Get all the alerts for given client
|
||||
/// </summary>
|
||||
/// <param name="client">client to retrieve alerts for</param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client);
|
||||
|
||||
/// <summary>
|
||||
/// Trigger a new alert
|
||||
/// </summary>
|
||||
/// <param name="alert">Alert to trigger</param>
|
||||
void AddAlert(Alert.AlertState alert);
|
||||
|
||||
/// <summary>
|
||||
/// Marks an alert as read and removes it from the manager
|
||||
/// </summary>
|
||||
/// <param name="alertId">Id of the alert to mark as read</param>
|
||||
void MarkAlertAsRead(Guid alertId);
|
||||
|
||||
/// <summary>
|
||||
/// Mark all alerts intended for the given recipientId as read
|
||||
/// </summary>
|
||||
/// <param name="recipientId">Identifier of the recipient</param>
|
||||
void MarkAllAlertsAsRead(int recipientId);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a static (persistent) event source eg datastore that
|
||||
/// gets initialized at startup
|
||||
/// </summary>
|
||||
/// <param name="alertSource">Source action</param>
|
||||
void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource);
|
||||
|
||||
/// <summary>
|
||||
/// Fires when an alert has been consumed (dimissed)
|
||||
/// </summary>
|
||||
EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
|
||||
|
||||
}
|
@ -10,7 +10,7 @@ namespace SharedLibraryCore.Interfaces
|
||||
Task<T> Delete(T entity);
|
||||
Task<T> Update(T entity);
|
||||
Task<T> Get(int entityID);
|
||||
Task<T> GetUnique(long entityProperty);
|
||||
Task<T> GetUnique(long entityProperty, object altKey);
|
||||
Task<IList<T>> Find(Func<T, bool> expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,5 +102,7 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// event executed when event has finished executing
|
||||
/// </summary>
|
||||
event EventHandler<GameEvent> OnGameEventExecuted;
|
||||
|
||||
IAlertManager AlertManager { get; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -37,5 +37,13 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// <returns></returns>
|
||||
Task<IEnumerable<ClientHistoryInfo>> ClientHistoryAsync(TimeSpan? overPeriod = null,
|
||||
CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the number of ranked clients for given server id
|
||||
/// </summary>
|
||||
/// <param name="serverId">ServerId to query on</param>
|
||||
/// <param name="token">CancellationToken</param>
|
||||
/// <returns></returns>
|
||||
Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,16 +7,15 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// <summary>
|
||||
/// generates and returns a token for the given network id
|
||||
/// </summary>
|
||||
/// <param name="networkId">network id of the players to generate the token for</param>
|
||||
/// <param name="authInfo">auth information for next token generation</param>
|
||||
/// <returns>4 character string token</returns>
|
||||
TokenState GenerateNextToken(long networkId);
|
||||
TokenState GenerateNextToken(ITokenIdentifier authInfo);
|
||||
|
||||
/// <summary>
|
||||
/// authorizes given token
|
||||
/// </summary>
|
||||
/// <param name="networkId">network id of the client to authorize</param>
|
||||
/// <param name="token">token to authorize</param>
|
||||
/// <param name="authInfo">auth information</param>
|
||||
/// <returns>true if token authorized successfully, false otherwise</returns>
|
||||
bool AuthorizeToken(long networkId, string token);
|
||||
bool AuthorizeToken(ITokenIdentifier authInfo);
|
||||
}
|
||||
}
|
||||
|
7
SharedLibraryCore/Interfaces/ITokenIdentifier.cs
Normal file
7
SharedLibraryCore/Interfaces/ITokenIdentifier.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace SharedLibraryCore.Interfaces;
|
||||
|
||||
public interface ITokenIdentifier
|
||||
{
|
||||
int ClientId { get; }
|
||||
string Token { get; }
|
||||
}
|
@ -76,7 +76,7 @@ namespace SharedLibraryCore.Database.Models
|
||||
[NotMapped]
|
||||
public virtual int? IPAddress
|
||||
{
|
||||
get => CurrentAlias.IPAddress;
|
||||
get => CurrentAlias?.IPAddress;
|
||||
set => CurrentAlias.IPAddress = value;
|
||||
}
|
||||
|
||||
@ -100,7 +100,10 @@ namespace SharedLibraryCore.Database.Models
|
||||
|
||||
[NotMapped] public int Score { get; set; }
|
||||
|
||||
[NotMapped] public bool IsBot => NetworkId == Name.GenerateGuidFromString();
|
||||
[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);
|
||||
@ -682,7 +685,7 @@ namespace SharedLibraryCore.Database.Models
|
||||
|
||||
// we want to get any penalties that are tied to their IP or AliasLink (but not necessarily their GUID)
|
||||
var activePenalties = await CurrentServer.Manager.GetPenaltyService()
|
||||
.GetActivePenaltiesAsync(AliasLinkId, CurrentAliasId, NetworkId, ipAddress);
|
||||
.GetActivePenaltiesAsync(AliasLinkId, CurrentAliasId, NetworkId, GameName, ipAddress);
|
||||
var banPenalty = activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.Ban);
|
||||
var tempbanPenalty =
|
||||
activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.TempBan);
|
||||
|
@ -33,7 +33,8 @@ namespace SharedLibraryCore
|
||||
T6 = 7,
|
||||
T7 = 8,
|
||||
SHG1 = 9,
|
||||
CSGO = 10
|
||||
CSGO = 10,
|
||||
H1 = 11
|
||||
}
|
||||
|
||||
// only here for performance
|
||||
@ -200,7 +201,7 @@ namespace SharedLibraryCore
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public virtual Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
||||
public virtual Task<bool> ProcessUpdatesAsync(CancellationToken token)
|
||||
{
|
||||
return (Task<bool>)Task.CompletedTask;
|
||||
}
|
||||
@ -224,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
|
||||
{
|
||||
@ -288,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()))
|
||||
{
|
||||
|
@ -23,25 +23,26 @@ namespace SharedLibraryCore.Services
|
||||
{
|
||||
public class ClientService : IEntityService<EFClient>, IResourceQueryHelper<FindClientRequest, FindClientResult>
|
||||
{
|
||||
private static readonly Func<DatabaseContext, long, Task<EFClient>> _getUniqueQuery =
|
||||
EF.CompileAsyncQuery((DatabaseContext context, long networkId) =>
|
||||
private static readonly Func<DatabaseContext, long, Reference.Game, Task<EFClient>> GetUniqueQuery =
|
||||
EF.CompileAsyncQuery((DatabaseContext context, long networkId, Reference.Game game) =>
|
||||
context.Clients
|
||||
.Select(_client => new EFClient
|
||||
.Select(client => new EFClient
|
||||
{
|
||||
ClientId = _client.ClientId,
|
||||
AliasLinkId = _client.AliasLinkId,
|
||||
Level = _client.Level,
|
||||
Connections = _client.Connections,
|
||||
FirstConnection = _client.FirstConnection,
|
||||
LastConnection = _client.LastConnection,
|
||||
Masked = _client.Masked,
|
||||
NetworkId = _client.NetworkId,
|
||||
TotalConnectionTime = _client.TotalConnectionTime,
|
||||
AliasLink = _client.AliasLink,
|
||||
Password = _client.Password,
|
||||
PasswordSalt = _client.PasswordSalt
|
||||
ClientId = client.ClientId,
|
||||
AliasLinkId = client.AliasLinkId,
|
||||
Level = client.Level,
|
||||
Connections = client.Connections,
|
||||
FirstConnection = client.FirstConnection,
|
||||
LastConnection = client.LastConnection,
|
||||
Masked = client.Masked,
|
||||
NetworkId = client.NetworkId,
|
||||
TotalConnectionTime = client.TotalConnectionTime,
|
||||
AliasLink = client.AliasLink,
|
||||
Password = client.Password,
|
||||
PasswordSalt = client.PasswordSalt,
|
||||
GameName = client.GameName
|
||||
})
|
||||
.FirstOrDefault(c => c.NetworkId == networkId)
|
||||
.FirstOrDefault(client => client.NetworkId == networkId && client.GameName == game)
|
||||
);
|
||||
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
@ -178,6 +179,7 @@ namespace SharedLibraryCore.Services
|
||||
.Select(_client => new EFClient
|
||||
{
|
||||
ClientId = _client.ClientId,
|
||||
GameName = _client.GameName,
|
||||
AliasLinkId = _client.AliasLinkId,
|
||||
Level = _client.Level,
|
||||
Connections = _client.Connections,
|
||||
@ -234,10 +236,10 @@ namespace SharedLibraryCore.Services
|
||||
return foundClient.Client;
|
||||
}
|
||||
|
||||
public virtual async Task<EFClient> GetUnique(long entityAttribute)
|
||||
public virtual async Task<EFClient> GetUnique(long entityAttribute, object altKey = null)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
return await _getUniqueQuery(context, entityAttribute);
|
||||
return await GetUniqueQuery(context, entityAttribute, (Reference.Game)altKey);
|
||||
}
|
||||
|
||||
public async Task<EFClient> Update(EFClient temporalClient)
|
||||
@ -284,7 +286,7 @@ namespace SharedLibraryCore.Services
|
||||
entity.PasswordSalt = temporalClient.PasswordSalt;
|
||||
}
|
||||
|
||||
entity.GameName ??= temporalClient.GameName;
|
||||
entity.GameName = temporalClient.GameName;
|
||||
|
||||
// update in database
|
||||
await context.SaveChangesAsync();
|
||||
@ -757,19 +759,20 @@ namespace SharedLibraryCore.Services
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
return await context.Clients
|
||||
.Select(_client => new EFClient
|
||||
.Select(client => new EFClient
|
||||
{
|
||||
NetworkId = _client.NetworkId,
|
||||
ClientId = _client.ClientId,
|
||||
NetworkId = client.NetworkId,
|
||||
ClientId = client.ClientId,
|
||||
CurrentAlias = new EFAlias
|
||||
{
|
||||
Name = _client.CurrentAlias.Name
|
||||
Name = client.CurrentAlias.Name
|
||||
},
|
||||
Password = _client.Password,
|
||||
PasswordSalt = _client.PasswordSalt,
|
||||
Level = _client.Level
|
||||
Password = client.Password,
|
||||
PasswordSalt = client.PasswordSalt,
|
||||
GameName = client.GameName,
|
||||
Level = client.Level
|
||||
})
|
||||
.FirstAsync(_client => _client.ClientId == clientId);
|
||||
.FirstAsync(client => client.ClientId == clientId);
|
||||
}
|
||||
|
||||
public async Task<List<EFClient>> GetPrivilegedClients(bool includeName = true)
|
||||
@ -789,7 +792,8 @@ namespace SharedLibraryCore.Services
|
||||
PasswordSalt = client.PasswordSalt,
|
||||
NetworkId = client.NetworkId,
|
||||
LastConnection = client.LastConnection,
|
||||
Masked = client.Masked
|
||||
Masked = client.Masked,
|
||||
GameName = client.GameName
|
||||
};
|
||||
|
||||
return await iqClients.ToListAsync();
|
||||
@ -849,31 +853,35 @@ namespace SharedLibraryCore.Services
|
||||
|
||||
else
|
||||
{
|
||||
iqClients = iqClients.Where(_client => networkId == _client.NetworkId ||
|
||||
linkIds.Contains(_client.AliasLinkId)
|
||||
|| !_appConfig.EnableImplicitAccountLinking &&
|
||||
_client.CurrentAlias.IPAddress != null &&
|
||||
_client.CurrentAlias.IPAddress == ipAddress);
|
||||
iqClients = iqClients.Where(client => networkId == client.NetworkId || linkIds.Contains(client.AliasLinkId));
|
||||
}
|
||||
|
||||
if (ipAddress is not null && !_appConfig.EnableImplicitAccountLinking)
|
||||
{
|
||||
iqClients = iqClients.Union(context.Clients.Where(client => client.CurrentAlias.IPAddress == ipAddress));
|
||||
}
|
||||
|
||||
// we want to project our results
|
||||
var iqClientProjection = iqClients.OrderByDescending(_client => _client.LastConnection)
|
||||
.Select(_client => new PlayerInfo
|
||||
var iqClientProjection = iqClients.OrderByDescending(client => client.LastConnection)
|
||||
.Select(client => new PlayerInfo
|
||||
{
|
||||
Name = _client.CurrentAlias.Name,
|
||||
LevelInt = (int)_client.Level,
|
||||
LastConnection = _client.LastConnection,
|
||||
ClientId = _client.ClientId,
|
||||
IPAddress = _client.CurrentAlias.IPAddress.HasValue
|
||||
? _client.CurrentAlias.SearchableIPAddress
|
||||
: ""
|
||||
Name = client.CurrentAlias.Name,
|
||||
LevelInt = (int)client.Level,
|
||||
LastConnection = client.LastConnection,
|
||||
ClientId = client.ClientId,
|
||||
IPAddress = client.CurrentAlias.IPAddress.HasValue
|
||||
? client.CurrentAlias.SearchableIPAddress
|
||||
: "",
|
||||
Game = client.GameName
|
||||
});
|
||||
|
||||
var clients = await iqClientProjection.ToListAsync();
|
||||
|
||||
// this is so we don't try to evaluate this in the linq to entities query
|
||||
foreach (var client in clients)
|
||||
{
|
||||
client.Level = ((Permission)client.LevelInt).ToLocalizedLevelName();
|
||||
}
|
||||
|
||||
return clients;
|
||||
}
|
||||
@ -930,6 +938,14 @@ namespace SharedLibraryCore.Services
|
||||
return clientList;
|
||||
}
|
||||
|
||||
public async Task<string> GetClientNameById(int clientId)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext();
|
||||
var match = await context.Clients.Select(client => new { client.CurrentAlias.Name, client.ClientId })
|
||||
.FirstOrDefaultAsync(client => client.ClientId == clientId);
|
||||
return match?.Name;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ namespace SharedLibraryCore.Services
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<EFPenalty> GetUnique(long entityProperty)
|
||||
public Task<EFPenalty> GetUnique(long entityProperty, object altKey)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@ -139,10 +139,10 @@ namespace SharedLibraryCore.Services
|
||||
LinkedPenalties.Contains(pi.Penalty.Type) && pi.Penalty.Active &&
|
||||
(pi.Penalty.Expires == null || pi.Penalty.Expires > DateTime.UtcNow);
|
||||
|
||||
public async Task<List<EFPenalty>> GetActivePenaltiesAsync(int linkId, int currentAliasId, long networkId,
|
||||
public async Task<List<EFPenalty>> GetActivePenaltiesAsync(int linkId, int currentAliasId, long networkId, Reference.Game game,
|
||||
int? ip = null)
|
||||
{
|
||||
var penaltiesByIdentifier = await GetActivePenaltiesByIdentifier(ip, networkId);
|
||||
var penaltiesByIdentifier = await GetActivePenaltiesByIdentifier(ip, networkId, game);
|
||||
|
||||
if (penaltiesByIdentifier.Any())
|
||||
{
|
||||
@ -183,16 +183,16 @@ namespace SharedLibraryCore.Services
|
||||
return activePenalties.OrderByDescending(p => p.When).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<EFPenalty>> GetActivePenaltiesByIdentifier(int? ip, long networkId)
|
||||
public async Task<List<EFPenalty>> GetActivePenaltiesByIdentifier(int? ip, long networkId, Reference.Game game)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
|
||||
var activePenaltiesIds = context.PenaltyIdentifiers.Where(identifier =>
|
||||
identifier.IPv4Address != null && identifier.IPv4Address == ip || identifier.NetworkId == networkId)
|
||||
identifier.IPv4Address != null && identifier.IPv4Address == ip || identifier.NetworkId == networkId && identifier.Penalty.Offender.GameName == game)
|
||||
.Where(FilterById);
|
||||
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<EFPenalty>> ActivePenaltiesByRecentIdentifiers(int linkId)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
@ -214,12 +214,12 @@ namespace SharedLibraryCore.Services
|
||||
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync();
|
||||
}
|
||||
|
||||
public virtual async Task RemoveActivePenalties(int aliasLinkId, long networkId, int? ipAddress = null)
|
||||
public virtual async Task RemoveActivePenalties(int aliasLinkId, long networkId, Reference.Game game, int? ipAddress = null)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var activePenalties = await GetActivePenaltiesByIdentifier(ipAddress, networkId);
|
||||
var activePenalties = await GetActivePenaltiesByIdentifier(ipAddress, networkId, game);
|
||||
|
||||
if (activePenalties.Any())
|
||||
{
|
||||
|
@ -4,7 +4,7 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
|
||||
<Version>2022.3.23.1</Version>
|
||||
<Version>2022.6.16.1</Version>
|
||||
<Authors>RaidMax</Authors>
|
||||
<Company>Forever None</Company>
|
||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||
@ -19,7 +19,7 @@
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Description>Shared Library for IW4MAdmin</Description>
|
||||
<PackageVersion>2022.3.23.1</PackageVersion>
|
||||
<PackageVersion>2022.6.16.1</PackageVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user