Compare commits

...

74 Commits

Author SHA1 Message Date
444c06e65e make sure color tokens are mapped for kick messages 2022-07-23 13:48:46 -05:00
561909158f improve penalty display on mobile view 2022-07-23 11:22:16 -05:00
cd12c3f26e set default permission for read message to user 2022-07-23 11:13:21 -05:00
c817f9a810 improve audit log display on mobile 2022-07-23 11:09:23 -05:00
b27ae1517e fix issue with duplicate key on top stats page 2022-07-22 10:28:26 -05:00
507688a175 small tweaks for notes/tags 2022-07-20 11:39:46 -05:00
d2cfd50e39 update webfront permission types 2022-07-20 10:34:33 -05:00
51e8b31e42 add client note command and feature 2022-07-20 10:32:26 -05:00
fa1567d3f5 add set client tag to webfront profile as button 2022-07-19 20:37:48 -05:00
f97e266c24 send correct type to inc/dec meta service in game interface 2022-07-16 17:47:07 -05:00
506b17dbb3 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2022-07-16 09:56:48 -05:00
bef8c08d90 misc performance graph display tweaks 2022-07-16 09:56:41 -05:00
b78c467539 tweaks and persistent guid update to game integration/interface 2022-07-16 09:32:07 -05:00
Edo
c3e042521a Improvements to game scripts (#253) 2022-07-16 08:40:10 -05:00
cb5f490d3b fix incorrect js bundle input source 2022-07-13 16:27:47 -05:00
0a55c54c42 update to game interface/integration for persistent stat data 2022-07-13 16:10:16 -05:00
f43f7b5040 misc webfront tweaks 2022-07-10 21:06:58 -05:00
540cf7489d update pluto t6 parser for unknown ip 2022-07-10 20:09:57 -05:00
1a72faee60 add date stamp to performance graphs / increase number of performance rating snapshots / localize graph timestamps 2022-07-10 17:06:46 -05:00
4e44bb5ea1 fix rcon issue on restart 2022-07-09 20:57:00 -05:00
9e17bcc38f improve ban management display and additional translations 2022-07-09 16:32:23 -05:00
4b33b33d01 fix issue with alert on warn in game interface 2022-07-09 14:23:08 -05:00
6f1bc7ab90 cleanup table display of admins on mobile display 2022-07-09 13:54:35 -05:00
63e1774cb6 gracefully handle when infoString does not include all expected data 2022-07-09 10:52:27 -05:00
61df873bb1 more localization tweaks 2022-07-08 20:40:27 -05:00
052eeb0615 fix tag on welcome issue 2022-07-08 20:39:58 -05:00
88e67747fe add option to normalize diacritics for rcon parsers (applied to T6) 2022-07-06 15:42:31 -05:00
5db94723aa Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2022-07-06 10:02:09 -05:00
ea8216ecdf Add H1 maps and gametypes (#252) 2022-07-06 10:01:01 -05:00
6abbcbe464 prevent waiting for response on quit command 2022-07-06 09:55:06 -05:00
57484690b6 clean up display and uniformity of social icons 2022-07-06 09:49:44 -05:00
7a022a1973 fix grouping of commands on help page 2022-07-05 15:57:39 -05:00
7108e23a03 fix issue with context menu close not working on mobile 2022-07-05 15:15:25 -05:00
77d25890da clean up some more translations 2022-07-05 12:42:17 -05:00
2fca68a7ea update webfront translation strings 2022-07-05 12:02:43 -05:00
a6c0a94f6c support per-command override of rcon timeouts / update t5 parser to reflect 2022-07-01 09:59:11 -05:00
71abaac9e1 remove reports on ban/tempban 2022-07-01 09:14:57 -05:00
e07651b931 fix toast message issue on pages with query params 2022-06-28 10:03:05 -05:00
5a2ee36df9 use "unknown" ip as bot indicator 2022-06-28 09:15:37 -05:00
2daa4991d1 fix issue with previous change 2022-06-21 16:57:06 -05:00
775c0a91b5 small parser changes 2022-06-21 16:33:11 -05:00
55bccc7d3d ensure commands are not displayed/usable for unsupported games 2022-06-17 13:11:44 -05:00
4322e8d882 add migration logic for MySQL case sensitivity 2022-06-17 09:44:14 -05:00
a92f9fc29c optimize client searching 2022-06-16 18:44:49 -05:00
fbf424c77d optimize chat filtering/searching 2022-06-16 18:03:23 -05:00
b8e001fcfe misc ui tweaks 2022-06-16 14:02:44 -05:00
5ab5b73ecf order report servers by most recent report 2022-06-16 10:11:01 -05:00
4534d24fe6 fix token auth issue 2022-06-16 10:07:03 -05:00
73c8d0da33 improve icon alignment for nav menu 2022-06-16 09:46:01 -05:00
16d75470b5 fix login persistence issue 2022-06-15 21:00:01 -05:00
f02552faa1 fix up query/check 2022-06-15 20:19:22 -05:00
a4923d03f9 hide token generation button for non-logged-in users 2022-06-15 19:39:53 -05:00
8ae6561f4e update schema to support unique guid + game combinations 2022-06-15 19:37:34 -05:00
deeb1dea87 set the rcon parser game name for retail WaW 2022-06-14 15:12:19 -05:00
9ab34614c5 don't publish disconnect event if no client id 2022-06-14 15:00:23 -05:00
2cff25d6b3 make alert menu scrollable for large # of alerts 2022-06-13 11:03:39 -05:00
df3e226dc9 actually fix the previous issue 2022-06-12 16:37:07 -05:00
ef3db63ba7 fix issue that shouldn't actually be an issue 2022-06-12 15:09:26 -05:00
49fe4520ff improve alert display for mobile 2022-06-12 12:20:08 -05:00
6587187a34 fix memory/database leak with ranked player count cache 2022-06-12 12:19:32 -05:00
b337e232a2 use bot ip address when determining if client is bot 2022-06-12 10:09:56 -05:00
a44b4e9475 add alert/notification functionality (for server connection events and messages) 2022-06-11 11:34:00 -05:00
ffb0e5cac1 update for t5 dvar format change 2022-06-11 09:56:28 -05:00
ecc2b5bf54 increase width of side context menu for longer server names 2022-06-09 13:59:00 -05:00
2ac9cc4379 fix bug with loading top stats for individual servers 2022-06-09 13:50:58 -05:00
215037095f remove extra parenthesis oops.. 2022-06-09 10:15:43 -05:00
5433d7d1d2 add total ranked client number for stats pages 2022-06-09 09:56:41 -05:00
0446fe1ec5 revert time out for status preventing server from entering unreachable state 2022-06-08 09:10:31 -05:00
cf2a00e5b3 add game to player profile and admins page 2022-06-07 21:58:32 -05:00
ab494a22cb add mwr to game list (h1) 2022-06-07 12:10:39 -05:00
b690579154 fix issue with meta event context after 1st page load 2022-06-05 16:35:39 -05:00
acc967e50a add ban management page 2022-06-05 16:27:56 -05:00
c493fbe13d add game badge to server overview 2022-06-04 09:58:30 -05:00
ee56a5db1f fix map/gametype alignment on server overview and add back ip display on connect click 2022-06-04 09:21:08 -05:00
165 changed files with 18476 additions and 1128 deletions

View 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;
}
}

View 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);
}
}
}

View File

@ -57,10 +57,11 @@ namespace IW4MAdmin.Application
private readonly List<MessageToken> MessageTokens; private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc; private readonly ClientService ClientSvc;
readonly PenaltyService PenaltySvc; readonly PenaltyService PenaltySvc;
private readonly IAlertManager _alertManager;
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler; public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList; readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0); 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 Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration; private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
@ -82,18 +83,19 @@ namespace IW4MAdmin.Application
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents, IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory, IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider, IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService) ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
{ {
MiddlewareActionHandler = actionHandler; MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>(); _servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>(); MessageTokens = new List<MessageToken>();
ClientSvc = clientService; ClientSvc = clientService;
PenaltySvc = penaltyService; PenaltySvc = penaltyService;
_alertManager = alertManager;
ConfigHandler = appConfigHandler; ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow; StartTime = DateTime.UtcNow;
PageList = new PageList(); PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, _appConfig) }; AdditionalEventParsers = new List<IEventParser> { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) }; AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication(); TokenAuthenticator = new TokenAuthentication();
_logger = logger; _logger = logger;
_tokenSource = new CancellationTokenSource(); _tokenSource = new CancellationTokenSource();
@ -508,6 +510,7 @@ namespace IW4MAdmin.Application
#endregion #endregion
_metaRegistration.Register(); _metaRegistration.Register();
await _alertManager.Initialize();
#region CUSTOM_EVENTS #region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events)) foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
@ -610,6 +613,7 @@ namespace IW4MAdmin.Application
{ {
IsRestartRequested = true; IsRestartRequested = true;
Stop().GetAwaiter().GetResult(); Stop().GetAwaiter().GetResult();
_tokenSource = new CancellationTokenSource();
} }
[Obsolete] [Obsolete]
@ -629,9 +633,9 @@ namespace IW4MAdmin.Application
return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList(); 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() GetActiveClients()
.FirstOrDefault(c => c.NetworkId == client.NetworkId) ?? client : .FirstOrDefault(c => c.NetworkId == client.NetworkId && c.GameName == client.GameName) ?? client :
client; client;
public ClientService GetClientService() public ClientService GetClientService()
@ -697,5 +701,6 @@ namespace IW4MAdmin.Application
} }
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName); public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
public IAlertManager AlertManager => _alertManager;
} }
} }

View 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"]);
}
}

View File

@ -1,11 +1,14 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client; using Data.Models.Client;
using Data.Models.Misc; using Data.Models.Misc;
using IW4MAdmin.Application.Alerts;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -16,19 +19,66 @@ namespace IW4MAdmin.Application.Commands
{ {
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IAlertManager _alertManager;
private const short MaxLength = 1024; private const short MaxLength = 1024;
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout, 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"; Name = "offlinemessage";
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"]; Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
Alias = "om"; Alias = "om";
Permission = EFClient.Permission.Moderator; Permission = EFClient.Permission.Moderator;
RequiresTarget = true; RequiresTarget = true;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _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) 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)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
return; return;
} }
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId) if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
{ {
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength)); gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
return; return;
} }
if (gameEvent.Target.IsIngame) 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; return;
} }
await using var context = _contextFactory.CreateContext(enableTracking: false); await using var context = _contextFactory.CreateContext(enableTracking: false);
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString()); var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
var newMessage = new EFInboxMessage() var newMessage = new EFInboxMessage
{ {
SourceClientId = gameEvent.Origin.ClientId, SourceClientId = gameEvent.Origin.ClientId,
DestinationClientId = gameEvent.Target.ClientId, DestinationClientId = gameEvent.Target.ClientId,
@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands
Message = gameEvent.Data, 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 try
{ {
context.Set<EFInboxMessage>().Add(newMessage); context.Set<EFInboxMessage>().Add(newMessage);
@ -75,4 +132,4 @@ namespace IW4MAdmin.Application.Commands
} }
} }
} }
} }

View File

@ -24,7 +24,7 @@ namespace IW4MAdmin.Application.Commands
Name = "readmessage"; Name = "readmessage";
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"]; Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
Alias = "rm"; Alias = "rm";
Permission = EFClient.Permission.Flagged; Permission = EFClient.Permission.User;
_contextFactory = contextFactory; _contextFactory = contextFactory;
_logger = logger; _logger = logger;
@ -76,4 +76,4 @@ namespace IW4MAdmin.Application.Commands
} }
} }
} }
} }

View File

@ -564,7 +564,56 @@
"Alias": "Momentum" "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": [ "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", "Game": "CSGO",
"Maps": [ "Maps": [

View File

@ -9,6 +9,7 @@ using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -24,8 +25,10 @@ using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient; using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models; using Data.Models;
using Data.Models.Server; using Data.Models.Server;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Commands; using IW4MAdmin.Application.Commands;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Alerts;
using static Data.Models.Client.EFClient; using static Data.Models.Client.EFClient;
namespace IW4MAdmin namespace IW4MAdmin
@ -73,7 +76,7 @@ namespace IW4MAdmin
{ {
ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber); 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 // first time client is connecting to server
if (client == null) if (client == null)
@ -116,7 +119,7 @@ namespace IW4MAdmin
public override async Task OnClientDisconnected(EFClient client) 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())) using (LogContext.PushProperty("Server", ToString()))
{ {
@ -152,10 +155,10 @@ namespace IW4MAdmin
{ {
if (E.IsBlocking) if (E.IsBlocking)
{ {
await E.Origin?.Lock(); await E.Origin.Lock();
} }
bool canExecuteCommand = true; var canExecuteCommand = true;
try try
{ {
@ -164,30 +167,30 @@ namespace IW4MAdmin
return; return;
} }
Command C = null; Command command = null;
if (E.Type == GameEvent.EventType.Command) if (E.Type == GameEvent.EventType.Command)
{ {
try 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) 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 }); new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId });
E.FailReason = GameEvent.EventFailReason.Invalid; E.FailReason = GameEvent.EventFailReason.Invalid;
} }
if (C != null) if (command != null)
{ {
E.Extra = C; E.Extra = command;
} }
} }
try try
{ {
var loginPlugin = Manager.Plugins.FirstOrDefault(_plugin => _plugin.Name == "Login"); var loginPlugin = Manager.Plugins.FirstOrDefault(plugin => plugin.Name == "Login");
if (loginPlugin != null) if (loginPlugin != null)
{ {
@ -202,15 +205,15 @@ namespace IW4MAdmin
} }
// hack: this prevents commands from getting executing that 'shouldn't' be // 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)) (canExecuteCommand || E.Origin?.Level == Permission.Console))
{ {
ServerLogger.LogInformation("Executing command {comamnd} for {client}", command.Name, E.Origin.ToString()); ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name, E.Origin.ToString());
await command.ExecuteAsync(E); await cmd.ExecuteAsync(E);
} }
var pluginTasks = Manager.Plugins var pluginTasks = Manager.Plugins
.Where(_plugin => _plugin.Name != "Login") .Where(plugin => plugin.Name != "Login")
.Select(async plugin => await CreatePluginTask(plugin, E)); .Select(async plugin => await CreatePluginTask(plugin, E));
await Task.WhenAll(pluginTasks); await Task.WhenAll(pluginTasks);
@ -306,8 +309,16 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{ {
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}")); 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; Throttled = true;
} }
@ -318,7 +329,15 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost) 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)) if (!string.IsNullOrEmpty(CustomSayName))
@ -355,9 +374,9 @@ namespace IW4MAdmin
var clientTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2, var clientTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
EFMeta.ClientTagNameV2, E.Origin.ClientId, Manager.CancellationToken); 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 try
@ -431,7 +450,7 @@ namespace IW4MAdmin
Clients[E.Origin.ClientNumber] = E.Origin; Clients[E.Origin.ClientNumber] = E.Origin;
try try
{ {
E.Origin.GameName = (Reference.Game?)GameName; E.Origin.GameName = (Reference.Game)GameName;
E.Origin = await OnClientConnected(E.Origin); E.Origin = await OnClientConnected(E.Origin);
E.Target = E.Origin; E.Target = E.Origin;
} }
@ -499,7 +518,7 @@ namespace IW4MAdmin
E.Target.SetLevel(Permission.User, E.Origin); E.Target.SetLevel(Permission.User, E.Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId, 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); await Manager.GetPenaltyService().Create(unflagPenalty);
} }
@ -671,23 +690,50 @@ namespace IW4MAdmin
else else
{ {
Gametype = dict["gametype"]; if (dict.ContainsKey("gametype"))
Hostname = dict["hostname"]; {
Gametype = dict["gametype"];
}
string mapname = dict["mapname"] ?? CurrentMap.Name; if (dict.ContainsKey("hostname"))
UpdateMap(mapname); {
Hostname = dict["hostname"];
}
var newMapName = dict.ContainsKey("mapname")
? dict["mapname"] ?? CurrentMap.Name
: CurrentMap.Name;
UpdateMap(newMapName);
} }
} }
else else
{ {
var dict = (Dictionary<string, string>) E.Extra; var dict = (Dictionary<string, string>)E.Extra;
Gametype = dict["g_gametype"]; if (dict.ContainsKey("g_gametype"))
Hostname = dict["sv_hostname"]; {
MaxClients = int.Parse(dict["sv_maxclients"]); Gametype = dict["g_gametype"];
}
string mapname = dict["mapname"]; if (dict.ContainsKey("sv_hostname"))
UpdateMap(mapname); {
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) if (E.GameTime.HasValue)
@ -723,6 +769,34 @@ namespace IW4MAdmin
{ {
E.Origin.UpdateTeam(E.Extra as string); 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) lock (ChatHistory)
{ {
@ -745,7 +819,7 @@ namespace IW4MAdmin
private async Task OnClientUpdate(EFClient origin) 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) if (client == null)
{ {
@ -790,12 +864,10 @@ namespace IW4MAdmin
/// array index 2 = updated clients /// array index 2 = updated clients
/// </summary> /// </summary>
/// <returns></returns> /// <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 currentClients = GetClientsAsList();
var statusResponse = await this.GetStatusAsync(tokenSource.Token); var statusResponse = await this.GetStatusAsync(token);
if (statusResponse is null) if (statusResponse is null)
{ {
@ -918,11 +990,11 @@ namespace IW4MAdmin
private DateTime _lastMessageSent = DateTime.Now; private DateTime _lastMessageSent = DateTime.Now;
private DateTime _lastPlayerCount = DateTime.Now; private DateTime _lastPlayerCount = DateTime.Now;
public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts) public override async Task<bool> ProcessUpdatesAsync(CancellationToken token)
{ {
try try
{ {
if (cts.IsCancellationRequested) if (token.IsCancellationRequested)
{ {
await ShutdownInternal(); await ShutdownInternal();
return true; return true;
@ -936,7 +1008,7 @@ namespace IW4MAdmin
return true; return true;
} }
var polledClients = await PollPlayersAsync(); var polledClients = await PollPlayersAsync(token);
if (polledClients is null) if (polledClients is null)
{ {
@ -947,7 +1019,7 @@ namespace IW4MAdmin
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */)) .Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
{ {
disconnectingClient.CurrentServer = this; disconnectingClient.CurrentServer = this;
var e = new GameEvent() var e = new GameEvent
{ {
Type = GameEvent.EventType.PreDisconnect, Type = GameEvent.EventType.PreDisconnect,
Origin = disconnectingClient, Origin = disconnectingClient,
@ -964,7 +1036,7 @@ namespace IW4MAdmin
!string.IsNullOrEmpty(client.Name) && (client.Ping != 999 || client.IsBot))) !string.IsNullOrEmpty(client.Name) && (client.Ping != 999 || client.IsBot)))
{ {
client.CurrentServer = this; client.CurrentServer = this;
client.GameName = (Reference.Game?)GameName; client.GameName = (Reference.Game)GameName;
var e = new GameEvent var e = new GameEvent
{ {
@ -1229,28 +1301,17 @@ namespace IW4MAdmin
this.GamePassword = gamePassword.Value; this.GamePassword = gamePassword.Value;
UpdateMap(mapname); UpdateMap(mapname);
if (RconParser.CanGenerateLogPath) if (RconParser.CanGenerateLogPath && string.IsNullOrEmpty(ServerConfig.ManualLogPath))
{ {
bool needsRestart = false;
if (logsync.Value == 0) if (logsync.Value == 0)
{ {
await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4 await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4
needsRestart = true;
} }
if (string.IsNullOrWhiteSpace(logfile.Value)) if (string.IsNullOrWhiteSpace(logfile.Value))
{ {
logfile.Value = "games_mp.log"; logfile.Value = "games_mp.log";
await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken); await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken);
needsRestart = true;
}
if (needsRestart)
{
// disabling this for the time being
/*Logger.WriteWarning("Game log file not properly initialized, restarting map...");
await this.ExecuteCommandAsync("map_restart");*/
} }
// this DVAR isn't set until the a map is loaded // this DVAR isn't set until the a map is loaded
@ -1456,6 +1517,11 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Creating tempban penalty for {TargetClient}", targetClient.ToString()); ServerLogger.LogDebug("Creating tempban penalty for {TargetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger); 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) if (activeClient.IsIngame)
{ {
@ -1486,6 +1552,11 @@ namespace IW4MAdmin
activeClient.SetLevel(Permission.Banned, originClient); activeClient.SetLevel(Permission.Banned, originClient);
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger); 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) if (activeClient.IsIngame)
{ {
ServerLogger.LogDebug("Attempting to kicking newly banned client {ActiveClient}", activeClient.ToString()); 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()); ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString());
targetClient.SetLevel(Permission.User, originClient); targetClient.SetLevel(Permission.User, originClient);
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId, await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
targetClient.NetworkId, targetClient.CurrentAlias?.IPAddress); targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unbanPenalty); await Manager.GetPenaltyService().Create(unbanPenalty);
} }

View File

@ -27,6 +27,7 @@ using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Helpers; using Data.Helpers;
using Integrations.Source.Extensions; using Integrations.Source.Extensions;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Extensions; using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Localization; using IW4MAdmin.Application.Localization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -448,6 +449,7 @@ namespace IW4MAdmin.Application
.AddSingleton<IServerDataCollector, ServerDataCollector>() .AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IEventPublisher, EventPublisher>() .AddSingleton<IEventPublisher, EventPublisher>()
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb"))) .AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>() .AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton(translationLookup) .AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig); .AddDatabaseContextOptions(appConfig);

View File

@ -33,7 +33,7 @@ namespace IW4MAdmin.Application.Misc
builder.Append(header); builder.Append(header);
builder.Append(config.NoticeLineSeparator); builder.Append(config.NoticeLineSeparator);
// build the reason // build the reason
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense); var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense.FormatMessageForEngine(config));
if (isNewLineSeparator) if (isNewLineSeparator)
{ {
@ -117,4 +117,4 @@ namespace IW4MAdmin.Application.Misc
return segments; return segments;
} }
} }
} }

View File

@ -10,6 +10,7 @@ namespace IW4MAdmin.Application.Misc
{ {
public event EventHandler<GameEvent> OnClientDisconnect; public event EventHandler<GameEvent> OnClientDisconnect;
public event EventHandler<GameEvent> OnClientConnect; public event EventHandler<GameEvent> OnClientConnect;
public event EventHandler<GameEvent> OnClientMetaUpdated;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -29,10 +30,15 @@ namespace IW4MAdmin.Application.Misc
OnClientConnect?.Invoke(this, gameEvent); OnClientConnect?.Invoke(this, gameEvent);
} }
if (gameEvent.Type == GameEvent.EventType.Disconnect) if (gameEvent.Type == GameEvent.EventType.Disconnect && gameEvent.Origin.ClientId != 0)
{ {
OnClientDisconnect?.Invoke(this, gameEvent); OnClientDisconnect?.Invoke(this, gameEvent);
} }
if (gameEvent.Type == GameEvent.EventType.MetaUpdated)
{
OnClientMetaUpdated?.Invoke(this, gameEvent);
}
} }
catch (Exception ex) catch (Exception ex)
@ -41,4 +47,4 @@ namespace IW4MAdmin.Application.Misc
} }
} }
} }
} }

View File

@ -7,7 +7,10 @@ using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models; using Data.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper; using SharedLibraryCore.QueryHelper;
@ -19,13 +22,15 @@ public class MetaServiceV2 : IMetaServiceV2
{ {
private readonly IDictionary<MetaType, List<dynamic>> _metaActions; private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger; private readonly ILogger _logger;
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory) public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory, IServiceProvider serviceProvider)
{ {
_logger = logger; _logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>(); _metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory; _contextFactory = contextFactory;
_serviceProvider = serviceProvider;
} }
public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId, public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId,
@ -64,6 +69,26 @@ public class MetaServiceV2 : IMetaServiceV2
} }
await context.SaveChangesAsync(token); 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, public async Task SetPersistentMetaValue<T>(string metaKey, T metaValue, int clientId,

View File

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

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client; using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Server; using Data.Models.Server;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -22,18 +23,20 @@ namespace IW4MAdmin.Application.Misc
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache; private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache; private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache; private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
private readonly TimeSpan? _cacheTimeSpan = private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10); Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache, public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache, IDataValueCache<EFClient, (int, int)> serverStatsCache,
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache) IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
{ {
_logger = logger; _logger = logger;
_snapshotCache = snapshotCache; _snapshotCache = snapshotCache;
_serverStatsCache = serverStatsCache; _serverStatsCache = serverStatsCache;
_clientHistoryCache = clientHistoryCache; _clientHistoryCache = clientHistoryCache;
_rankedClientsCache = rankedClientsCache;
} }
public async Task<(int?, DateTime?)> public async Task<(int?, DateTime?)>
@ -160,5 +163,30 @@ namespace IW4MAdmin.Application.Misc
return Enumerable.Empty<ClientHistoryInfo>(); 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;
}
}
} }
} }

View File

@ -9,40 +9,41 @@ namespace IW4MAdmin.Application.Misc
{ {
internal class TokenAuthentication : ITokenAuthentication internal class TokenAuthentication : ITokenAuthentication
{ {
private readonly ConcurrentDictionary<long, TokenState> _tokens; private readonly ConcurrentDictionary<int, TokenState> _tokens;
private readonly RandomNumberGenerator _random; 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; private const short TokenLength = 4;
public TokenAuthentication() public TokenAuthentication()
{ {
_tokens = new ConcurrentDictionary<long, TokenState>(); _tokens = new ConcurrentDictionary<int, TokenState>();
_random = RandomNumberGenerator.Create(); _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) if (authorizeSuccessful)
{ {
_tokens.TryRemove(networkId, out _); _tokens.TryRemove(authInfo.ClientId, out _);
} }
return authorizeSuccessful; return authorizeSuccessful;
} }
public TokenState GenerateNextToken(long networkId) public TokenState GenerateNextToken(ITokenIdentifier authInfo)
{ {
TokenState state; 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 else
@ -53,17 +54,16 @@ namespace IW4MAdmin.Application.Misc
state = new TokenState state = new TokenState
{ {
NetworkId = networkId,
Token = _generateToken(), Token = _generateToken(),
TokenDuration = TimeoutPeriod 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 // perform some housekeeping so we don't have built up tokens if they're not ever used
foreach (var (key, value) in _tokens) foreach (var (key, value) in _tokens)
{ {
if ((DateTime.Now - value.RequestTime) > TimeoutPeriod) if (DateTime.Now - value.RequestTime > TimeoutPeriod)
{ {
_tokens.TryRemove(key, out _); _tokens.TryRemove(key, out _);
} }

View File

@ -20,6 +20,7 @@ namespace IW4MAdmin.Application.RConParsers
public class BaseRConParser : IRConParser public class BaseRConParser : IRConParser
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private static string _botIpIndicator = "00000000.";
public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory) 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.RConName, 5);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7); 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.RConDvarName, 1);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2); Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3); 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) 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); var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token);
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray(); return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
} }
@ -104,7 +105,7 @@ namespace IW4MAdmin.Application.RConParsers
lineSplit = Array.Empty<string>(); 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); var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (response.Contains("Unknown command") || if (response.Contains("Unknown command") ||
@ -290,8 +291,15 @@ namespace IW4MAdmin.Application.RConParsers
long networkId; long networkId;
var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine(); var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString; string networkIdString;
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP(); 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 try
{ {
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]]; networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
@ -306,9 +314,9 @@ namespace IW4MAdmin.Application.RConParsers
continue; continue;
} }
var client = new EFClient() var client = new EFClient
{ {
CurrentAlias = new EFAlias() CurrentAlias = new EFAlias
{ {
Name = name, Name = name,
IPAddress = ip IPAddress = ip
@ -360,15 +368,28 @@ namespace IW4MAdmin.Application.RConParsers
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) : (T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
default; default;
public TimeSpan OverrideTimeoutForCommand(string command) public TimeSpan? OverrideTimeoutForCommand(string command)
{ {
if (command.Contains("map_rotate", StringComparison.InvariantCultureIgnoreCase) || if (string.IsNullOrEmpty(command))
command.StartsWith("map ", StringComparison.InvariantCultureIgnoreCase))
{ {
return TimeSpan.FromSeconds(30); return TimeSpan.Zero;
}
var commandToken = command.Split(' ', StringSplitOptions.RemoveEmptyEntries).First().ToLower();
if (!Configuration.OverrideCommandTimeouts.ContainsKey(commandToken))
{
return TimeSpan.Zero;
} }
return TimeSpan.Zero; var timeoutValue = Configuration.OverrideCommandTimeouts[commandToken];
if (timeoutValue.HasValue && timeoutValue.Value != 0) // JINT doesn't seem to be able to properly set nulls on dictionaries
{
return TimeSpan.FromSeconds(timeoutValue.Value);
}
return null;
} }
} }
} }

View File

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

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -9,6 +10,11 @@ namespace Data.Abstractions
{ {
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName, void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
TimeSpan? expirationTime = null, bool autoRefresh = false); 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, CancellationToken token = default);
Task<TReturnType> GetCacheItem(string keyName, object id = null, CancellationToken token = default);
} }
} }

View File

@ -85,7 +85,15 @@ namespace Data.Context
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
// make network id unique // 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 => modelBuilder.Entity<EFPenalty>(entity =>
{ {

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
@ -15,8 +17,8 @@ namespace Data.Helpers
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory; private readonly IDatabaseContextFactory _contextFactory;
private readonly ConcurrentDictionary<string, CacheState<TReturnType>> _cacheStates = private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
new ConcurrentDictionary<string, CacheState<TReturnType>>(); private readonly object _defaultKey = new();
private bool _autoRefresh; private bool _autoRefresh;
private const int DefaultExpireMinutes = 15; private const int DefaultExpireMinutes = 15;
@ -51,41 +53,61 @@ namespace Data.Helpers
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key, public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
TimeSpan? expirationTime = null, bool autoRefresh = false) TimeSpan? expirationTime = null, bool autoRefresh = false)
{ {
if (_cacheStates.ContainsKey(key)) SetCacheItem(getter, key, null, expirationTime, autoRefresh);
{
_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();
} }
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)) if (!_cacheStates.ContainsKey(keyName))
{ {
throw new ArgumentException("No cache found for key {key}", 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 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 // 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
} }
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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;");
}
}
}

View File

@ -64,7 +64,7 @@ namespace Data.Migrations.MySql
b.Property<DateTime>("FirstConnection") b.Property<DateTime>("FirstConnection")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<int?>("GameName") b.Property<int>("GameName")
.HasColumnType("int"); .HasColumnType("int");
b.Property<DateTime>("LastConnection") b.Property<DateTime>("LastConnection")
@ -90,12 +90,13 @@ namespace Data.Migrations.MySql
b.HasKey("ClientId"); b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId"); b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId"); b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId") b.HasIndex("NetworkId");
.IsUnique();
b.ToTable("EFClients", (string)null); b.ToTable("EFClients", (string)null);
}); });
@ -456,6 +457,8 @@ namespace Data.Migrations.MySql
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking"); b.HasIndex("Ranking");
b.HasIndex("ServerId"); b.HasIndex("ServerId");

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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""");
}
}
}

View File

@ -71,7 +71,7 @@ namespace Data.Migrations.Postgresql
b.Property<DateTime>("FirstConnection") b.Property<DateTime>("FirstConnection")
.HasColumnType("timestamp without time zone"); .HasColumnType("timestamp without time zone");
b.Property<int?>("GameName") b.Property<int>("GameName")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<DateTime>("LastConnection") b.Property<DateTime>("LastConnection")
@ -97,12 +97,13 @@ namespace Data.Migrations.Postgresql
b.HasKey("ClientId"); b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId"); b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId"); b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId") b.HasIndex("NetworkId");
.IsUnique();
b.ToTable("EFClients", (string)null); b.ToTable("EFClients", (string)null);
}); });
@ -475,6 +476,8 @@ namespace Data.Migrations.Postgresql
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking"); b.HasIndex("Ranking");
b.HasIndex("ServerId"); b.HasIndex("ServerId");

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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)
{
}
}
}

View File

@ -62,7 +62,7 @@ namespace Data.Migrations.Sqlite
b.Property<DateTime>("FirstConnection") b.Property<DateTime>("FirstConnection")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("GameName") b.Property<int>("GameName")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<DateTime>("LastConnection") b.Property<DateTime>("LastConnection")
@ -88,12 +88,13 @@ namespace Data.Migrations.Sqlite
b.HasKey("ClientId"); b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId"); b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId"); b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId") b.HasIndex("NetworkId");
.IsUnique();
b.ToTable("EFClients", (string)null); b.ToTable("EFClients", (string)null);
}); });
@ -454,6 +455,8 @@ namespace Data.Migrations.Sqlite
b.HasIndex("ClientId"); b.HasIndex("ClientId");
b.HasIndex("CreatedDateTime");
b.HasIndex("Ranking"); b.HasIndex("Ranking");
b.HasIndex("ServerId"); b.HasIndex("ServerId");

View File

@ -63,7 +63,7 @@ namespace Data.Models.Client
public DateTime FirstConnection { get; set; } public DateTime FirstConnection { get; set; }
[Required] [Required]
public DateTime LastConnection { get; set; } 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; } public bool Masked { get; set; }
[Required] [Required]
public int AliasLinkId { get; set; } public int AliasLinkId { get; set; }

View File

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

View File

@ -86,7 +86,8 @@ namespace Data.Models.Configuration
entity.HasIndex(ranking => ranking.Ranking); entity.HasIndex(ranking => ranking.Ranking);
entity.HasIndex(ranking => ranking.ZScore); entity.HasIndex(ranking => ranking.ZScore);
entity.HasIndex(ranking => ranking.UpdatedDateTime); entity.HasIndex(ranking => ranking.UpdatedDateTime);
entity.HasIndex(ranking => ranking.CreatedDateTime);
}); });
} }
} }
} }

View File

@ -15,7 +15,8 @@
T6 = 7, T6 = 7,
T7 = 8, T7 = 8,
SHG1 = 9, SHG1 = 9,
CSGO = 10 CSGO = 10,
H1 = 11
} }
public enum ConnectionType public enum ConnectionType
@ -24,4 +25,4 @@
Disconnect Disconnect
} }
} }
} }

View File

@ -29,8 +29,9 @@ onPlayerConnect( player )
for( ;; ) for( ;; )
{ {
level waittill( "connected", player ); level waittill( "connected", player );
player setClientDvar("cl_autorecord", 1); player setClientDvars( "cl_autorecord", 1,
player setClientDvar("cl_demosKeep", 200); "cl_demosKeep", 200 );
player thread waitForFrameThread(); player thread waitForFrameThread();
player thread waitForAttack(); player thread waitForAttack();
} }
@ -60,7 +61,7 @@ getHttpString( url )
runRadarUpdates() runRadarUpdates()
{ {
interval = int(getDvar("sv_printradar_updateinterval")); interval = getDvarInt( "sv_printradar_updateinterval", 500 );
for ( ;; ) for ( ;; )
{ {
@ -191,7 +192,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
i++; i++;
} }
lastAttack = int(getTime()) - int(self.lastAttackTime); lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self); isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" ); logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );

View File

@ -53,7 +53,7 @@ waitForAttack()
runRadarUpdates() runRadarUpdates()
{ {
interval = int(getDvar("sv_printradar_updateinterval")); interval = getDvarInt( "sv_printradar_updateinterval", 500 );
for ( ;; ) for ( ;; )
{ {
@ -183,7 +183,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
i++; i++;
} }
lastAttack = int(getTime()) - int(self.lastAttackTime); lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self); isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" ); logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );

View File

@ -60,7 +60,7 @@ waitForAttack()
runRadarUpdates() runRadarUpdates()
{ {
interval = int(getDvar("sv_printradar_updateinterval")); interval = getDvarInt( "sv_printradar_updateinterval" );
for ( ;; ) for ( ;; )
{ {
@ -190,7 +190,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
i++; i++;
} }
lastAttack = int(getTime()) - int(self.lastAttackTime); lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self); isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" ); logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );

View File

@ -53,15 +53,13 @@ init()
level thread OnPlayerConnect(); level thread OnPlayerConnect();
} }
////////////////////////////////// //////////////////////////////////
// Client Methods // Client Methods
////////////////////////////////// //////////////////////////////////
OnPlayerConnect() OnPlayerConnect()
{ {
level endon ( "disconnect" ); level endon ( "game_ended" );
for ( ;; ) for ( ;; )
{ {
@ -69,7 +67,7 @@ OnPlayerConnect()
level.iw4adminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" ); 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 // we don't want to track bots
continue; continue;
@ -106,7 +104,7 @@ OnPlayerSpawned()
OnPlayerDisconnect() OnPlayerDisconnect()
{ {
level endon ( "disconnect" ); self endon ( "disconnect" );
for ( ;; ) for ( ;; )
{ {
@ -141,8 +139,6 @@ OnPlayerJoinedSpectators()
OnGameEnded() OnGameEnded()
{ {
level endon ( "disconnect" );
for ( ;; ) for ( ;; )
{ {
level waittill( "game_ended" ); level waittill( "game_ended" );
@ -167,6 +163,33 @@ DisplayWelcomeData()
self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection ); 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() PlayerConnectEvents()
{ {
self endon( "disconnect" ); self endon( "disconnect" );
@ -208,8 +231,7 @@ PlayerTrackingOnInterval()
MonitorClientEvents() MonitorClientEvents()
{ {
level endon( "disconnect" ); level endon( "game_ended" );
self endon( "disconnect" );
for ( ;; ) for ( ;; )
{ {
@ -304,6 +326,107 @@ DecrementClientMeta( metaKey, decrementValue, clientId )
SetClientMeta( metaKey, decrementValue, clientId, "decrement" ); SetClientMeta( metaKey, decrementValue, clientId, "decrement" );
} }
SplitGuid()
{
guid = self GetGuid();
if ( isDefined( self.guid ) )
{
guid = self.guid;
}
firstPart = 0;
secondPart = 0;
stringLength = 17;
firstPartExp = 0;
secondPartExp = 0;
for ( i = stringLength - 1; i > 0; i-- )
{
char = GetSubStr( guid, i - 1, i );
if ( char == "" )
{
char = "0";
}
if ( i > stringLength / 2 )
{
value = GetIntForHexChar( char );
power = Pow( 16, secondPartExp );
secondPart = secondPart + ( value * power );
secondPartExp++;
}
else
{
value = GetIntForHexChar( char );
power = Pow( 16, firstPartExp );
firstPart = firstPart + ( value * power );
firstPartExp++;
}
}
split = [];
split["low"] = int( secondPart );
split["high"] = int( firstPart );
return split;
}
Pow( num, exponent )
{
result = 1;
while( exponent != 0 )
{
result = result * num;
exponent--;
}
return result;
}
GetIntForHexChar( char )
{
char = ToLower( char );
// generated by co-pilot because I can't be bothered to make it more "elegant"
switch( char )
{
case "0":
return 0;
case "1":
return 1;
case "2":
return 2;
case "3":
return 3;
case "4":
return 4;
case "5":
return 5;
case "6":
return 6;
case "7":
return 7;
case "8":
return 8;
case "9":
return 9;
case "a":
return 10;
case "b":
return 11;
case "c":
return 12;
case "d":
return 13;
case "e":
return 14;
case "f":
return 15;
default:
return 0;
}
}
GenerateJoinTeamString( isSpectator ) GenerateJoinTeamString( isSpectator )
{ {
team = self.team; team = self.team;
@ -456,7 +579,7 @@ MonitorBus()
QueueEvent( request, eventType, notifyEntity ) QueueEvent( request, eventType, notifyEntity )
{ {
level endon( "disconnect" ); level endon( "game_ended" );
start = GetTime(); start = GetTime();
maxWait = level.eventBus.timeout * 1000; // 30 seconds maxWait = level.eventBus.timeout * 1000; // 30 seconds
@ -490,6 +613,8 @@ QueueEvent( request, eventType, notifyEntity )
{ {
notifyEntity NotifyClientEventTimeout( eventType ); notifyEntity NotifyClientEventTimeout( eventType );
} }
SetDvar( level.eventBus.inVar, "" );
return; return;
} }
@ -643,6 +768,7 @@ OnClientDataReceived( event )
self.persistentClientId = event.data["clientId"]; self.persistentClientId = event.data["clientId"];
self thread DisplayWelcomeData(); self thread DisplayWelcomeData();
self setPersistentData();
} }
OnExecuteCommand( event ) OnExecuteCommand( event )
@ -902,7 +1028,7 @@ GotoCoordImpl( data )
return; 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 SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" ); self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
} }

View File

@ -253,8 +253,10 @@ namespace Integrations.Cod
try try
{ {
connectionState.LastQuery = DateTime.Now; connectionState.LastQuery = DateTime.Now;
var timeout = _parser.OverrideTimeoutForCommand(parameters);
waitForResponse = waitForResponse && timeout.HasValue;
response = await SendPayloadAsync(payload, waitForResponse, response = await SendPayloadAsync(payload, waitForResponse,
_parser.OverrideTimeoutForCommand(parameters), token); timeout ?? TimeSpan.Zero, token);
if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse) if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse)
{ {
@ -456,6 +458,12 @@ namespace Integrations.Cod
connectionState.SendEventArgs.DisconnectReuseSocket = true; 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); connectionState.SendEventArgs.SetBuffer(payload);
// send the data to the server // send the data to the server

View File

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

View File

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

View File

@ -4,6 +4,7 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using System.Threading.Tasks; using System.Threading.Tasks;
using SharedLibraryCore.Helpers;
namespace IW4MAdmin.Plugins.Login.Commands namespace IW4MAdmin.Plugins.Login.Commands
{ {
@ -18,7 +19,7 @@ namespace IW4MAdmin.Plugins.Login.Commands
RequiresTarget = false; RequiresTarget = false;
Arguments = new CommandArgument[] Arguments = new CommandArgument[]
{ {
new CommandArgument() new()
{ {
Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"], Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
Required = true 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) if (!success)
{ {
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, E.Origin.PasswordSalt)); var hashedPassword = await Task.FromResult(Hashing.Hash(gameEvent.Data, gameEvent.Origin.PasswordSalt));
success = hashedPassword[0] == E.Origin.Password; success = hashedPassword[0] == gameEvent.Origin.Password;
} }
if (success) if (success)
{ {
Plugin.AuthorizedClients[E.Origin.ClientId] = true; Plugin.AuthorizedClients[gameEvent.Origin.ClientId] = true;
} }
_ = success ? _ = success ?
E.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) : gameEvent.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_FAIL"]);
} }
} }
} }

View File

@ -19,7 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

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

View File

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

View File

@ -20,7 +20,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"'; rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick {0} "{1}"'; rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n'; 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.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.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
rconParser.Configuration.WaitForResponse = false; rconParser.Configuration.WaitForResponse = false;

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = { var plugin = {
author: 'RaidMax, Xerxes', author: 'RaidMax, Xerxes',
version: 1.2, version: 1.4,
name: 'Plutonium T6 Parser', name: 'Plutonium T6 Parser',
isParser: true, isParser: true,
@ -29,9 +29,10 @@ var plugin = {
rconParser.Configuration.NoticeLineSeparator = '. '; rconParser.Configuration.NoticeLineSeparator = '. ';
rconParser.Configuration.DefaultRConPort = 4976; rconParser.Configuration.DefaultRConPort = 4976;
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t6'; 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.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(100, 1);
rconParser.Configuration.Status.AddMapping(101, 2); rconParser.Configuration.Status.AddMapping(101, 2);
rconParser.Configuration.Status.AddMapping(102, 3); rconParser.Configuration.Status.AddMapping(102, 3);

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = { var plugin = {
author: 'RaidMax', author: 'RaidMax',
version: 0.1, version: 0.2,
name: 'Plutonium T5 Parser', name: 'Plutonium T5 Parser',
isParser: true, isParser: true,
@ -16,13 +16,18 @@ var plugin = {
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t5'; rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t5';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n'; 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.Tell = 'tell {0} {1}';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined; rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
rconParser.Configuration.GuidNumberStyle = 7; // Integer rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.DefaultRConPort = 3074; rconParser.Configuration.DefaultRConPort = 3074;
rconParser.Configuration.CanGenerateLogPath = false; 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.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 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'; eventParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';

View File

@ -17,7 +17,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'kickClient {0} "{1}"'; rconParser.Configuration.CommandPrefixes.Ban = 'kickClient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'kickClient {0} "{1}"'; rconParser.Configuration.CommandPrefixes.TempBan = 'kickClient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint'; 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.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.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
rconParser.Configuration.Status.AddMapping(102, 4); rconParser.Configuration.Status.AddMapping(102, 4);
@ -37,4 +37,4 @@ var plugin = {
onUnloadAsync: function() {}, onUnloadAsync: function() {},
onTickAsync: function(server) {} onTickAsync: function(server) {}
}; };

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = { var plugin = {
author: 'RaidMax', author: 'RaidMax',
version: 0.2, version: 0.3,
name: 'Call of Duty 5: World at War Parser', name: 'Call of Duty 5: World at War Parser',
isParser: true, isParser: true,
@ -17,6 +17,7 @@ var plugin = {
rconParser.Configuration.GuidNumberStyle = 7; // Integer rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.DefaultRConPort = 28960; 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.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.Configuration.GuidNumberStyle = 7; // Integer
eventParser.GameName = 5; // T4 eventParser.GameName = 5; // T4

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models.Client; using Data.Models.Client;
@ -88,8 +89,8 @@ namespace Stats.Client
return zScore ?? 0; return zScore ?? 0;
}, MaxZScoreCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30)); }, MaxZScoreCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30));
await _distributionCache.GetCacheItem(DistributionCacheKey); await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey); await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
/*foreach (var serverId in _serverIds) /*foreach (var serverId in _serverIds)
{ {
@ -132,7 +133,7 @@ namespace Stats.Client
public async Task<double> GetZScoreForServer(long serverId, double value) 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)) if (!serverParams.ContainsKey(serverId))
{ {
return 0.0; return 0.0;
@ -150,7 +151,7 @@ namespace Stats.Client
public async Task<double?> GetRatingForZScore(double? value) 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); return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore);
} }
} }

View File

@ -79,7 +79,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
} }
else else
{ {
gameEvent.Owner.Broadcast(topStats); await gameEvent.Owner.BroadcastAsync(topStats);
} }
} }
} }

View File

@ -14,6 +14,7 @@ namespace Stats.Dtos
public EFClient.Permission Level { get; set; } public EFClient.Permission Level { get; set; }
public double? Performance { get; set; } public double? Performance { get; set; }
public int? Ranking { get; set; } public int? Ranking { get; set; }
public int TotalRankedClients { get; set; }
public double? ZScore { get; set; } public double? ZScore { get; set; }
public double? Rating { get; set; } public double? Rating { get; set; }
public List<ServerInfo> Servers { get; set; } public List<ServerInfo> Servers { get; set; }
@ -25,4 +26,4 @@ namespace Stats.Dtos
public List<EFClientRankingHistory> Ratings { get; set; } public List<EFClientRankingHistory> Ratings { get; set; }
public List<EFClientStatistics> LegacyStats { get; set; } public List<EFClientStatistics> LegacyStats { get; set; }
} }
} }

View File

@ -23,7 +23,7 @@ namespace Stats.Dtos
/// <summary> /// <summary>
/// only look for messages sent after this date /// only look for messages sent after this date
/// </summary> /// </summary>
public DateTime SentAfter { get; set; } = DateTime.UtcNow.AddYears(-100); public DateTime? SentAfter { get; set; }
/// <summary> /// <summary>
/// only look for messages sent before this date0 /// only look for messages sent before this date0

View File

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

View File

@ -3,6 +3,7 @@ using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Models;
using Data.Models.Client; using Data.Models.Client;
using Data.Models.Client.Stats; using Data.Models.Client.Stats;
using IW4MAdmin.Plugins.Stats; using IW4MAdmin.Plugins.Stats;
@ -12,7 +13,6 @@ using Microsoft.Extensions.Logging;
using SharedLibraryCore.Dtos; using SharedLibraryCore.Dtos;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
using Stats.Client.Abstractions;
using Stats.Dtos; using Stats.Dtos;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -50,7 +50,8 @@ namespace Stats.Helpers
{ {
client.ClientId, client.ClientId,
client.CurrentAlias.Name, client.CurrentAlias.Name,
client.Level client.Level,
client.GameName
}).FirstOrDefaultAsync(client => client.ClientId == query.ClientId); }).FirstOrDefaultAsync(client => client.ClientId == query.ClientId);
if (clientInfo == null) if (clientInfo == null)
@ -77,7 +78,8 @@ namespace Stats.Helpers
.Where(r => r.ClientId == clientInfo.ClientId) .Where(r => r.ClientId == clientInfo.ClientId)
.Where(r => r.ServerId == serverId) .Where(r => r.ServerId == serverId)
.Where(r => r.Ranking != null) .Where(r => r.Ranking != null)
.OrderByDescending(r => r.UpdatedDateTime) .OrderByDescending(r => r.CreatedDateTime)
.Take(250)
.ToListAsync(); .ToListAsync();
var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest); var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest);
@ -111,8 +113,9 @@ namespace Stats.Helpers
Rating = mostRecentRanking?.PerformanceMetric, Rating = mostRecentRanking?.PerformanceMetric,
All = hitStats, All = hitStats,
Servers = _manager.GetServers() Servers = _manager.GetServers()
.Select(server => new ServerInfo() .Select(server => new ServerInfo
{Name = server.Hostname, IPAddress = server.IP, Port = server.Port}) {Name = server.Hostname, IPAddress = server.IP, Port = server.Port, Game = (Reference.Game)server.GameName})
.Where(server => server.Game == clientInfo.GameName)
.ToList(), .ToList(),
Aggregate = hitStats.FirstOrDefault(hit => Aggregate = hitStats.FirstOrDefault(hit =>
hit.HitLocationId == null && hit.ServerId == serverId && hit.WeaponId == null && hit.HitLocationId == null && hit.ServerId == serverId && hit.WeaponId == null &&
@ -153,4 +156,4 @@ namespace Stats.Helpers
&& (zScore == null || stats.ZScore > zScore); && (zScore == null || stats.ZScore > zScore);
} }
} }
} }

View File

@ -45,58 +45,53 @@ namespace Stats.Helpers
var result = new ResourceQueryHelperResult<MessageResponse>(); var result = new ResourceQueryHelperResult<MessageResponse>();
await using var context = _contextFactory.CreateContext(enableTracking: false); 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>() 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)) 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 var iqResponse = iqMessages
.Select(_message => new MessageResponse .Select(message => new MessageResponse
{ {
ClientId = _message.ClientId, ClientId = message.ClientId,
ClientName = query.IsProfileMeta ? "" : _message.Client.CurrentAlias.Name, ClientName = query.IsProfileMeta ? "" : message.Client.CurrentAlias.Name,
ServerId = _message.ServerId, ServerId = message.ServerId,
When = _message.TimeSent, When = message.TimeSent,
Message = _message.Message, Message = message.Message,
ServerName = query.IsProfileMeta ? "" : _message.Server.HostName, ServerName = query.IsProfileMeta ? "" : message.Server.HostName,
GameName = _message.Server.GameName == null ? Server.Game.IW4 : (Server.Game)_message.Server.GameName.Value, GameName = message.Server.GameName == null ? Server.Game.IW4 : (Server.Game)message.Server.GameName.Value,
SentIngame = _message.SentIngame SentIngame = message.SentIngame
}); });
if (query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending) iqResponse = query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending
{ ? iqResponse.OrderByDescending(message => message.When)
iqResponse = iqResponse.OrderByDescending(_message => _message.When); : iqResponse.OrderBy(message => message.When);
}
else
{
iqResponse = iqResponse.OrderBy(_message => _message.When);
}
var resultList = await iqResponse var resultList = await iqResponse
.Skip(query.Offset) .Skip(query.Offset)
.Take(query.Count) .Take(query.Count)
@ -115,13 +110,13 @@ namespace Stats.Helpers
{ {
var quickMessages = _defaultSettings var quickMessages = _defaultSettings
.QuickMessages .QuickMessages
.First(_qm => _qm.Game == message.GameName); .First(qm => qm.Game == message.GameName);
message.Message = quickMessages.Messages[message.Message.Substring(1)]; message.Message = quickMessages.Messages[message.Message.Substring(1)];
message.IsQuickMessage = true; message.IsQuickMessage = true;
} }
catch catch
{ {
message.Message = message.Message.Substring(1); message.Message = message.Message[1..];
} }
} }

View File

@ -86,7 +86,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public async Task<int> GetClientOverallRanking(int clientId, long? serverId = null) public async Task<int> GetClientOverallRanking(int clientId, long? serverId = null)
{ {
await using var context = _contextFactory.CreateContext(enableTracking: false); await using var context = _contextFactory.CreateContext(enableTracking: false);
if (_config.EnableAdvancedMetrics) if (_config.EnableAdvancedMetrics)
{ {
var clientRanking = await context.Set<EFClientRankingHistory>() var clientRanking = await context.Set<EFClientRankingHistory>()
@ -117,7 +117,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return 0; return 0;
} }
public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null, long? serverId = null) public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null,
long? serverId = null)
{ {
return (ranking) => ranking.ServerId == serverId return (ranking) => ranking.ServerId == serverId
&& ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned && ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned
@ -138,6 +139,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.CountAsync(); .CountAsync();
} }
public class RankingSnapshot
{
public int ClientId { get; set; }
public string Name { get; set; }
public DateTime LastConnection { get; set; }
public double? PerformanceMetric { get; set; }
public double? ZScore { get; set; }
public int? Ranking { get; set; }
public DateTime CreatedDateTime { get; set; }
}
public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null) public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null)
{ {
await using var context = _contextFactory.CreateContext(false); await using var context = _contextFactory.CreateContext(false);
@ -150,24 +162,38 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.Take(count) .Take(count)
.ToListAsync(); .ToListAsync();
var rankings = await context.Set<EFClientRankingHistory>() var rankingsDict = new Dictionary<int, List<RankingSnapshot>>();
.Where(ranking => clientIdsList.Contains(ranking.ClientId))
.Where(ranking => ranking.ServerId == serverId) foreach (var clientId in clientIdsList)
.Select(ranking => new {
{ var eachRank = await context.Set<EFClientRankingHistory>()
ranking.ClientId, .Where(ranking => ranking.ClientId == clientId)
ranking.Client.CurrentAlias.Name, .Where(ranking => ranking.ServerId == serverId)
ranking.Client.LastConnection, .OrderByDescending(ranking => ranking.CreatedDateTime)
ranking.PerformanceMetric, .Select(ranking => new RankingSnapshot
ranking.ZScore, {
ranking.Ranking, ClientId = ranking.ClientId,
ranking.CreatedDateTime Name = ranking.Client.CurrentAlias.Name,
}) LastConnection = ranking.Client.LastConnection,
.ToListAsync(); 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>() var statsInfo = await context.Set<EFClientStatistics>()
.Where(stat => clientIdsList.Contains(stat.ClientId)) .Where(stat => clientIdsList.Contains(stat.ClientId))
.Where(stat => stat.TimePlayed > 0) .Where(stat => stat.TimePlayed > 0)
@ -179,7 +205,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ClientId = s.Key, ClientId = s.Key,
Kills = s.Sum(c => c.Kills), Kills = s.Sum(c => c.Kills),
Deaths = s.Sum(c => c.Deaths), 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), s.Sum(c => c.TimePlayed),
TotalTimePlayed = s.Sum(c => c.TimePlayed), TotalTimePlayed = s.Sum(c => c.TimePlayed),
UpdatedAt = s.Max(c => c.UpdatedAt) UpdatedAt = s.Max(c => c.UpdatedAt)
@ -187,30 +213,32 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.ToListAsync(); .ToListAsync();
var finished = statsInfo var finished = statsInfo
.OrderByDescending(stat => rankingsDict[stat.ClientId].Last().PerformanceMetric) .OrderByDescending(stat => rankingsDict[stat.ClientId].First().PerformanceMetric)
.Select((s, index) => new TopStatsInfo() .Select((s, index) => new TopStatsInfo
{ {
ClientId = s.ClientId, ClientId = s.ClientId,
Id = (int?) serverId ?? 0, Id = (int?)serverId ?? 0,
Deaths = s.Deaths, Deaths = s.Deaths,
Kills = s.Kills, Kills = s.Kills,
KDR = Math.Round(s.KDR, 2), KDR = Math.Round(s.KDR, 2),
LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection)) LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].First().LastConnection))
.HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false), .HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false),
LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection), LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].First().LastConnection),
Name = rankingsDict[s.ClientId].First().Name, Name = rankingsDict[s.ClientId].First().Name,
Performance = Math.Round(rankingsDict[s.ClientId].Last().PerformanceMetric ?? 0, 2), Performance = Math.Round(rankingsDict[s.ClientId].First().PerformanceMetric ?? 0, 2),
RatingChange = (rankingsDict[s.ClientId].First().Ranking - RatingChange = (rankingsDict[s.ClientId].Last().Ranking -
rankingsDict[s.ClientId].Last().Ranking) ?? 0, rankingsDict[s.ClientId].First().Ranking) ?? 0,
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => ranking.PerformanceMetric ?? 0).ToList(), PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => new PerformanceHistory
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"), { Performance = ranking.PerformanceMetric ?? 0, OccurredAt = ranking.CreatedDateTime })
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed), .ToList(),
Ranking = index + start + 1, TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
ZScore = rankingsDict[s.ClientId].Last().ZScore, TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed),
ServerId = serverId Ranking = index + start + 1,
}) ZScore = rankingsDict[s.ClientId].First().ZScore,
.OrderBy(r => r.Ranking) ServerId = serverId
.ToList(); })
.OrderBy(r => r.Ranking)
.ToList();
return finished; return finished;
} }
@ -221,7 +249,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{ {
return await GetNewTopStats(start, count, serverId); return await GetNewTopStats(start, count, serverId);
} }
await using var context = _contextFactory.CreateContext(enableTracking: false); await using var context = _contextFactory.CreateContext(enableTracking: false);
// setup the query for the clients within the given rating range // setup the query for the clients within the given rating range
var iqClientRatings = (from rating in context.Set<EFRating>() var iqClientRatings = (from rating in context.Set<EFRating>()
@ -264,7 +292,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.Select(grp => new .Select(grp => new
{ {
grp.Key, 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>() var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
@ -278,7 +306,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ClientId = s.Key, ClientId = s.Key,
Kills = s.Sum(c => c.Kills), Kills = s.Sum(c => c.Kills),
Deaths = s.Sum(c => c.Deaths), 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), s.Sum(c => c.TimePlayed),
TotalTimePlayed = 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() var finished = topPlayers.Select(s => new TopStatsInfo()
{ {
ClientId = s.ClientId, ClientId = s.ClientId,
Id = (int?) serverId ?? 0, Id = (int?)serverId ?? 0,
Deaths = s.Deaths, Deaths = s.Deaths,
Kills = s.Kills, Kills = s.Kills,
KDR = Math.Round(s.KDR, 2), 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, ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1 PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1
? ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When) ? ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When)
.Select(r => r.Performance).ToList() .Select(r => new PerformanceHistory { Performance = r.Performance, OccurredAt = r.When })
: new List<double>() .ToList()
{clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance}, : 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"), TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed) TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed)
}) })
@ -366,7 +404,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Port = sv.Port, Port = sv.Port,
EndPoint = sv.ToString(), EndPoint = sv.ToString(),
ServerId = serverId, ServerId = serverId,
GameName = (Reference.Game?) sv.GameName, GameName = (Reference.Game?)sv.GameName,
HostName = sv.Hostname 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 // 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.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
ctx.SaveChanges(); ctx.SaveChanges();
} }
@ -469,7 +507,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{ {
Active = true, Active = true,
HitCount = 0, HitCount = 0,
Location = (int) hl Location = (int)hl
}).ToList() }).ToList()
}; };
@ -489,7 +527,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{ {
Active = true, Active = true,
HitCount = 0, HitCount = 0,
Location = (int) hl Location = (int)hl
}) })
.ToList(); .ToList();
@ -521,9 +559,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
catch (DbUpdateException updateException) when ( catch (DbUpdateException updateException) when (
updateException.InnerException is PostgresException {SqlState: "23503"} updateException.InnerException is PostgresException { SqlState: "23503" }
|| updateException.InnerException is SqliteException {SqliteErrorCode: 787} || updateException.InnerException is SqliteException { SqliteErrorCode: 787 }
|| updateException.InnerException is MySqlException {SqlState: "23503"}) || updateException.InnerException is MySqlException { SqlState: "23503" })
{ {
_log.LogWarning("Trying to add {Client} to stats before they have been added to the database", _log.LogWarning("Trying to add {Client} to stats before they have been added to the database",
pl.ToString()); pl.ToString());
@ -644,9 +682,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ServerId = serverId, ServerId = serverId,
DeathOrigin = vDeathOrigin, DeathOrigin = vDeathOrigin,
KillOrigin = vKillOrigin, 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), 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, WeaponReference = weapon,
ViewAngles = vViewAngles, ViewAngles = vViewAngles,
TimeOffset = long.Parse(offset), TimeOffset = long.Parse(offset),
@ -660,21 +698,21 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
AnglesList = snapshotAngles, AnglesList = snapshotAngles,
IsAlive = isAlive == "1", IsAlive = isAlive == "1",
TimeSinceLastAttack = long.Parse(lastAttackTime), TimeSinceLastAttack = long.Parse(lastAttackTime),
GameName = (int) attacker.CurrentServer.GameName GameName = (int)attacker.CurrentServer.GameName
}; };
} }
catch (Exception ex) catch (Exception ex)
{ {
_log.LogError(ex, "Could not parse script hit data. Damage={Damage}, TimeOffset={Offset}, TimeSinceLastAttack={LastAttackTime}", _log.LogError(ex,
"Could not parse script hit data. Damage={Damage}, TimeOffset={Offset}, TimeSinceLastAttack={LastAttackTime}",
damage, offset, lastAttackTime); damage, offset, lastAttackTime);
return; return;
} }
hit.SetAdditionalProperty("HitLocationReference", hitLoc); 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 // we don't care about shield hits
return; return;
@ -693,9 +731,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
await waiter.WaitAsync(Utilities.DefaultCommandTimeout, Plugin.ServerManager.CancellationToken); await waiter.WaitAsync(Utilities.DefaultCommandTimeout, Plugin.ServerManager.CancellationToken);
// increment their hit count // increment their hit count
if (hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_PISTOL_BULLET || if (hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_RIFLE_BULLET || hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_HEAD_SHOT) hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
{ {
clientStats.HitLocations.First(hl => hl.Location == hit.HitLoc).HitCount += 1; clientStats.HitLocations.First(hl => hl.Location == hit.HitLoc).HitCount += 1;
} }
@ -838,7 +876,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (KeyNotFoundException) catch (KeyNotFoundException)
{ {
} }
try try
{ {
if (!gameDetectionTypes[server.GameName].Contains(detectionType)) if (!gameDetectionTypes[server.GameName].Contains(detectionType))
@ -870,7 +908,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
new EFPenalty() new EFPenalty()
{ {
AutomatedOffense = penalty.Type == Detection.DetectionType.Bone 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}", : $"{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 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}"; : $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}";
penaltyClient.AdministeredPenalties = new List<EFPenalty>() penaltyClient.AdministeredPenalties = new List<EFPenalty>()
@ -926,19 +964,19 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// update the total stats // update the total stats
_servers[serverId].ServerStatistics.TotalKills += 1; _servers[serverId].ServerStatistics.TotalKills += 1;
if (attackerStats == null) if (attackerStats == null)
{ {
_log.LogWarning("Stats for {Client} are not yet initialized", attacker.ToString()); _log.LogWarning("Stats for {Client} are not yet initialized", attacker.ToString());
return; return;
} }
if (victimStats == null) if (victimStats == null)
{ {
_log.LogWarning("Stats for {Client} are not yet initialized", victim.ToString()); _log.LogWarning("Stats for {Client} are not yet initialized", victim.ToString());
return; return;
} }
// this happens when the round has changed // this happens when the round has changed
if (attackerStats.SessionScore == 0) if (attackerStats.SessionScore == 0)
{ {
@ -951,10 +989,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
var estimatedAttackerScore = attacker.CurrentServer.GameName != Server.Game.CSGO var estimatedAttackerScore = attacker.CurrentServer.GameName != Server.Game.CSGO
? attacker.Score ? attacker.Score
: attackerStats.SessionKills * 50; : attackerStats.SessionKills * 50;
var estimatedVictimScore = attacker.CurrentServer.GameName != Server.Game.CSGO var estimatedVictimScore = attacker.CurrentServer.GameName != Server.Game.CSGO
? victim.Score ? victim.Score
: victimStats.SessionKills * 50; : victimStats.SessionKills * 50;
attackerStats.SessionScore = estimatedAttackerScore; attackerStats.SessionScore = estimatedAttackerScore;
@ -1042,7 +1080,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// <returns></returns> /// <returns></returns>
public async Task UpdateStatHistory(EFClient client, EFClientStatistics clientStats) 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 // don't update their stat history if they haven't played long
if (currentSessionTime < 60) if (currentSessionTime < 60)
@ -1215,7 +1253,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{ {
await using var context = _contextFactory.CreateContext(); await using var context = _contextFactory.CreateContext();
var minPlayTime = _config.TopPlayersMinPlayTime; var minPlayTime = _config.TopPlayersMinPlayTime;
var performances = await context.Set<EFClientStatistics>() var performances = await context.Set<EFClientStatistics>()
.AsNoTracking() .AsNoTracking()
.Where(stat => stat.ClientId == clientId) .Where(stat => stat.ClientId == clientId)
@ -1223,7 +1261,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.Where(stats => stats.UpdatedAt >= Extensions.FifteenDaysAgo()) .Where(stats => stats.UpdatedAt >= Extensions.FifteenDaysAgo())
.Where(stats => stats.TimePlayed >= minPlayTime) .Where(stats => stats.TimePlayed >= minPlayTime)
.ToListAsync(); .ToListAsync();
if (clientStats.TimePlayed >= minPlayTime) if (clientStats.TimePlayed >= minPlayTime)
{ {
clientStats.ZScore = await _serverDistributionCalculator.GetZScoreForServer(serverId, clientStats.ZScore = await _serverDistributionCalculator.GetZScoreForServer(serverId,
@ -1254,8 +1292,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (performances.Any(performance => performance.TimePlayed >= minPlayTime)) if (performances.Any(performance => performance.TimePlayed >= minPlayTime))
{ {
var aggregateZScore = performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime); var aggregateZScore =
performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
int? aggregateRanking = await context.Set<EFClientStatistics>() int? aggregateRanking = await context.Set<EFClientStatistics>()
.Where(stat => stat.ClientId != clientId) .Where(stat => stat.ClientId != clientId)
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime)) .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime))
@ -1274,7 +1313,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
clientStats.Client?.ToString(), aggregateZScore); clientStats.Client?.ToString(), aggregateZScore);
return; return;
} }
var aggregateRankingSnapshot = new EFClientRankingHistory var aggregateRankingSnapshot = new EFClientRankingHistory
{ {
ClientId = clientId, ClientId = clientId,
@ -1297,7 +1336,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.Where(r => r.ClientId == clientId) .Where(r => r.ClientId == clientId)
.Where(r => r.ServerId == serverId) .Where(r => r.ServerId == serverId)
.CountAsync(); .CountAsync();
var mostRecent = await context.Set<EFClientRankingHistory>() var mostRecent = await context.Set<EFClientRankingHistory>()
.Where(r => r.ClientId == clientId) .Where(r => r.ClientId == clientId)
.Where(r => r.ServerId == serverId) .Where(r => r.ServerId == serverId)
@ -1309,14 +1348,20 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
context.Update(mostRecent); context.Update(mostRecent);
} }
if (totalRankingEntries > EFClientRankingHistory.MaxRankingCount) const int maxRankingCount = 1728; // 60 / 2.5 * 24 * 3 ( 3 days at sample every 2.5 minutes)
if (totalRankingEntries > maxRankingCount)
{ {
var lastRating = await context.Set<EFClientRankingHistory>() var lastRating = await context.Set<EFClientRankingHistory>()
.Where(r => r.ClientId == clientId) .Where(r => r.ClientId == clientId)
.Where(r => r.ServerId == serverId) .Where(r => r.ServerId == serverId)
.OrderBy(r => r.CreatedDateTime) .OrderBy(r => r.CreatedDateTime)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
context.Remove(lastRating);
if (lastRating is not null)
{
context.Remove(lastRating);
}
} }
} }
@ -1325,7 +1370,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// </summary> /// </summary>
/// <param name="attackerStats">Stats of the attacker</param> /// <param name="attackerStats">Stats of the attacker</param>
/// <param name="victimStats">Stats of the victim</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) EFClient attacker, EFClient victim)
{ {
bool suicide = attackerStats.ClientId == victimStats.ClientId; bool suicide = attackerStats.ClientId == victimStats.ClientId;
@ -1351,7 +1396,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// calculate elo // calculate elo
var attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) - 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)); var winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E));
attackerStats.EloRating += 6.0 * (1 - winPercentage); 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)); victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2));
// update after calculation // update after calculation
attackerStats.TimePlayed += (int) (DateTime.UtcNow - attackerStats.LastActive).TotalSeconds; attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
victimStats.TimePlayed += (int) (DateTime.UtcNow - victimStats.LastActive).TotalSeconds; victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
attackerStats.LastActive = DateTime.UtcNow; attackerStats.LastActive = DateTime.UtcNow;
victimStats.LastActive = DateTime.UtcNow; victimStats.LastActive = DateTime.UtcNow;
} }
@ -1400,11 +1445,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var killSpm = scoreDifference / timeSinceLastCalc; var killSpm = scoreDifference / timeSinceLastCalc;
var spmMultiplier = 2.934 * var spmMultiplier = 2.934 *
Math.Pow( Math.Pow(
_servers[clientStats.ServerId] _servers[clientStats.ServerId]
.TeamCount((IW4Info.Team) clientStats.Team == IW4Info.Team.Allies .TeamCount((IW4Info.Team)clientStats.Team == IW4Info.Team.Allies
? IW4Info.Team.Axis ? IW4Info.Team.Axis
: IW4Info.Team.Allies), -0.454); : IW4Info.Team.Allies), -0.454);
killSpm *= Math.Max(1, spmMultiplier); killSpm *= Math.Max(1, spmMultiplier);
// update this for ac tracking // 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 // calculate the weight of the new play time against last 10 hours of gameplay
int totalPlayTime = (clientStats.TimePlayed == 0) int totalPlayTime = (clientStats.TimePlayed == 0)
? (int) (DateTime.UtcNow - clientStats.LastActive).TotalSeconds ? (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds
: clientStats.TimePlayed + (int) (DateTime.UtcNow - clientStats.LastActive).TotalSeconds; : clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0)); 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)) if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
{ {
_log.LogWarning("clientStats SPM/Skill NaN {@killInfo}", _log.LogWarning("clientStats SPM/Skill NaN {@killInfo}",
new {killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference}); new
{
killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference
});
clientStats.SPM = 0; clientStats.SPM = 0;
clientStats.Skill = 0; clientStats.Skill = 0;
} }
@ -1483,11 +1531,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public void ResetKillstreaks(Server sv) public void ResetKillstreaks(Server sv)
{ {
foreach (var session in sv.GetClientsAsList() foreach (var session in sv.GetClientsAsList()
.Select(_client => new .Select(_client => new
{ {
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY), stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY) detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
})) }))
{ {
session.stat?.StartNewSession(); session.stat?.StartNewSession();
session.detection?.OnMapChange(); session.detection?.OnMapChange();
@ -1549,8 +1597,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
foreach (var stats in sv.GetClientsAsList() foreach (var stats in sv.GetClientsAsList()
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY)) .Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))
.Where(_stats => _stats != null)) .Where(_stats => _stats != null))
{ {
await SaveClientStats(stats); await SaveClientStats(stats);
} }

View File

@ -42,10 +42,11 @@ namespace IW4MAdmin.Plugins.Stats
private readonly ILogger<Plugin> _logger; private readonly ILogger<Plugin> _logger;
private readonly List<IClientStatisticCalculator> _statCalculators; private readonly List<IClientStatisticCalculator> _statCalculators;
private readonly IServerDistributionCalculator _serverDistributionCalculator; private readonly IServerDistributionCalculator _serverDistributionCalculator;
private readonly IServerDataViewer _serverDataViewer;
public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory, public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory,
ITranslationLookup translationLookup, IMetaServiceV2 metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper, ILogger<StatManager> managerLogger, 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"); Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
_databaseContextFactory = databaseContextFactory; _databaseContextFactory = databaseContextFactory;
@ -56,6 +57,7 @@ namespace IW4MAdmin.Plugins.Stats
_logger = logger; _logger = logger;
_statCalculators = statCalculators.ToList(); _statCalculators = statCalculators.ToList();
_serverDistributionCalculator = serverDistributionCalculator; _serverDistributionCalculator = serverDistributionCalculator;
_serverDataViewer = serverDataViewer;
} }
public async Task OnEventAsync(GameEvent gameEvent, Server server) public async Task OnEventAsync(GameEvent gameEvent, Server server)
@ -201,13 +203,17 @@ namespace IW4MAdmin.Plugins.Stats
var performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed); var performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed);
var performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2); 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 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> return new List<InformationResponse>
{ {
new InformationResponse new InformationResponse
{ {
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"], 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, Column = 0,
Order = 0, Order = 0,
Type = MetaType.Information Type = MetaType.Information

View File

@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -53,7 +53,7 @@ namespace IW4MAdmin.Plugins.Welcome
{ {
var newPlayer = gameEvent.Origin; var newPlayer = gameEvent.Origin;
if (newPlayer.Level >= Permission.Trusted && !gameEvent.Origin.Masked|| 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.Level != Permission.Flagged && newPlayer.Level != Permission.Banned &&
!newPlayer.Masked) !newPlayer.Masked)
gameEvent.Owner.Broadcast( gameEvent.Owner.Broadcast(
@ -88,7 +88,7 @@ namespace IW4MAdmin.Plugins.Welcome
{ {
msg = msg.Replace("{{ClientName}}", joining.Name); msg = msg.Replace("{{ClientName}}", joining.Name);
msg = msg.Replace("{{ClientLevel}}", 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 // this prevents it from trying to evaluate it every message
if (msg.Contains("{{ClientLocation}}")) if (msg.Contains("{{ClientLocation}}"))
{ {
@ -111,7 +111,7 @@ namespace IW4MAdmin.Plugins.Welcome
try try
{ {
var response = 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); var responseObj = JObject.Parse(response);
response = responseObj["country"]?.ToString(); response = responseObj["country"]?.ToString();

View File

@ -20,7 +20,7 @@
</Target> </Target>
<ItemGroup> <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> </ItemGroup>
</Project> </Project>

View 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();
}
}

View File

@ -4,7 +4,6 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Context;
using Data.Models; using Data.Models;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
@ -20,28 +19,37 @@ namespace SharedLibraryCore
{ {
public class BaseController : Controller public class BaseController : Controller
{ {
protected readonly IAlertManager AlertManager;
/// <summary> /// <summary>
/// life span in months /// life span in months
/// </summary> /// </summary>
private const int COOKIE_LIFESPAN = 3; private const int CookieLifespan = 3;
private static readonly byte[] LocalHost = { 127, 0, 0, 1 }; private static readonly byte[] LocalHost = { 127, 0, 0, 1 };
private static string SocialLink; private static string _socialLink;
private static string SocialTitle; private static string _socialTitle;
protected readonly DatabaseContext Context;
protected List<Page> Pages; protected List<Page> Pages;
protected List<string> PermissionsSet; 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) public BaseController(IManager manager)
{ {
AlertManager = manager.AlertManager;
Manager = manager; Manager = manager;
Localization ??= Utilities.CurrentLocalization.LocalizationIndex; Localization = Utilities.CurrentLocalization.LocalizationIndex;
AppConfig = Manager.GetApplicationSettings().Configuration(); AppConfig = Manager.GetApplicationSettings().Configuration();
if (AppConfig.EnableSocialLink && SocialLink == null) if (AppConfig.EnableSocialLink && _socialLink == null)
{ {
SocialLink = AppConfig.SocialLinkAddress; _socialLink = AppConfig.SocialLinkAddress;
SocialTitle = AppConfig.SocialLinkTitle; _socialTitle = AppConfig.SocialLinkTitle;
} }
Pages = Manager.GetPageList().Pages Pages = Manager.GetPageList().Pages
@ -56,7 +64,7 @@ namespace SharedLibraryCore
ViewBag.EnableColorCodes = AppConfig.EnableColorCodes; ViewBag.EnableColorCodes = AppConfig.EnableColorCodes;
ViewBag.Language = Utilities.CurrentLocalization.Culture.TwoLetterISOLanguageName; ViewBag.Language = Utilities.CurrentLocalization.Culture.TwoLetterISOLanguageName;
Client ??= new EFClient Client = new EFClient
{ {
ClientId = -1, ClientId = -1,
Level = Data.Models.Client.EFClient.Permission.Banned, 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) protected async Task SignInAsync(ClaimsPrincipal claimsPrinciple)
{ {
@ -76,7 +80,7 @@ namespace SharedLibraryCore
new AuthenticationProperties new AuthenticationProperties
{ {
AllowRefresh = true, AllowRefresh = true,
ExpiresUtc = DateTime.UtcNow.AddMonths(COOKIE_LIFESPAN), ExpiresUtc = DateTime.UtcNow.AddMonths(CookieLifespan),
IsPersistent = true, IsPersistent = true,
IssuedUtc = DateTime.UtcNow IssuedUtc = DateTime.UtcNow
}); });
@ -96,7 +100,7 @@ namespace SharedLibraryCore
Client.ClientId = clientId; Client.ClientId = clientId;
Client.NetworkId = clientId == 1 Client.NetworkId = clientId == 1
? 0 ? 0
: User.Claims.First(_claim => _claim.Type == ClaimTypes.PrimarySid).Value : User.Claims.First(claim => claim.Type == ClaimTypes.PrimarySid).Value
.ConvertGuidToLong(NumberStyles.HexNumber); .ConvertGuidToLong(NumberStyles.HexNumber);
Client.Level = (Data.Models.Client.EFClient.Permission)Enum.Parse( Client.Level = (Data.Models.Client.EFClient.Permission)Enum.Parse(
typeof(Data.Models.Client.EFClient.Permission), typeof(Data.Models.Client.EFClient.Permission),
@ -104,6 +108,9 @@ namespace SharedLibraryCore
Client.CurrentAlias = new EFAlias Client.CurrentAlias = new EFAlias
{ Name = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value }; { Name = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value };
Authorized = Client.ClientId >= 0; 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.Role, Client.Level.ToString()),
new Claim(ClaimTypes.Sid, Client.ClientId.ToString()), new Claim(ClaimTypes.Sid, Client.ClientId.ToString()),
new Claim(ClaimTypes.PrimarySid, Client.NetworkId.ToString("X")), new Claim(ClaimTypes.PrimarySid, Client.NetworkId.ToString("X")),
new Claim(ClaimTypes.PrimaryGroupSid, Client.GameName.ToString())
}; };
var claimsIdentity = new ClaimsIdentity(claims, "login"); var claimsIdentity = new ClaimsIdentity(claims, "login");
SignInAsync(new ClaimsPrincipal(claimsIdentity)).Wait(); SignInAsync(new ClaimsPrincipal(claimsIdentity)).Wait();
@ -150,8 +158,8 @@ namespace SharedLibraryCore
ViewBag.Url = AppConfig.WebfrontUrl; ViewBag.Url = AppConfig.WebfrontUrl;
ViewBag.User = Client; ViewBag.User = Client;
ViewBag.Version = Manager.Version; ViewBag.Version = Manager.Version;
ViewBag.SocialLink = SocialLink ?? ""; ViewBag.SocialLink = _socialLink ?? "";
ViewBag.SocialTitle = SocialTitle; ViewBag.SocialTitle = _socialTitle;
ViewBag.Pages = Pages; ViewBag.Pages = Pages;
ViewBag.Localization = Utilities.CurrentLocalization.LocalizationIndex; ViewBag.Localization = Utilities.CurrentLocalization.LocalizationIndex;
ViewBag.CustomBranding = shouldUseCommunityName ViewBag.CustomBranding = shouldUseCommunityName
@ -169,6 +177,7 @@ namespace SharedLibraryCore
ViewBag.ReportCount = Manager.GetServers().Sum(server => ViewBag.ReportCount = Manager.GetServers().Sum(server =>
server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24))); server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24)));
ViewBag.PermissionsSet = PermissionsSet; ViewBag.PermissionsSet = PermissionsSet;
ViewBag.Alerts = AlertManager.RetrieveAlerts(Client).ToList();
base.OnActionExecuting(context); base.OnActionExecuting(context);
} }

View File

@ -11,163 +11,180 @@ namespace SharedLibraryCore.Commands
{ {
public class CommandProcessing 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) CommandConfiguration commandConfig)
{ {
var loc = Utilities.CurrentLocalization.LocalizationIndex; var loc = Utilities.CurrentLocalization.LocalizationIndex;
var Manager = E.Owner.Manager; var manager = gameEvent.Owner.Manager;
var isBroadcast = E.Data.StartsWith(appConfig.BroadcastCommandPrefix); var isBroadcast = gameEvent.Data.StartsWith(appConfig.BroadcastCommandPrefix);
var prefixLength = isBroadcast ? appConfig.BroadcastCommandPrefix.Length : appConfig.CommandPrefix.Length; var prefixLength = isBroadcast ? appConfig.BroadcastCommandPrefix.Length : appConfig.CommandPrefix.Length;
var CommandString = E.Data.Substring(prefixLength, E.Data.Length - prefixLength).Split(' ')[0]; var commandString =
E.Message = E.Data; gameEvent.Data.Substring(prefixLength, gameEvent.Data.Length - prefixLength).Split(' ')[0];
gameEvent.Message = gameEvent.Data;
Command C = null; Command matchedCommand = null;
foreach (Command cmd in Manager.GetCommands() foreach (var availableCommand in manager.GetCommands()
.Where(c => c.Name != null)) .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) if (availableCommand.Name.Equals(commandString, StringComparison.OrdinalIgnoreCase) ||
{ (availableCommand.Alias ?? "").Equals(commandString, StringComparison.OrdinalIgnoreCase))
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 (!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; 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) if (found != null)
{ {
found = Manager.FindActiveClient(found); found = manager.FindActiveClient(found);
E.Target = found; gameEvent.Target = found;
E.Target.CurrentServer = found.CurrentServer ?? E.Owner; gameEvent.Target.CurrentServer = found.CurrentServer ?? gameEvent.Owner;
E.Data = string.Join(" ", Args.Skip(1)); 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 ) // user specifying target by client num
{ {
if (E.Owner.Clients[cNum] != null) if (gameEvent.Owner.Clients[cNum] != null)
{ {
E.Target = E.Owner.Clients[cNum]; gameEvent.Target = gameEvent.Owner.Clients[cNum];
E.Data = string.Join(" ", Args.Skip(1)); gameEvent.Data = string.Join(" ", args.Skip(1));
} }
} }
} }
List<EFClient> matchingPlayers; 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) if (matchingPlayers.Count > 1)
{ {
E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]); gameEvent.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
throw new CommandException($"{E.Origin} had multiple players found for {C.Name}"); throw new CommandException(
$"{gameEvent.Origin} had multiple players found for {matchedCommand.Name}");
} }
if (matchingPlayers.Count == 1) 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); 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"]); gameEvent.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
E.Origin.Tell(C.Syntax); gameEvent.Origin.Tell(matchedCommand.Syntax);
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\""); 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) if (matchingPlayers.Count > 1)
{ {
E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]); gameEvent.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
foreach (var p in matchingPlayers) foreach (var p in matchingPlayers)
E.Origin.Tell($"[(Color::Yellow){p.ClientNumber}(Color::White)] {p.Name}"); gameEvent.Origin.Tell($"[(Color::Yellow){p.ClientNumber}(Color::White)] {p.Name}");
throw new CommandException($"{E.Origin} had multiple players found for {C.Name}"); throw new CommandException(
$"{gameEvent.Origin} had multiple players found for {matchedCommand.Name}");
} }
if (matchingPlayers.Count == 1) 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 escapedArg = Regex.Escape(Args[0]); var escapedArg = Regex.Escape(args[0]);
var reg = new Regex($"({escapedName})|({escapedArg})", RegexOptions.IgnoreCase); 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() || if ((gameEvent.Data.Trim() == gameEvent.Target.CleanedName.ToLower().Trim() ||
E.Data == string.Empty) && gameEvent.Data == string.Empty) &&
C.RequiresTarget) matchedCommand.RequiresTarget)
{ {
E.Origin.Tell(loc["COMMAND_MISSINGARGS"]); gameEvent.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
E.Origin.Tell(C.Syntax); gameEvent.Origin.Tell(matchedCommand.Syntax);
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\""); 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"]); gameEvent.Origin.Tell(loc["COMMAND_TARGET_NOTFOUND"]);
throw new CommandException($"{E.Origin} specified invalid player for \"{C.Name}\""); throw new CommandException(
$"{gameEvent.Origin} specified invalid player for \"{matchedCommand.Name}\"");
} }
} }
E.Data = E.Data.Trim(); gameEvent.Data = gameEvent.Data.Trim();
return C; return matchedCommand;
} }
} }
} }

View File

@ -381,7 +381,7 @@ namespace SharedLibraryCore.Commands
{ {
// todo: don't do the lookup here // todo: don't do the lookup here
var penalties = await gameEvent.Owner.Manager.GetPenaltyService().GetActivePenaltiesAsync(gameEvent.Target.AliasLinkId, 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 if (penalties
.FirstOrDefault(p => .FirstOrDefault(p =>
@ -897,7 +897,7 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E) public override async Task ExecuteAsync(GameEvent E)
{ {
var existingPenalties = await E.Owner.Manager.GetPenaltyService() 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); var penalty = existingPenalties.FirstOrDefault(b => b.Type > EFPenalty.PenaltyType.Kick);
if (penalty == null) if (penalty == null)

View File

@ -1,6 +1,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models.Client; using Data.Models.Client;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
namespace SharedLibraryCore.Commands namespace SharedLibraryCore.Commands
@ -19,13 +20,16 @@ namespace SharedLibraryCore.Commands
RequiresTarget = false; RequiresTarget = false;
} }
public override Task ExecuteAsync(GameEvent E) public override Task ExecuteAsync(GameEvent gameEvent)
{ {
var state = E.Owner.Manager.TokenAuthenticator.GenerateNextToken(E.Origin.NetworkId); var state = gameEvent.Owner.Manager.TokenAuthenticator.GenerateNextToken(new TokenIdentifier
E.Origin.Tell(string.Format(_translationLookup["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token, {
$"{state.RemainingTime} {_translationLookup["GLOBAL_MINUTES"]}", E.Origin.ClientId)); 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; return Task.CompletedTask;
} }
} }
} }

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Data.Models.Misc;
using Newtonsoft.Json; using Newtonsoft.Json;
using SharedLibraryCore.Configuration.Attributes; using SharedLibraryCore.Configuration.Attributes;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -154,6 +155,13 @@ namespace SharedLibraryCore.Configuration
{ Permission.Console.ToString(), new List<string> { "*" } } { 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] [ConfigurationIgnore]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")] [LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")]
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new() public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new()

View File

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

View File

@ -1,4 +1,5 @@
using System; using System;
using Data.Models;
using Data.Models.Client; using Data.Models.Client;
namespace SharedLibraryCore.Dtos namespace SharedLibraryCore.Dtos
@ -10,6 +11,7 @@ namespace SharedLibraryCore.Dtos
public int LinkId { get; set; } public int LinkId { get; set; }
public EFClient.Permission Level { get; set; } public EFClient.Permission Level { get; set; }
public DateTime LastConnection { get; set; } public DateTime LastConnection { get; set; }
public Reference.Game Game { get; set; }
public bool IsMasked { get; set; } public bool IsMasked { get; set; }
} }
} }

View File

@ -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; }
}

View File

@ -9,6 +9,7 @@ namespace SharedLibraryCore.Dtos
public class PlayerInfo public class PlayerInfo
{ {
public string Name { get; set; } public string Name { get; set; }
public Reference.Game Game { get; set; }
public int ClientId { get; set; } public int ClientId { get; set; }
public string Level { get; set; } public string Level { get; set; }
public string Tag { get; set; } public string Tag { get; set; }
@ -32,5 +33,6 @@ namespace SharedLibraryCore.Dtos
public string ConnectProtocolUrl { get;set; } public string ConnectProtocolUrl { get;set; }
public string CurrentServerName { get; set; } public string CurrentServerName { get; set; }
public IGeoLocationResult GeoLocationInfo { get; set; } public IGeoLocationResult GeoLocationInfo { get; set; }
public ClientNoteMetaResponse NoteMeta { get; set; }
} }
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Data.Models;
using SharedLibraryCore.Helpers; using SharedLibraryCore.Helpers;
namespace SharedLibraryCore.Dtos namespace SharedLibraryCore.Dtos
@ -40,5 +41,6 @@ namespace SharedLibraryCore.Dtos
return Math.Round(valid.Select(player => player.ZScore.Value).Average(), 2); return Math.Round(valid.Select(player => player.ZScore.Value).Average(), 2);
} }
} }
public Reference.Game Game { get; set; }
} }
} }

View File

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

View 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; }
}

View File

@ -4,7 +4,6 @@ namespace SharedLibraryCore.Helpers
{ {
public sealed class TokenState public sealed class TokenState
{ {
public long NetworkId { get; set; }
public DateTime RequestTime { get; set; } = DateTime.Now; public DateTime RequestTime { get; set; } = DateTime.Now;
public TimeSpan TokenDuration { get; set; } public TimeSpan TokenDuration { get; set; }
public string Token { 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) public string RemainingTime => Math.Round(-(DateTime.Now - RequestTime).Subtract(TokenDuration).TotalMinutes, 1)
.ToString(); .ToString();
} }
} }

View 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; }
}

View File

@ -10,7 +10,7 @@ namespace SharedLibraryCore.Interfaces
Task<T> Delete(T entity); Task<T> Delete(T entity);
Task<T> Update(T entity); Task<T> Update(T entity);
Task<T> Get(int entityID); 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); Task<IList<T>> Find(Func<T, bool> expression);
} }
} }

View File

@ -102,5 +102,7 @@ namespace SharedLibraryCore.Interfaces
/// event executed when event has finished executing /// event executed when event has finished executing
/// </summary> /// </summary>
event EventHandler<GameEvent> OnGameEventExecuted; event EventHandler<GameEvent> OnGameEventExecuted;
IAlertManager AlertManager { get; }
} }
} }

View File

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

View File

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

View File

@ -37,5 +37,13 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<ClientHistoryInfo>> ClientHistoryAsync(TimeSpan? overPeriod = null, Task<IEnumerable<ClientHistoryInfo>> ClientHistoryAsync(TimeSpan? overPeriod = null,
CancellationToken token = default); 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);
} }
} }

View File

@ -7,16 +7,15 @@ namespace SharedLibraryCore.Interfaces
/// <summary> /// <summary>
/// generates and returns a token for the given network id /// generates and returns a token for the given network id
/// </summary> /// </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> /// <returns>4 character string token</returns>
TokenState GenerateNextToken(long networkId); TokenState GenerateNextToken(ITokenIdentifier authInfo);
/// <summary> /// <summary>
/// authorizes given token /// authorizes given token
/// </summary> /// </summary>
/// <param name="networkId">network id of the client to authorize</param> /// <param name="authInfo">auth information</param>
/// <param name="token">token to authorize</param>
/// <returns>true if token authorized successfully, false otherwise</returns> /// <returns>true if token authorized successfully, false otherwise</returns>
bool AuthorizeToken(long networkId, string token); bool AuthorizeToken(ITokenIdentifier authInfo);
} }
} }

View File

@ -0,0 +1,7 @@
namespace SharedLibraryCore.Interfaces;
public interface ITokenIdentifier
{
int ClientId { get; }
string Token { get; }
}

View File

@ -76,7 +76,7 @@ namespace SharedLibraryCore.Database.Models
[NotMapped] [NotMapped]
public virtual int? IPAddress public virtual int? IPAddress
{ {
get => CurrentAlias.IPAddress; get => CurrentAlias?.IPAddress;
set => CurrentAlias.IPAddress = value; set => CurrentAlias.IPAddress = value;
} }
@ -100,7 +100,10 @@ namespace SharedLibraryCore.Database.Models
[NotMapped] public int Score { get; set; } [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"; [NotMapped] public bool IsZombieClient => IsBot && Name == "Zombie";
@ -170,7 +173,7 @@ namespace SharedLibraryCore.Database.Models
?.CorrelationId ?? Guid.NewGuid() ?.CorrelationId ?? Guid.NewGuid()
}; };
e.Output.Add(message.FormatMessageForEngine(CurrentServer?.RconParser.Configuration.ColorCodeMapping) e.Output.Add(message.FormatMessageForEngine(CurrentServer?.RconParser.Configuration)
.StripColors()); .StripColors());
CurrentServer?.Manager.AddEvent(e); 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) // 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() 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 banPenalty = activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.Ban);
var tempbanPenalty = var tempbanPenalty =
activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.TempBan); activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.TempBan);

View File

@ -33,7 +33,8 @@ namespace SharedLibraryCore
T6 = 7, T6 = 7,
T7 = 8, T7 = 8,
SHG1 = 9, SHG1 = 9,
CSGO = 10 CSGO = 10,
H1 = 11
} }
// only here for performance // only here for performance
@ -200,7 +201,7 @@ namespace SharedLibraryCore
.ToList(); .ToList();
} }
public virtual Task<bool> ProcessUpdatesAsync(CancellationToken cts) public virtual Task<bool> ProcessUpdatesAsync(CancellationToken token)
{ {
return (Task<bool>)Task.CompletedTask; return (Task<bool>)Task.CompletedTask;
} }
@ -224,7 +225,7 @@ namespace SharedLibraryCore
var formattedMessage = string.Format(RconParser.Configuration.CommandPrefixes.Say ?? "", var formattedMessage = string.Format(RconParser.Configuration.CommandPrefixes.Say ?? "",
$"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{message}"); $"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{message}");
ServerLogger.LogDebug("All-> {Message}", ServerLogger.LogDebug("All-> {Message}",
message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping).StripColors()); message.FormatMessageForEngine(RconParser.Configuration).StripColors());
var e = new GameEvent var e = new GameEvent
{ {
@ -288,13 +289,13 @@ namespace SharedLibraryCore
else else
{ {
ServerLogger.LogDebug("Tell[{ClientNumber}]->{Message}", targetClient.ClientNumber, 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) if (targetClient.Level == Data.Models.Client.EFClient.Permission.Console)
{ {
Console.ForegroundColor = ConsoleColor.Green; Console.ForegroundColor = ConsoleColor.Green;
var cleanMessage = message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping) var cleanMessage = message.FormatMessageForEngine(RconParser.Configuration)
.StripColors(); .StripColors();
using (LogContext.PushProperty("Server", ToString())) using (LogContext.PushProperty("Server", ToString()))
{ {

View File

@ -23,25 +23,26 @@ namespace SharedLibraryCore.Services
{ {
public class ClientService : IEntityService<EFClient>, IResourceQueryHelper<FindClientRequest, FindClientResult> public class ClientService : IEntityService<EFClient>, IResourceQueryHelper<FindClientRequest, FindClientResult>
{ {
private static readonly Func<DatabaseContext, long, Task<EFClient>> _getUniqueQuery = private static readonly Func<DatabaseContext, long, Reference.Game, Task<EFClient>> GetUniqueQuery =
EF.CompileAsyncQuery((DatabaseContext context, long networkId) => EF.CompileAsyncQuery((DatabaseContext context, long networkId, Reference.Game game) =>
context.Clients context.Clients
.Select(_client => new EFClient .Select(client => new EFClient
{ {
ClientId = _client.ClientId, ClientId = client.ClientId,
AliasLinkId = _client.AliasLinkId, AliasLinkId = client.AliasLinkId,
Level = _client.Level, Level = client.Level,
Connections = _client.Connections, Connections = client.Connections,
FirstConnection = _client.FirstConnection, FirstConnection = client.FirstConnection,
LastConnection = _client.LastConnection, LastConnection = client.LastConnection,
Masked = _client.Masked, Masked = client.Masked,
NetworkId = _client.NetworkId, NetworkId = client.NetworkId,
TotalConnectionTime = _client.TotalConnectionTime, TotalConnectionTime = client.TotalConnectionTime,
AliasLink = _client.AliasLink, AliasLink = client.AliasLink,
Password = _client.Password, Password = client.Password,
PasswordSalt = _client.PasswordSalt PasswordSalt = client.PasswordSalt,
GameName = client.GameName
}) })
.FirstOrDefault(c => c.NetworkId == networkId) .FirstOrDefault(client => client.NetworkId == networkId && client.GameName == game)
); );
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
@ -178,6 +179,7 @@ namespace SharedLibraryCore.Services
.Select(_client => new EFClient .Select(_client => new EFClient
{ {
ClientId = _client.ClientId, ClientId = _client.ClientId,
GameName = _client.GameName,
AliasLinkId = _client.AliasLinkId, AliasLinkId = _client.AliasLinkId,
Level = _client.Level, Level = _client.Level,
Connections = _client.Connections, Connections = _client.Connections,
@ -234,10 +236,10 @@ namespace SharedLibraryCore.Services
return foundClient.Client; 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); 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) public async Task<EFClient> Update(EFClient temporalClient)
@ -284,7 +286,7 @@ namespace SharedLibraryCore.Services
entity.PasswordSalt = temporalClient.PasswordSalt; entity.PasswordSalt = temporalClient.PasswordSalt;
} }
entity.GameName ??= temporalClient.GameName; entity.GameName = temporalClient.GameName;
// update in database // update in database
await context.SaveChangesAsync(); await context.SaveChangesAsync();
@ -757,19 +759,20 @@ namespace SharedLibraryCore.Services
{ {
await using var context = _contextFactory.CreateContext(false); await using var context = _contextFactory.CreateContext(false);
return await context.Clients return await context.Clients
.Select(_client => new EFClient .Select(client => new EFClient
{ {
NetworkId = _client.NetworkId, NetworkId = client.NetworkId,
ClientId = _client.ClientId, ClientId = client.ClientId,
CurrentAlias = new EFAlias CurrentAlias = new EFAlias
{ {
Name = _client.CurrentAlias.Name Name = client.CurrentAlias.Name
}, },
Password = _client.Password, Password = client.Password,
PasswordSalt = _client.PasswordSalt, PasswordSalt = client.PasswordSalt,
Level = _client.Level 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) public async Task<List<EFClient>> GetPrivilegedClients(bool includeName = true)
@ -789,7 +792,8 @@ namespace SharedLibraryCore.Services
PasswordSalt = client.PasswordSalt, PasswordSalt = client.PasswordSalt,
NetworkId = client.NetworkId, NetworkId = client.NetworkId,
LastConnection = client.LastConnection, LastConnection = client.LastConnection,
Masked = client.Masked Masked = client.Masked,
GameName = client.GameName
}; };
return await iqClients.ToListAsync(); return await iqClients.ToListAsync();
@ -849,31 +853,35 @@ namespace SharedLibraryCore.Services
else else
{ {
iqClients = iqClients.Where(_client => networkId == _client.NetworkId || iqClients = iqClients.Where(client => networkId == client.NetworkId || linkIds.Contains(client.AliasLinkId));
linkIds.Contains(_client.AliasLinkId) }
|| !_appConfig.EnableImplicitAccountLinking &&
_client.CurrentAlias.IPAddress != null && if (ipAddress is not null && !_appConfig.EnableImplicitAccountLinking)
_client.CurrentAlias.IPAddress == ipAddress); {
iqClients = iqClients.Union(context.Clients.Where(client => client.CurrentAlias.IPAddress == ipAddress));
} }
// we want to project our results // we want to project our results
var iqClientProjection = iqClients.OrderByDescending(_client => _client.LastConnection) var iqClientProjection = iqClients.OrderByDescending(client => client.LastConnection)
.Select(_client => new PlayerInfo .Select(client => new PlayerInfo
{ {
Name = _client.CurrentAlias.Name, Name = client.CurrentAlias.Name,
LevelInt = (int)_client.Level, LevelInt = (int)client.Level,
LastConnection = _client.LastConnection, LastConnection = client.LastConnection,
ClientId = _client.ClientId, ClientId = client.ClientId,
IPAddress = _client.CurrentAlias.IPAddress.HasValue IPAddress = client.CurrentAlias.IPAddress.HasValue
? _client.CurrentAlias.SearchableIPAddress ? client.CurrentAlias.SearchableIPAddress
: "" : "",
Game = client.GameName
}); });
var clients = await iqClientProjection.ToListAsync(); var clients = await iqClientProjection.ToListAsync();
// this is so we don't try to evaluate this in the linq to entities query // this is so we don't try to evaluate this in the linq to entities query
foreach (var client in clients) foreach (var client in clients)
{
client.Level = ((Permission)client.LevelInt).ToLocalizedLevelName(); client.Level = ((Permission)client.LevelInt).ToLocalizedLevelName();
}
return clients; return clients;
} }
@ -930,6 +938,14 @@ namespace SharedLibraryCore.Services
return clientList; return clientList;
} }
public async Task<string> GetClientNameById(int clientId)
{
await using var context = _contextFactory.CreateContext();
var match = await context.Clients.Select(client => new { client.CurrentAlias.Name, client.ClientId })
.FirstOrDefaultAsync(client => client.ClientId == clientId);
return match?.Name;
}
#endregion #endregion
} }
} }

View File

@ -88,7 +88,7 @@ namespace SharedLibraryCore.Services
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<EFPenalty> GetUnique(long entityProperty) public Task<EFPenalty> GetUnique(long entityProperty, object altKey)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -139,10 +139,10 @@ namespace SharedLibraryCore.Services
LinkedPenalties.Contains(pi.Penalty.Type) && pi.Penalty.Active && LinkedPenalties.Contains(pi.Penalty.Type) && pi.Penalty.Active &&
(pi.Penalty.Expires == null || pi.Penalty.Expires > DateTime.UtcNow); (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) int? ip = null)
{ {
var penaltiesByIdentifier = await GetActivePenaltiesByIdentifier(ip, networkId); var penaltiesByIdentifier = await GetActivePenaltiesByIdentifier(ip, networkId, game);
if (penaltiesByIdentifier.Any()) if (penaltiesByIdentifier.Any())
{ {
@ -183,16 +183,16 @@ namespace SharedLibraryCore.Services
return activePenalties.OrderByDescending(p => p.When).ToList(); 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); await using var context = _contextFactory.CreateContext(false);
var activePenaltiesIds = context.PenaltyIdentifiers.Where(identifier => 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); .Where(FilterById);
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync(); return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync();
} }
public async Task<List<EFPenalty>> ActivePenaltiesByRecentIdentifiers(int linkId) public async Task<List<EFPenalty>> ActivePenaltiesByRecentIdentifiers(int linkId)
{ {
await using var context = _contextFactory.CreateContext(false); await using var context = _contextFactory.CreateContext(false);
@ -214,12 +214,12 @@ namespace SharedLibraryCore.Services
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync(); 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(); await using var context = _contextFactory.CreateContext();
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var activePenalties = await GetActivePenaltiesByIdentifier(ipAddress, networkId); var activePenalties = await GetActivePenaltiesByIdentifier(ipAddress, networkId, game);
if (activePenalties.Any()) if (activePenalties.Any())
{ {

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId> <PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2022.3.23.1</Version> <Version>2022.6.16.1</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description> <Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2022.3.23.1</PackageVersion> <PackageVersion>2022.6.16.1</PackageVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn> <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup> </PropertyGroup>

Some files were not shown because too many files have changed in this diff Show More