Compare commits

...

117 Commits

Author SHA1 Message Date
76925a78d4 possible improvements for game interface rcon operations 2022-10-13 13:53:28 -05:00
7b869a3f43 bump plugin shared library core reference version 2022-10-13 13:53:28 -05:00
0ce9dec3ea fix issue with new remote command execution 2022-10-13 13:29:39 -05:00
069e6a0517 improve penalty colors 2022-10-13 13:29:39 -05:00
778feb8024 Fixed [JsonIgnore]
Fixed migration penalty creation
Fixed on migration command execution
Moved out CreatePenalty
Removed ClientId & AdminId since handled by Penalties
2022-10-13 13:29:39 -05:00
44f22dae3a update mute plugin to utilize new interaction forms
bump shared library core version
2022-10-13 13:29:39 -05:00
cf3209e1d0 Added !unmute, !tempmute, !listmutes
Quick fix for PowerShell IE use

Makes date readable for target player

Resolved translation string inconsistencies

Minor code cleanups

Initial commit from review

Cleaned up code & amended a few checks

Comment typo

Fix infinite unmuting

Removed unnecessary checks (Unmuting an already unmuted player will not trigger MuteStateMeta creation (if already doesn't exist))
Resolved !listmutes showing expired mutes

Committing before refactor

Refactor from review

Removed reference to AdditionalProperty

Fix check for meta state when unmuting

Continued request solves main problem

Handle potential failed command execution

Missed CommandExecuted onJoin

Fix another PS Reference to Invoke-WebRequest

Fixes review issues & Cleaned up code
Adds support for Intercepting Commands via Plugin (Credit: @RaidMax)

Comparing

Revert formatting changes

Removing MuteList for Penalty
Added Mute, TempMute & Unmute Penalty

Fixed reference in Mute.csproj & Removed ListMutesCommand.cs
2022-10-13 13:29:39 -05:00
a15da15d3e fix issue with vpn detection using new interaction 2022-10-13 10:47:25 -05:00
3b83729457 add level color coding to target on penalty list for issue #265 2022-10-13 10:41:51 -05:00
407ce2bc8f fix argument call to interactions 2022-10-13 10:26:22 -05:00
24d91f228b update interactions to allow building custom forms 2022-10-12 21:06:18 -05:00
53cbd11008 update shared library to fix data library issue 2022-10-12 12:14:43 -05:00
186db53bad update plugins to support command interception 2022-10-12 10:32:45 -05:00
40466f84c4 add command interceptor functionality 2022-10-11 16:18:56 -05:00
bdb5a1c5f8 Merge branch 'release/pre' of github.com:RaidMax/IW4M-Admin into release/pre 2022-10-05 09:51:24 -05:00
5d9e2b3bf1 Game Interface ported to T5. (#254)
* Implement game interface for IW5 and T5
2022-10-05 09:49:00 -05:00
1cf99869f6 remove unneeded check for has permission 2022-09-24 10:22:05 -05:00
12da0f463b add client tag to default game interface data 2022-09-24 10:06:07 -05:00
e88071684d provide client tag in game interface meta 2022-09-21 13:04:15 -05:00
cd6097d133 default user permission for guest requests 2022-09-19 22:01:34 -05:00
d5cf4451a2 NoClip Fix - Removed NoClipOff - Toggle Hide (#263)
* Usage of Hide is now consistent with NoClip; toggleable
Removed obsolete !NoClipOff
2022-09-11 11:51:10 -05:00
1e1e8bbe7b fix issue with game interface meta/provide full example 2022-09-11 11:46:13 -05:00
dadd236069 upgrade nuget packages 2022-09-09 09:45:46 -05:00
2380f23dbe implement profile interaction registration through plugins (mute and vpn detection implementation) 2022-09-08 15:03:38 -05:00
3cffdfdd9d Merge branch 'release/pre' of github.com:RaidMax/IW4M-Admin into release/pre 2022-09-07 09:16:58 -05:00
400c5d1f4d increase security on webfront cookie state/update events 2022-09-06 15:44:13 -05:00
ca35fbb19f iw4x integration - add delay before sending up persistent data 2022-08-31 16:17:02 -05:00
809cb0b7f4 account for trailing color code on long cod4x names 2022-08-27 21:25:42 -05:00
18f23fd07d Adding Mute for IW4x (#257)
* Adding Mute for IW4x
2022-08-26 12:09:33 -05:00
7526f86dab fix issues with game interface reconnecting after rcon connection lost 2022-08-26 12:07:43 -05:00
527ffbaced actual fix of setpassword from web console 2022-08-20 11:34:52 -05:00
6f086ac565 modularize the game integration files and better organize the anticheat folder structure 2022-08-20 10:57:03 -05:00
cf4dd6a868 fix issue with set password 2022-08-20 10:42:34 -05:00
3efafa24ff map the g_password dvar for T7 parser 2022-08-17 21:57:13 -05:00
fe919251fb add chat/chatteam event mapping for T7 2022-08-16 18:37:35 -05:00
a67f7f9351 don't display client banned on webfront if a linked ban has been revoked but they haven't reconnected yet 2022-07-25 11:54:55 -05:00
e99ca3c140 add more cases to "About" regex rule numbering scheme 2022-07-25 10:33:44 -05:00
ccedb01e8d improve help display and add supported games list 2022-07-25 10:21:08 -05:00
841bcf6156 tweak for T6 parser 2022-07-25 09:10:12 -05:00
b381af5fba fix dvar regex for T7 2022-07-24 13:29:40 -05:00
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
f235d0fafd update for pluto t5 rcon issue 2022-06-03 17:01:58 -05:00
7ecf516278 add plutonium T5 parser. Must use ManualLogPath 2022-06-03 16:26:58 -05:00
210f1ca336 fix incorrect wildcard colorcode 2022-06-02 19:59:09 -05:00
206 changed files with 22405 additions and 2533 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

@ -26,12 +26,12 @@
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2038" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="RestEase" Version="1.5.5" />
<PackageReference Include="RestEase" Version="1.5.7" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
</ItemGroup>

View File

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

View File

@ -7,6 +7,6 @@ foreach($localization in $localizations)
{
$url = "http://api.raidmax.org:5000/localization/{0}" -f $localization
$filePath = "{0}Localization\IW4MAdmin.{1}.json" -f $OutputDir, $localization
$response = Invoke-WebRequest $url
$response = Invoke-WebRequest $url -UseBasicParsing
Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8
}
}

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.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Misc;
using IW4MAdmin.Application.Alerts;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -16,19 +19,66 @@ namespace IW4MAdmin.Application.Commands
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
private readonly IAlertManager _alertManager;
private const short MaxLength = 1024;
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout)
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger, IAlertManager alertManager)
: base(config, layout)
{
Name = "offlinemessage";
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
Alias = "om";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
_contextFactory = contextFactory;
_logger = logger;
_alertManager = alertManager;
_alertManager.RegisterStaticAlertSource(async () =>
{
var context = contextFactory.CreateContext(false);
return await context.InboxMessages.Where(message => !message.IsDelivered)
.Where(message => message.CreatedDateTime >= DateTime.UtcNow.AddDays(-7))
.Where(message => message.DestinationClient.Level > EFClient.Permission.User)
.Select(message => new Alert.AlertState
{
OccuredAt = message.CreatedDateTime,
Message = message.Message,
ExpiresAt = DateTime.UtcNow.AddDays(7),
Category = Alert.AlertCategory.Message,
Source = message.SourceClient.CurrentAlias.Name.StripColors(),
SourceId = message.SourceClientId,
RecipientId = message.DestinationClientId,
ReferenceId = message.InboxMessageId,
Type = nameof(EFInboxMessage)
}).ToListAsync();
});
_alertManager.OnAlertConsumed += (_, state) =>
{
if (state.Category != Alert.AlertCategory.Message || state.ReferenceId is null)
{
return;
}
try
{
var context = contextFactory.CreateContext(true);
foreach (var message in context.InboxMessages
.Where(message => message.InboxMessageId == state.ReferenceId.Value).ToList())
{
message.IsDelivered = true;
}
context.SaveChanges();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not update message state for alert {@Alert}", state);
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
@ -38,23 +88,24 @@ namespace IW4MAdmin.Application.Commands
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
return;
}
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
return;
}
if (gameEvent.Target.IsIngame)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"].FormatExt(gameEvent.Target.Name));
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"]
.FormatExt(gameEvent.Target.Name));
return;
}
await using var context = _contextFactory.CreateContext(enableTracking: false);
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
var newMessage = new EFInboxMessage()
var newMessage = new EFInboxMessage
{
SourceClientId = gameEvent.Origin.ClientId,
DestinationClientId = gameEvent.Target.ClientId,
@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands
Message = gameEvent.Data,
};
_alertManager.AddAlert(gameEvent.Target.BuildAlert(Alert.AlertCategory.Message)
.WithMessage(gameEvent.Data.Trim())
.FromClient(gameEvent.Origin)
.OfType(nameof(EFInboxMessage))
.ExpiresIn(TimeSpan.FromDays(7)));
try
{
context.Set<EFInboxMessage>().Add(newMessage);
@ -75,4 +132,4 @@ namespace IW4MAdmin.Application.Commands
}
}
}
}
}

View File

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

View File

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

View File

@ -105,6 +105,8 @@ namespace IW4MAdmin.Application.EventParsers
{
{"say", GameEvent.EventType.Say},
{"sayteam", GameEvent.EventType.Say},
{"chat", GameEvent.EventType.Say},
{"chatteam", GameEvent.EventType.Say},
{"K", GameEvent.EventType.Kill},
{"D", GameEvent.EventType.Damage},
{"J", GameEvent.EventType.PreConnect},

View File

@ -9,6 +9,7 @@ using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@ -24,8 +25,10 @@ using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models;
using Data.Models.Server;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Commands;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Alerts;
using static Data.Models.Client.EFClient;
namespace IW4MAdmin
@ -73,7 +76,7 @@ namespace IW4MAdmin
{
ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber);
EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId);
var client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId, GameName);
// first time client is connecting to server
if (client == null)
@ -116,7 +119,7 @@ namespace IW4MAdmin
public override async Task OnClientDisconnected(EFClient client)
{
if (!GetClientsAsList().Any(_client => _client.NetworkId == client.NetworkId))
if (GetClientsAsList().All(eachClient => eachClient.NetworkId != client.NetworkId))
{
using (LogContext.PushProperty("Server", ToString()))
{
@ -152,11 +155,9 @@ namespace IW4MAdmin
{
if (E.IsBlocking)
{
await E.Origin?.Lock();
await E.Origin.Lock();
}
bool canExecuteCommand = true;
try
{
if (!await ProcessEvent(E))
@ -164,53 +165,52 @@ namespace IW4MAdmin
return;
}
Command C = null;
Command command = null;
if (E.Type == GameEvent.EventType.Command)
{
try
{
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
command = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
}
catch (CommandException e)
{
ServerLogger.LogWarning(e, "Error validating command from event {@event}",
ServerLogger.LogWarning(e, "Error validating command from event {@Event}",
new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId });
E.FailReason = GameEvent.EventFailReason.Invalid;
}
if (C != null)
if (command != null)
{
E.Extra = C;
E.Extra = command;
}
}
try
var canExecuteCommand = Manager.CommandInterceptors.All(interceptor =>
{
var loginPlugin = Manager.Plugins.FirstOrDefault(_plugin => _plugin.Name == "Login");
if (loginPlugin != null)
try
{
await loginPlugin.OnEventAsync(E, this);
return interceptor(E);
}
catch
{
return true;
}
});
if (!canExecuteCommand)
{
E.Origin.Tell(_translationLookup["SERVER_COMMANDS_INTERCEPTED"]);
}
catch (AuthorizationException e)
else if (E.Type == GameEvent.EventType.Command && E.Extra is Command cmd)
{
E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
canExecuteCommand = false;
}
// hack: this prevents commands from getting executing that 'shouldn't' be
if (E.Type == GameEvent.EventType.Command && E.Extra is Command command &&
(canExecuteCommand || E.Origin?.Level == Permission.Console))
{
ServerLogger.LogInformation("Executing command {comamnd} for {client}", command.Name, E.Origin.ToString());
await command.ExecuteAsync(E);
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name,
E.Origin.ToString());
await cmd.ExecuteAsync(E);
}
var pluginTasks = Manager.Plugins
.Where(_plugin => _plugin.Name != "Login")
.Select(async plugin => await CreatePluginTask(plugin, E));
await Task.WhenAll(pluginTasks);
@ -306,8 +306,16 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Error)
.FromSource("System")
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
}
Throttled = true;
}
@ -318,7 +326,15 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Information)
.FromSource("System")
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
}
if (!string.IsNullOrEmpty(CustomSayName))
@ -355,9 +371,9 @@ namespace IW4MAdmin
var clientTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
EFMeta.ClientTagNameV2, E.Origin.ClientId, Manager.CancellationToken);
if (clientTag?.LinkedMeta != null)
if (clientTag?.Value != null)
{
E.Origin.Tag = clientTag.LinkedMeta.Value;
E.Origin.Tag = clientTag.Value;
}
try
@ -431,7 +447,7 @@ namespace IW4MAdmin
Clients[E.Origin.ClientNumber] = E.Origin;
try
{
E.Origin.GameName = (Reference.Game?)GameName;
E.Origin.GameName = (Reference.Game)GameName;
E.Origin = await OnClientConnected(E.Origin);
E.Target = E.Origin;
}
@ -499,7 +515,7 @@ namespace IW4MAdmin
E.Target.SetLevel(Permission.User, E.Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
E.Target.CurrentAlias?.IPAddress);
E.Target.GameName, E.Target.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unflagPenalty);
}
@ -671,23 +687,50 @@ namespace IW4MAdmin
else
{
Gametype = dict["gametype"];
Hostname = dict["hostname"];
if (dict.ContainsKey("gametype"))
{
Gametype = dict["gametype"];
}
string mapname = dict["mapname"] ?? CurrentMap.Name;
UpdateMap(mapname);
if (dict.ContainsKey("hostname"))
{
Hostname = dict["hostname"];
}
var newMapName = dict.ContainsKey("mapname")
? dict["mapname"] ?? CurrentMap.Name
: CurrentMap.Name;
UpdateMap(newMapName);
}
}
else
{
var dict = (Dictionary<string, string>) E.Extra;
Gametype = dict["g_gametype"];
Hostname = dict["sv_hostname"];
MaxClients = int.Parse(dict["sv_maxclients"]);
var dict = (Dictionary<string, string>)E.Extra;
if (dict.ContainsKey("g_gametype"))
{
Gametype = dict["g_gametype"];
}
string mapname = dict["mapname"];
UpdateMap(mapname);
if (dict.ContainsKey("sv_hostname"))
{
Hostname = dict["sv_hostname"];
}
if (dict.ContainsKey("sv_maxclients"))
{
MaxClients = int.Parse(dict["sv_maxclients"]);
}
else if (dict.ContainsKey("com_maxclients"))
{
MaxClients = int.Parse(dict["com_maxclients"]);
}
if (dict.ContainsKey("mapname"))
{
UpdateMap(dict["mapname"]);
}
}
if (E.GameTime.HasValue)
@ -723,6 +766,34 @@ namespace IW4MAdmin
{
E.Origin.UpdateTeam(E.Extra as string);
}
else if (E.Type == GameEvent.EventType.MetaUpdated)
{
if (E.Extra is "PersistentClientGuid")
{
var parts = E.Data.Split(",");
if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
int.TryParse(parts[1], out var low))
{
var guid = long.Parse(high.ToString("X") + low.ToString("X"), NumberStyles.HexNumber);
var penalties = await Manager.GetPenaltyService()
.GetActivePenaltiesByIdentifier(null, guid, (Reference.Game)GameName);
var banPenalty =
penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
if (banPenalty is not null && E.Origin.Level != Permission.Banned)
{
ServerLogger.LogInformation(
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
E.Origin.ToString(), guid);
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(guid),
Utilities.IW4MAdminClient(this), true);
}
}
}
}
lock (ChatHistory)
{
@ -745,7 +816,7 @@ namespace IW4MAdmin
private async Task OnClientUpdate(EFClient origin)
{
var client = Manager.GetActiveClients().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
var client = GetClientsAsList().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
if (client == null)
{
@ -790,12 +861,10 @@ namespace IW4MAdmin
/// array index 2 = updated clients
/// </summary>
/// <returns></returns>
async Task<List<EFClient>[]> PollPlayersAsync()
async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
var currentClients = GetClientsAsList();
var statusResponse = await this.GetStatusAsync(tokenSource.Token);
var statusResponse = await this.GetStatusAsync(token);
if (statusResponse is null)
{
@ -918,11 +987,11 @@ namespace IW4MAdmin
private DateTime _lastMessageSent = DateTime.Now;
private DateTime _lastPlayerCount = DateTime.Now;
public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
public override async Task<bool> ProcessUpdatesAsync(CancellationToken token)
{
try
{
if (cts.IsCancellationRequested)
if (token.IsCancellationRequested)
{
await ShutdownInternal();
return true;
@ -936,7 +1005,7 @@ namespace IW4MAdmin
return true;
}
var polledClients = await PollPlayersAsync();
var polledClients = await PollPlayersAsync(token);
if (polledClients is null)
{
@ -947,7 +1016,7 @@ namespace IW4MAdmin
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
{
disconnectingClient.CurrentServer = this;
var e = new GameEvent()
var e = new GameEvent
{
Type = GameEvent.EventType.PreDisconnect,
Origin = disconnectingClient,
@ -964,7 +1033,7 @@ namespace IW4MAdmin
!string.IsNullOrEmpty(client.Name) && (client.Ping != 999 || client.IsBot)))
{
client.CurrentServer = this;
client.GameName = (Reference.Game?)GameName;
client.GameName = (Reference.Game)GameName;
var e = new GameEvent
{
@ -1229,28 +1298,17 @@ namespace IW4MAdmin
this.GamePassword = gamePassword.Value;
UpdateMap(mapname);
if (RconParser.CanGenerateLogPath)
if (RconParser.CanGenerateLogPath && string.IsNullOrEmpty(ServerConfig.ManualLogPath))
{
bool needsRestart = false;
if (logsync.Value == 0)
{
await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4
needsRestart = true;
}
if (string.IsNullOrWhiteSpace(logfile.Value))
{
logfile.Value = "games_mp.log";
await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken);
needsRestart = true;
}
if (needsRestart)
{
// disabling this for the time being
/*Logger.WriteWarning("Game log file not properly initialized, restarting map...");
await this.ExecuteCommandAsync("map_restart");*/
}
// this DVAR isn't set until the a map is loaded
@ -1456,6 +1514,11 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Creating tempban penalty for {TargetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
foreach (var reports in Manager.GetServers().Select(server => server.Reports))
{
reports.RemoveAll(report => report.Target.ClientId == targetClient.ClientId);
}
if (activeClient.IsIngame)
{
@ -1486,6 +1549,11 @@ namespace IW4MAdmin
activeClient.SetLevel(Permission.Banned, originClient);
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
foreach (var reports in Manager.GetServers().Select(server => server.Reports))
{
reports.RemoveAll(report => report.Target.ClientId == targetClient.ClientId);
}
if (activeClient.IsIngame)
{
ServerLogger.LogDebug("Attempting to kicking newly banned client {ActiveClient}", activeClient.ToString());
@ -1514,7 +1582,7 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString());
targetClient.SetLevel(Permission.User, originClient);
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
targetClient.NetworkId, targetClient.CurrentAlias?.IPAddress);
targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unbanPenalty);
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,131 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
using InteractionRegistrationCallback =
System.Func<int?, Data.Models.Reference.Game?, System.Threading.CancellationToken,
System.Threading.Tasks.Task<SharedLibraryCore.Interfaces.IInteractionData>>;
namespace IW4MAdmin.Application.Misc;
public class InteractionRegistration : IInteractionRegistration
{
private readonly ILogger<InteractionRegistration> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly ConcurrentDictionary<string, InteractionRegistrationCallback> _interactions = new();
public InteractionRegistration(ILogger<InteractionRegistration> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration)
{
var plugin = _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>()
.FirstOrDefault(plugin => plugin.Name == source);
if (plugin is not ScriptPlugin scriptPlugin)
{
return;
}
var wrappedDelegate = (int? clientId, Reference.Game? game, CancellationToken token) =>
Task.FromResult(
scriptPlugin.WrapDelegate<IInteractionData>(interactionRegistration, clientId, game, token));
if (!_interactions.ContainsKey(interactionName))
{
_interactions.TryAdd(interactionName, wrappedDelegate);
}
else
{
_interactions[interactionName] = wrappedDelegate;
}
}
public void RegisterInteraction(string interactionName, InteractionRegistrationCallback interactionRegistration)
{
if (!_interactions.ContainsKey(interactionName))
{
_interactions.TryAdd(interactionName, interactionRegistration);
}
else
{
_interactions[interactionName] = interactionRegistration;
}
}
public void UnregisterInteraction(string interactionName)
{
if (_interactions.ContainsKey(interactionName))
{
_interactions.TryRemove(interactionName, out _);
}
}
public async Task<IEnumerable<IInteractionData>> GetInteractions(int? clientId = null,
Reference.Game? game = null, CancellationToken token = default)
{
return (await Task.WhenAll(_interactions.Select(async kvp =>
{
try
{
return await kvp.Value(clientId, game, token);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not get interaction for interaction {InteractionName} and ClientId {ClientId}", kvp.Key,
clientId);
return null;
}
}))).Where(interaction => interaction is not null);
}
public async Task<string> ProcessInteraction(string interactionId, int originId, int? targetId = null,
Reference.Game? game = null, IDictionary<string, string> meta = null, CancellationToken token = default)
{
if (!_interactions.ContainsKey(interactionId))
{
throw new ArgumentException($"Interaction with ID {interactionId} has not been registered");
}
try
{
var interaction = await _interactions[interactionId](targetId, game, token);
if (interaction.Action is not null)
{
return await interaction.Action(originId, targetId, game, meta, token);
}
if (interaction.ScriptAction is not null)
{
foreach (var plugin in _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>())
{
if (plugin is not ScriptPlugin scriptPlugin || scriptPlugin.Name != interaction.Source)
{
continue;
}
return scriptPlugin.ExecuteAction<string>(interaction.ScriptAction, originId, targetId, game, meta, token);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not process interaction for interaction {InteractionName} and OriginId {ClientId}",
interactionId, originId);
}
return null;
}
}

View File

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

View File

@ -0,0 +1,88 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Services;
namespace IW4MAdmin.Application.Misc;
public class RemoteCommandService : IRemoteCommandService
{
private readonly ApplicationConfiguration _appConfig;
private readonly ClientService _clientService;
public RemoteCommandService(ApplicationConfiguration appConfig, ClientService clientService)
{
_appConfig = appConfig;
_clientService = clientService;
}
public async Task<IEnumerable<CommandResponseInfo>> Execute(int originId, int? targetId, string command,
IEnumerable<string> arguments, Server server)
{
var client = await _clientService.Get(originId);
client.CurrentServer = server;
command += $" {(targetId.HasValue ? $"@{targetId} " : "")}{string.Join(" ", arguments ?? Enumerable.Empty<string>())}";
var remoteEvent = new GameEvent
{
Type = GameEvent.EventType.Command,
Data = command.StartsWith(_appConfig.CommandPrefix) ||
command.StartsWith(_appConfig.BroadcastCommandPrefix)
? command
: $"{_appConfig.CommandPrefix}{command}",
Origin = client,
Owner = server,
IsRemote = true
};
server.Manager.AddEvent(remoteEvent);
CommandResponseInfo[] response;
try
{
// wait for the event to process
var completedEvent =
await remoteEvent.WaitAsync(Utilities.DefaultCommandTimeout, server.Manager.CancellationToken);
if (completedEvent.FailReason == GameEvent.EventFailReason.Timeout)
{
response = new[]
{
new CommandResponseInfo()
{
ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMAND_TIMEOUT"]
}
};
}
else
{
response = completedEvent.Output.Select(output => new CommandResponseInfo()
{
Response = output,
ClientId = client.ClientId
}).ToArray();
}
}
catch (System.OperationCanceledException)
{
response = new[]
{
new CommandResponseInfo
{
ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_SUCCESS"]
}
};
}
return response;
}
}

View File

@ -42,6 +42,7 @@ namespace IW4MAdmin.Application.Misc
private Engine _scriptEngine;
private readonly string _fileName;
private readonly SemaphoreSlim _onProcessing = new(1, 1);
private readonly SemaphoreSlim _onDvarActionComplete = new(1, 1);
private bool _successfullyLoaded;
private readonly List<string> _registeredCommandNames;
private readonly ILogger _logger;
@ -111,12 +112,16 @@ namespace IW4MAdmin.Application.Misc
}
_scriptEngine = new Engine(cfg =>
cfg.AllowClr(new[]
cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable))
.AllowClr(new[]
{
typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly,
typeof(Utilities).Assembly,
typeof(Encoding).Assembly
typeof(Encoding).Assembly,
typeof(CancellationTokenSource).Assembly,
typeof(Data.Models.Client.EFClient).Assembly,
typeof(IW4MAdmin.Plugins.Stats.Plugin).Assembly
})
.CatchClrExceptions()
.AddObjectConverter(new PermissionLevelToStringConverter()));
@ -337,6 +342,41 @@ namespace IW4MAdmin.Application.Misc
return Task.CompletedTask;
}
public T ExecuteAction<T>(Delegate action, params object[] param)
{
try
{
_onProcessing.Wait();
var args = param.Select(p => JsValue.FromObject(_scriptEngine, p)).ToArray();
var result = action.DynamicInvoke(JsValue.Undefined, args);
return (T)(result as JsValue)?.ToObject();
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
public T WrapDelegate<T>(Delegate act, params object[] args)
{
try
{
_onProcessing.Wait();
return (T)(act.DynamicInvoke(JsValue.Null,
args.Select(arg => JsValue.FromObject(_scriptEngine, arg)).ToArray()) as ObjectWrapper)?.ToObject();
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}
/// <summary>
/// finds declared script commands in the script plugin
/// </summary>
@ -355,6 +395,11 @@ namespace IW4MAdmin.Application.Misc
string name = dynamicCommand.name;
string alias = dynamicCommand.alias;
string description = dynamicCommand.description;
if (dynamicCommand.permission is Data.Models.Client.EFClient.Permission perm)
{
dynamicCommand.permission = perm.ToString();
}
string permission = dynamicCommand.permission;
List<Server.Game> supportedGames = null;
var targetRequired = false;
@ -438,7 +483,6 @@ namespace IW4MAdmin.Application.Misc
throw new PluginException("An error occured while executing action for script plugin");
}
finally
{
if (_onProcessing.CurrentCount == 0)
@ -457,18 +501,11 @@ namespace IW4MAdmin.Application.Misc
private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
server.BeginGetDvar(dvarName, result =>
void OnComplete(IAsyncResult result)
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
@ -478,9 +515,36 @@ namespace IW4MAdmin.Application.Misc
JsValue.FromObject(_scriptEngine, success)
});
}
catch (JavaScriptException ex)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex, "Could not complete BeginGetDvar for {Filename} {@Location}",
Path.GetFileName(_fileName), ex.Location);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not complete {BeginGetDvar} for {Class}", nameof(BeginGetDvar), Name);
}
}
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
server.BeginGetDvar(dvarName, result =>
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
}
finally
{
OnComplete(result);
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release();
@ -492,17 +556,13 @@ namespace IW4MAdmin.Application.Misc
private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
server.BeginSetDvar(dvarName, dvarValue, result =>
void OnComplete(IAsyncResult result)
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
var success = (bool)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined,
new[]
{
@ -512,8 +572,31 @@ namespace IW4MAdmin.Application.Misc
JsValue.FromObject(_scriptEngine, success)
});
}
catch (JavaScriptException ex)
{
using (LogContext.PushProperty("Server", server.ToString()))
{
_logger.LogError(ex, "Could complete BeginSetDvar for {Filename} {@Location}",
Path.GetFileName(_fileName), ex.Location);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not complete {BeginSetDvar} for {Class}", nameof(BeginSetDvar), Name);
}
}
server.BeginSetDvar(dvarName, dvarValue, result =>
{
var shouldRelease = false;
try
{
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
}
finally
{
OnComplete(result);
if (_onProcessing.CurrentCount == 0 && shouldRelease)
{
_onProcessing.Release();

View File

@ -111,7 +111,7 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
{
return;
}
_logger.LogDebug("-Releasing OnTick for timer");
_onDependentAction?.Release(1);
}
private void OnTick()
@ -128,7 +128,8 @@ public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
_onRunningTick.Reset();
// the js engine is not thread safe so we need to ensure we're not executing OnTick and OnEventAsync simultaneously
_onDependentAction?.WaitAsync().Wait();
_onDependentAction?.Wait();
_logger.LogDebug("+Running OnTick for timer");
var start = DateTime.Now;
_jsAction.DynamicInvoke(JsValue.Undefined, new[] { JsValue.Undefined });
_logger.LogDebug("OnTick took {Time}ms", (DateTime.Now - start).TotalMilliseconds);

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Server;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -22,18 +23,20 @@ namespace IW4MAdmin.Application.Misc
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache,
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache)
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
{
_logger = logger;
_snapshotCache = snapshotCache;
_serverStatsCache = serverStatsCache;
_clientHistoryCache = clientHistoryCache;
_rankedClientsCache = rankedClientsCache;
}
public async Task<(int?, DateTime?)>
@ -160,5 +163,30 @@ namespace IW4MAdmin.Application.Misc
return Enumerable.Empty<ClientHistoryInfo>();
}
}
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
{
_rankedClientsCache.SetCacheItem(async (set, cancellationToken) =>
{
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
return await set
.Where(rating => rating.Newest)
.Where(rating => rating.ServerId == serverId)
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken);
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
try
{
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(RankedClientsCountAsync));
return 0;
}
}
}
}

View File

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

View File

@ -20,6 +20,7 @@ namespace IW4MAdmin.Application.RConParsers
public class BaseRConParser : IRConParser
{
private readonly ILogger _logger;
private static string _botIpIndicator = "00000000.";
public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory)
{
@ -52,7 +53,7 @@ namespace IW4MAdmin.Application.RConParsers
Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7);
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n(?:latched: \"(.+)?\"\n)? *(.+)$";
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n?(?:latched: \"(.+)?\"\n?)? *(.+)?$";
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
@ -81,7 +82,7 @@ namespace IW4MAdmin.Application.RConParsers
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default)
{
command = command.FormatMessageForEngine(Configuration?.ColorCodeMapping);
command = command.FormatMessageForEngine(Configuration);
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token);
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
}
@ -104,7 +105,7 @@ namespace IW4MAdmin.Application.RConParsers
lineSplit = Array.Empty<string>();
}
var response = string.Join('\n', lineSplit).TrimEnd('\0');
var response = string.Join('\n', lineSplit).Replace("\n", "").TrimEnd('\0');
var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (response.Contains("Unknown command") ||
@ -146,7 +147,7 @@ namespace IW4MAdmin.Application.RConParsers
{
GetDvarAsync<string>(connection, dvarName, token: token).ContinueWith(action =>
{
if (action.Exception is null)
if (action.IsCompletedSuccessfully)
{
callback?.Invoke(new AsyncResult
{
@ -163,7 +164,7 @@ namespace IW4MAdmin.Application.RConParsers
AsyncState = (false, (string)null)
});
}
}, token);
}, CancellationToken.None);
}
public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default)
@ -175,16 +176,16 @@ namespace IW4MAdmin.Application.RConParsers
return new StatusResponse
{
Clients = ClientsFromStatus(response).ToArray(),
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus.Pattern),
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus.Pattern),
Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus.Pattern),
MaxClients = GetValueFromStatus<int?>(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus.Pattern)
Map = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusMap, Configuration.MapStatus),
GameType = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusGametype, Configuration.GametypeStatus),
Hostname = GetValueFromStatus<string>(response, ParserRegex.GroupType.RConStatusHostname, Configuration.HostnameStatus),
MaxClients = GetValueFromStatus<int?>(response, ParserRegex.GroupType.RConStatusMaxPlayers, Configuration.MaxPlayersStatus)
};
}
private T GetValueFromStatus<T>(IEnumerable<string> response, ParserRegex.GroupType groupType, string groupPattern)
private T GetValueFromStatus<T>(IEnumerable<string> response, ParserRegex.GroupType groupType, ParserRegex parserRegex)
{
if (string.IsNullOrEmpty(groupPattern))
if (string.IsNullOrEmpty(parserRegex.Pattern))
{
return default;
}
@ -192,10 +193,10 @@ namespace IW4MAdmin.Application.RConParsers
string value = null;
foreach (var line in response)
{
var regex = Regex.Match(line, groupPattern);
if (regex.Success)
var regex = Regex.Match(line, parserRegex.Pattern);
if (regex.Success && parserRegex.GroupMapping.ContainsKey(groupType))
{
value = regex.Groups[Configuration.MapStatus.GroupMapping[groupType]].ToString();
value = regex.Groups[parserRegex.GroupMapping[groupType]].ToString();
}
}
@ -226,7 +227,7 @@ namespace IW4MAdmin.Application.RConParsers
{
SetDvarAsync(connection, dvarName, dvarValue, token).ContinueWith(action =>
{
if (action.Exception is null)
if (action.Exception is null && !action.IsCanceled)
{
callback?.Invoke(new AsyncResult
{
@ -243,7 +244,7 @@ namespace IW4MAdmin.Application.RConParsers
AsyncState = false
});
}
}, token);
}, CancellationToken.None);
}
private List<EFClient> ClientsFromStatus(string[] Status)
@ -290,8 +291,15 @@ namespace IW4MAdmin.Application.RConParsers
long networkId;
var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString;
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]]
.Contains(_botIpIndicator))
{
ip = System.Net.IPAddress.Broadcast.ToString().ConvertToIP();
}
try
{
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
@ -306,9 +314,9 @@ namespace IW4MAdmin.Application.RConParsers
continue;
}
var client = new EFClient()
var client = new EFClient
{
CurrentAlias = new EFAlias()
CurrentAlias = new EFAlias
{
Name = name,
IPAddress = ip
@ -360,15 +368,28 @@ namespace IW4MAdmin.Application.RConParsers
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
default;
public TimeSpan OverrideTimeoutForCommand(string command)
public TimeSpan? OverrideTimeoutForCommand(string command)
{
if (command.Contains("map_rotate", StringComparison.InvariantCultureIgnoreCase) ||
command.StartsWith("map ", StringComparison.InvariantCultureIgnoreCase))
if (string.IsNullOrEmpty(command))
{
return TimeSpan.FromSeconds(30);
return TimeSpan.Zero;
}
var commandToken = command.Split(' ', StringSplitOptions.RemoveEmptyEntries).First().ToLower();
if (!Configuration.OverrideCommandTimeouts.ContainsKey(commandToken))
{
return TimeSpan.Zero;
}
return TimeSpan.Zero;
var timeoutValue = Configuration.OverrideCommandTimeouts[commandToken];
if (timeoutValue.HasValue && timeoutValue.Value != 0) // JINT doesn't seem to be able to properly set nulls on dictionaries
{
return TimeSpan.FromSeconds(timeoutValue.Value);
}
return null;
}
}
}

View File

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

View File

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

View File

@ -85,7 +85,15 @@ namespace Data.Context
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// make network id unique
modelBuilder.Entity<EFClient>(entity => { entity.HasIndex(e => e.NetworkId).IsUnique(); });
modelBuilder.Entity<EFClient>(entity =>
{
entity.HasIndex(e => e.NetworkId);
entity.HasAlternateKey(client => new
{
client.NetworkId,
client.GameName
});
});
modelBuilder.Entity<EFPenalty>(entity =>
{

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
@ -15,8 +17,8 @@ namespace Data.Helpers
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ConcurrentDictionary<string, CacheState<TReturnType>> _cacheStates =
new ConcurrentDictionary<string, CacheState<TReturnType>>();
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
private readonly object _defaultKey = new();
private bool _autoRefresh;
private const int DefaultExpireMinutes = 15;
@ -51,41 +53,61 @@ namespace Data.Helpers
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
TimeSpan? expirationTime = null, bool autoRefresh = false)
{
if (_cacheStates.ContainsKey(key))
{
_logger.LogDebug("Cache key {Key} is already added", key);
return;
}
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_autoRefresh = autoRefresh;
_cacheStates.TryAdd(key, state);
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
SetCacheItem(getter, key, null, expirationTime, autoRefresh);
}
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default)
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
{
ids ??= new[] { _defaultKey };
if (!_cacheStates.ContainsKey(key))
{
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
}
foreach (var id in ids)
{
if (_cacheStates[key].ContainsKey(id))
{
continue;
}
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_cacheStates[key].Add(id, state);
_autoRefresh = autoRefresh;
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
}
}
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
await GetCacheItem(keyName, null, cancellationToken);
public async Task<TReturnType> GetCacheItem(string keyName, object id = null,
CancellationToken cancellationToken = default)
{
if (!_cacheStates.ContainsKey(keyName))
{
throw new ArgumentException("No cache found for key {key}", keyName);
}
var state = _cacheStates[keyName];
var state = id is null ? _cacheStates[keyName].Values.First() : _cacheStates[keyName][id];
// when auto refresh is off we want to check the expiration and value
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
@ -115,4 +137,4 @@ namespace Data.Helpers
}
}
}
}
}

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

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

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

View File

@ -63,7 +63,7 @@ namespace Data.Models.Client
public DateTime FirstConnection { get; set; }
[Required]
public DateTime LastConnection { get; set; }
public Reference.Game? GameName { get; set; } = Reference.Game.UKN;
public Reference.Game GameName { get; set; } = Reference.Game.UKN;
public bool Masked { get; set; }
[Required]
public int AliasLinkId { get; set; }

View File

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

View File

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

View File

@ -18,6 +18,9 @@ namespace Data.Models
Unban,
Any,
Unflag,
Mute,
TempMute,
Unmute,
Other = 100
}

View File

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

View File

@ -9,7 +9,7 @@ foreach($localization in $localizations)
{
$url = "http://api.raidmax.org:5000/localization/{0}" -f $localization
$filePath = "{0}\Localization\IW4MAdmin.{1}.json" -f $PublishDir, $localization
$response = Invoke-WebRequest $url
$response = Invoke-WebRequest $url -UseBasicParsing
Out-File -FilePath $filePath -InputObject $response.Content -Encoding utf8
}
@ -20,4 +20,4 @@ Minor = $versionInfo.ProductMinorPart
Build = $versionInfo.ProductBuildPart
Revision = $versionInfo.ProductPrivatePart
}
$json | ConvertTo-Json | Out-File -FilePath ("{0}\VersionInformation.json" -f $PublishDir) -Encoding ASCII
$json | ConvertTo-Json | Out-File -FilePath ("{0}\VersionInformation.json" -f $PublishDir) -Encoding ASCII

View File

@ -39,7 +39,7 @@ else
Write-Output "Retrieving latest version info..."
$releaseInfo = (Invoke-WebRequest $releasesUri | ConvertFrom-Json) | Select -First 1
$releaseInfo = (Invoke-WebRequest $releasesUri -UseBasicParsing | ConvertFrom-Json) | Select -First 1
$asset = $releaseInfo.assets | Where-Object name -like $assetPattern | Select -First 1
$downloadUri = $asset.browser_download_url
$filename = Split-Path $downloadUri -leaf
@ -55,7 +55,7 @@ if (!$Silent)
Write-Output "Downloading update. This might take a moment..."
$fileDownload = Invoke-WebRequest -Uri $downloadUri
$fileDownload = Invoke-WebRequest -Uri $downloadUri -UseBasicParsing
if ($fileDownload.StatusDescription -ne "OK")
{
throw "Could not update IW4MAdmin. ($fileDownload.StatusDescription)"

View File

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

View File

@ -0,0 +1,23 @@
# IW5
This expands IW4M-Admins's Anti-cheat to Plutonium IW5
## Installation
Add ``_customcallbacks.gsc`` into the scripts folder. (%localappdata%\Plutonium\storage\iw5\scripts)
For more info check out Chase's [how-to guide](https://forum.plutonium.pw/topic/10738/tutorial-loading-custom-gsc-scripts).
You need to add this to you ``StatsPluginSettings.json`` found in your IW4M-Admin configuration folder.
```
"IW5": {
"Recoil": [
"iw5_1887_mp.*",
"turret_minigun_mp"
],
"Button": [
".*akimbo.*"
]
}
```
[Example](https://imgur.com/Ji9AafI)

View File

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

View File

@ -24,12 +24,12 @@ Add this to the WeaponNameParserConfigurations List in the StatsPluginSettings.j
}
```
Now create the following entry for __EVERY__ T6 server you are using this on in the ServerDetectionTypes list:
Now update the `GameDetectionTypes` list with the following, if it does not already exist:
```
"1270014976": [
"T6": [
"Offset",
"Strain",
"Snap"
]
```
"Snap",
"Strain"
]
```

View File

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

View File

@ -0,0 +1,718 @@
#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\gametypes\_hud_util;
Init()
{
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
// setup default vars
level.eventBus = spawnstruct();
level.eventBus.inVar = "sv_iw4madmin_in";
level.eventBus.outVar = "sv_iw4madmin_out";
level.eventBus.failKey = "fail";
level.eventBus.timeoutKey = "timeout";
level.eventBus.timeout = 30;
level.commonFunctions = spawnstruct();
level.commonFunctions.SetDvar = "SetDvarIfUninitialized";
level.notifyTypes = spawnstruct();
level.notifyTypes.gameFunctionsInitialized = "GameFunctionsInitialized";
level.notifyTypes.integrationBootstrapInitialized = "IntegrationBootstrapInitialized";
level.clientDataKey = "clientData";
level.eventTypes = spawnstruct();
level.eventTypes.localClientEvent = "client_event";
level.eventTypes.clientDataReceived = "ClientDataReceived";
level.eventTypes.clientDataRequested = "ClientDataRequested";
level.eventTypes.setClientDataRequested = "SetClientDataRequested";
level.eventTypes.setClientDataCompleted = "SetClientDataCompleted";
level.eventTypes.executeCommandRequested = "ExecuteCommandRequested";
level.iw4madminIntegrationDebug = 0;
// map the event type to the handler
level.eventCallbacks = [];
level.eventCallbacks[level.eventTypes.clientDataReceived] = ::OnClientDataReceived;
level.eventCallbacks[level.eventTypes.executeCommandRequested] = ::OnExecuteCommand;
level.eventCallbacks[level.eventTypes.setClientDataCompleted] = ::OnSetClientDataCompleted;
level.clientCommandCallbacks = [];
level.clientCommandRusAsTarget = [];
level.logger = spawnstruct();
level.overrideMethods = [];
level.iw4madminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" );
InitializeLogger();
wait ( 0.05 ); // needed to give script engine time to propagate notifies
level notify( level.notifyTypes.integrationBootstrapInitialized );
level waittill( level.notifyTypes.gameFunctionsInitialized );
LogDebug( "Integration received notify that game functions are initialized" );
_SetDvarIfUninitialized( level.eventBus.inVar, "" );
_SetDvarIfUninitialized( level.eventBus.outVar, "" );
_SetDvarIfUninitialized( "sv_iw4madmin_integration_enabled", 1 );
_SetDvarIfUninitialized( "sv_iw4madmin_integration_debug", 0 );
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
// start long running tasks
level thread MonitorClientEvents();
level thread MonitorBus();
level thread OnPlayerConnect();
}
//////////////////////////////////
// Client Methods
//////////////////////////////////
OnPlayerConnect()
{
level endon ( "game_ended" );
for ( ;; )
{
level waittill( "connected", player );
if ( _IsBot( player ) )
{
// we don't want to track bots
continue;
}
if ( !IsDefined( player.pers[level.clientDataKey] ) )
{
player.pers[level.clientDataKey] = spawnstruct();
}
player thread OnPlayerSpawned();
player thread OnPlayerJoinedTeam();
player thread OnPlayerJoinedSpectators();
player thread PlayerTrackingOnInterval();
}
}
OnPlayerSpawned()
{
self endon( "disconnect" );
for ( ;; )
{
self waittill( "spawned_player" );
self PlayerSpawnEvents();
}
}
OnPlayerDisconnect()
{
self endon ( "disconnect" );
for ( ;; )
{
self waittill( "disconnect" );
self SaveTrackingMetrics();
}
}
OnPlayerJoinedTeam()
{
self endon( "disconnect" );
for( ;; )
{
self waittill( "joined_team" );
// join spec and join team occur at the same moment - out of order logging would be problematic
wait( 0.25 );
LogPrint( GenerateJoinTeamString( false ) );
}
}
OnPlayerJoinedSpectators()
{
self endon( "disconnect" );
for( ;; )
{
self waittill( "joined_spectators" );
LogPrint( GenerateJoinTeamString( true ) );
}
}
OnGameEnded()
{
for ( ;; )
{
level waittill( "game_ended" );
// note: you can run data code here but it's possible for
// data to get truncated, so we will try a timer based approach for now
}
}
DisplayWelcomeData()
{
self endon( "disconnect" );
clientData = self.pers[level.clientDataKey];
if ( clientData.permissionLevel == "User" || clientData.permissionLevel == "Flagged" )
{
return;
}
self IPrintLnBold( "Welcome, your level is ^5" + clientData.permissionLevel );
wait( 2.0 );
self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection );
}
PlayerSpawnEvents()
{
self endon( "disconnect" );
clientData = self.pers[level.clientDataKey];
// this gives IW4MAdmin some time to register the player before making the request;
// although probably not necessary some users might have a slow database or poll rate
wait ( 2 );
if ( IsDefined( clientData.state ) && clientData.state == "complete" )
{
return;
}
self RequestClientBasicData();
}
PlayerTrackingOnInterval()
{
self endon( "disconnect" );
for ( ;; )
{
wait ( 120 );
if ( IsAlive( self ) )
{
self SaveTrackingMetrics();
}
}
}
MonitorClientEvents()
{
level endon( "game_ended" );
for ( ;; )
{
level waittill( level.eventTypes.localClientEvent, client );
LogDebug( "Processing Event " + client.event.type + "-" + client.event.subtype );
eventHandler = level.eventCallbacks[client.event.type];
if ( IsDefined( eventHandler ) )
{
client [[eventHandler]]( client.event );
LogDebug( "notify client for " + client.event.type );
client notify( level.eventTypes.localClientEvent, client.event );
}
client.eventData = [];
}
}
//////////////////////////////////
// Helper Methods
//////////////////////////////////
_IsBot( entity )
{
// there already is a cgame function exists as "IsBot", for IW4, but unsure what all titles have it defined,
// so we are defining it here
return IsDefined( entity.pers["isBot"] ) && entity.pers["isBot"];
}
_SetDvarIfUninitialized( dvarName, dvarValue )
{
[[level.overrideMethods[level.commonFunctions.SetDvar]]]( dvarName, dvarValue );
}
// Not every game can output to console or even game log.
// Adds a very basic logging system that every
// game specific script can extend.accumulate
// Logging to dvars used as example.
InitializeLogger()
{
level.logger._logger = [];
RegisterLogger( ::Log2Dvar );
RegisterLogger( ::Log2IngamePrint );
level.logger.debug = ::LogDebug;
level.logger.error = ::LogError;
level.logger.warning = ::LogWarning;
}
_Log( LogLevel, message )
{
for( i = 0; i < level.logger._logger.size; i++ )
{
[[level.logger._logger[i]]]( LogLevel, message );
}
}
LogDebug( message )
{
if ( level.iw4madminIntegrationDebug )
{
_Log( "debug", level.eventBus.gamename + ": " + message );
}
}
LogError( message )
{
_Log( "error", message );
}
LogWarning( message )
{
_Log( "warning", message );
}
Log2Dvar( LogLevel, message )
{
switch ( LogLevel )
{
case "debug":
SetDvar( "sv_iw4madmin_last_debug", message );
break;
case "error":
SetDvar( "sv_iw4madmin_last_error", message );
break;
case "warning":
SetDvar( "sv_iw4madmin_last_warning", message );
break;
}
}
Log2IngamePrint( LogLevel, message )
{
switch ( LogLevel )
{
case "debug":
IPrintLn( "[DEBUG] " + message );
break;
case "error":
IPrintLn( "[ERROR] " + message );
break;
case "warning":
IPrintLn( "[WARN] " + message );
break;
}
}
RegisterLogger( logger )
{
level.logger._logger[level.logger._logger.size] = logger;
}
RequestClientMeta( metaKey )
{
getClientMetaEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "Meta", self, metaKey );
level thread QueueEvent( getClientMetaEvent, level.eventTypes.clientDataRequested, self );
}
RequestClientBasicData()
{
getClientDataEvent = BuildEventRequest( true, level.eventTypes.clientDataRequested, "None", self, "" );
level thread QueueEvent( getClientDataEvent, level.eventTypes.clientDataRequested, self );
}
IncrementClientMeta( metaKey, incrementValue, clientId )
{
SetClientMeta( metaKey, incrementValue, clientId, "increment" );
}
DecrementClientMeta( metaKey, decrementValue, clientId )
{
SetClientMeta( metaKey, decrementValue, clientId, "decrement" );
}
GenerateJoinTeamString( isSpectator )
{
team = self.team;
if ( IsDefined( self.joining_team ) )
{
team = self.joining_team;
}
else
{
if ( isSpectator || !IsDefined( team ) )
{
team = "spectator";
}
}
guid = self GetXuid();
if ( guid == "0" )
{
guid = self.guid;
}
if ( !IsDefined( guid ) || guid == "0" )
{
guid = "undefined";
}
return "JT;" + guid + ";" + self getEntityNumber() + ";" + team + ";" + self.name + "\n";
}
SetClientMeta( metaKey, metaValue, clientId, direction )
{
data = "key=" + metaKey + "|value=" + metaValue;
clientNumber = -1;
if ( IsDefined ( clientId ) )
{
data = data + "|clientId=" + clientId;
clientNumber = -1;
}
if ( IsDefined( direction ) )
{
data = data + "|direction=" + direction;
}
if ( IsPlayer( self ) )
{
clientNumber = self getEntityNumber();
}
setClientMetaEvent = BuildEventRequest( true, level.eventTypes.setClientDataRequested, "Meta", clientNumber, data );
level thread QueueEvent( setClientMetaEvent, level.eventTypes.setClientDataRequested, self );
}
SaveTrackingMetrics()
{
if ( !IsDefined( self.persistentClientId ) )
{
return;
}
LogDebug( "Saving tracking metrics for " + self.persistentClientId );
if ( !IsDefined( self.lastShotCount ) )
{
self.lastShotCount = 0;
}
currentShotCount = self [[level.overrideMethods["GetTotalShotsFired"]]]();
change = currentShotCount - self.lastShotCount;
self.lastShotCount = currentShotCount;
LogDebug( "Total Shots Fired increased by " + change );
if ( !IsDefined( change ) )
{
change = 0;
}
if ( change == 0 )
{
return;
}
IncrementClientMeta( "TotalShotsFired", change, self.persistentClientId );
}
BuildEventRequest( responseExpected, eventType, eventSubtype, entOrId, data )
{
if ( !IsDefined( data ) )
{
data = "";
}
if ( !IsDefined( eventSubtype ) )
{
eventSubtype = "None";
}
if ( IsPlayer( entOrId ) )
{
entOrId = entOrId getEntityNumber();
}
request = "0";
if ( responseExpected )
{
request = "1";
}
request = request + ";" + eventType + ";" + eventSubtype + ";" + entOrId + ";" + data;
return request;
}
MonitorBus()
{
level endon( "game_ended" );
for( ;; )
{
wait ( 0.1 );
// check to see if IW4MAdmin is ready to receive more data
if ( getDvar( level.eventBus.inVar ) == "" )
{
level notify( "bus_ready" );
}
eventString = getDvar( level.eventBus.outVar );
if ( eventString == "" )
{
continue;
}
LogDebug( "-> " + eventString );
NotifyClientEvent( strtok( eventString, ";" ) );
SetDvar( level.eventBus.outVar, "" );
}
}
QueueEvent( request, eventType, notifyEntity )
{
level endon( "game_ended" );
start = GetTime();
maxWait = level.eventBus.timeout * 1000; // 30 seconds
timedOut = "";
while ( GetDvar( level.eventBus.inVar ) != "" && ( GetTime() - start ) < maxWait )
{
level [[level.overrideMethods["waittill_notify_or_timeout"]]]( "bus_ready", 1 );
if ( GetDvar( level.eventBus.inVar ) != "" )
{
LogDebug( "A request is already in progress..." );
timedOut = "set";
continue;
}
timedOut = "unset";
}
if ( timedOut == "set")
{
LogDebug( "Timed out waiting for response..." );
if ( IsDefined( notifyEntity ) )
{
notifyEntity NotifyClientEventTimeout( eventType );
}
SetDvar( level.eventBus.inVar, "" );
return;
}
LogDebug("<- " + request );
SetDvar( level.eventBus.inVar, request );
}
ParseDataString( data )
{
if ( !IsDefined( data ) )
{
LogDebug( "No data to parse" );
return [];
}
dataParts = strtok( data, "|" );
dict = [];
for ( i = 0; i < dataParts.size; i++ )
{
part = dataParts[i];
splitPart = strtok( part, "=" );
key = splitPart[0];
value = splitPart[1];
dict[key] = value;
dict[i] = key;
}
return dict;
}
NotifyClientEventTimeout( eventType )
{
// todo: make this actual eventing
if ( eventType == level.eventTypes.clientDataRequested )
{
self.pers["clientData"].state = level.eventBus.timeoutKey;
}
}
NotifyClientEvent( eventInfo )
{
origin = getPlayerFromClientNum( int( eventInfo[3] ) );
target = getPlayerFromClientNum( int( eventInfo[4] ) );
event = spawnstruct();
event.type = eventInfo[1];
event.subtype = eventInfo[2];
event.data = eventInfo[5];
event.origin = origin;
event.target = target;
if ( IsDefined( event.data ) )
{
LogDebug( "NotifyClientEvent->" + event.data );
}
if ( int( eventInfo[3] ) != -1 && !IsDefined( origin ) )
{
LogDebug( "origin is null but the slot id is " + int( eventInfo[3] ) );
}
if ( int( eventInfo[4] ) != -1 && !IsDefined( target ) )
{
LogDebug( "target is null but the slot id is " + int( eventInfo[4] ) );
}
if ( IsDefined( target ) )
{
client = event.target;
}
else if ( IsDefined( origin ) )
{
client = event.origin;
}
else
{
LogDebug( "Neither origin or target are set but we are a Client Event, aborting" );
return;
}
client.event = event;
level notify( level.eventTypes.localClientEvent, client );
}
GetPlayerFromClientNum( clientNum )
{
if ( clientNum < 0 )
{
return undefined;
}
for ( i = 0; i < level.players.size; i++ )
{
if ( level.players[i] getEntityNumber() == clientNum )
{
return level.players[i];
}
}
return undefined;
}
AddClientCommand( commandName, shouldRunAsTarget, callback, shouldOverwrite )
{
if ( IsDefined( level.clientCommandCallbacks[commandName] ) && IsDefined( shouldOverwrite ) && !shouldOverwrite )
{
return;
}
level.clientCommandCallbacks[commandName] = callback;
level.clientCommandRusAsTarget[commandName] = shouldRunAsTarget == true; //might speed up things later in case someone gives us a string or number instead of a boolean
}
//////////////////////////////////
// Event Handlers
/////////////////////////////////
OnClientDataReceived( event )
{
event.data = ParseDataString( event.data );
clientData = self.pers[level.clientDataKey];
if ( event.subtype == "Fail" )
{
LogDebug( "Received fail response" );
clientData.state = level.eventBus.failKey;
return;
}
if ( event.subtype == "Meta" )
{
if ( !IsDefined( clientData.meta ) )
{
clientData.meta = [];
}
metaKey = event.data[0];
clientData.meta[metaKey] = event.data[metaKey];
LogDebug( "Meta Key=" + metaKey + ", Meta Value=" + event.data[metaKey] );
return;
}
clientData.permissionLevel = event.data["level"];
clientData.clientId = event.data["clientId"];
clientData.lastConnection = event.data["lastConnection"];
clientData.tag = event.data["tag"];
clientData.state = "complete";
self.persistentClientId = event.data["clientId"];
self thread DisplayWelcomeData();
}
OnExecuteCommand( event )
{
data = ParseDataString( event.data );
response = "";
command = level.clientCommandCallbacks[event.subtype];
runAsTarget = level.clientCommandRusAsTarget[event.subtype];
executionContextEntity = event.origin;
if ( runAsTarget )
{
executionContextEntity = event.target;
}
if ( IsDefined( command ) )
{
response = executionContextEntity [[command]]( event, data );
}
else
{
LogDebug( "Unknown Client command->" + event.subtype );
}
// send back the response to the origin, but only if they're not the target
if ( IsDefined( response ) && response != "" && IsPlayer( event.origin ) && event.origin != event.target )
{
event.origin IPrintLnBold( response );
}
}
OnSetClientDataCompleted( event )
{
// IW4MAdmin let us know it persisted (success or fail)
LogDebug( "Set Client Data -> subtype = " + event.subType + " status = " + event.data["status"] );
}

View File

@ -0,0 +1,500 @@
#include common_scripts\iw4x_utility;
Init()
{
level.eventBus.gamename = "IW4";
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\_integration_base::RegisterLogger( ::Log2Console );
level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired;
level.overrideMethods["SetDvarIfUninitialized"] = ::_SetDvarIfUninitialized;
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
RegisterClientCommands();
level notify( level.notifyTypes.gameFunctionsInitialized );
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
level thread OnPlayerConnect();
}
OnPlayerConnect()
{
level endon ( "game_ended" );
for ( ;; )
{
level waittill( "connected", player );
if ( scripts\_integration_base::_IsBot( player ) )
{
// we don't want to track bots
continue;
}
player thread SetPersistentData();
player thread WaitForClientEvents();
}
}
RegisterClientCommands()
{
scripts\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
WaitForClientEvents()
{
self endon( "disconnect" );
// example of requesting a meta value
lastServerMetaKey = "LastServerPlayed";
// self scripts\_integration_base::RequestClientMeta( lastServerMetaKey );
for ( ;; )
{
self waittill( level.eventTypes.localClientEvent, event );
scripts\_integration_base::LogDebug( "Received client event " + event.type );
if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey )
{
clientData = self.pers[level.clientDataKey];
lastServerPlayed = clientData.meta[lastServerMetaKey];
}
}
}
GetTotalShotsFired()
{
return maps\mp\_utility::getPlayerStat( "mostshotsfired" );
}
_SetDvarIfUninitialized( dvar, value )
{
SetDvarIfUninitialized( dvar, value );
}
_waittill_notify_or_timeout( _notify, timeout )
{
common_scripts\utility::waittill_notify_or_timeout( _notify, timeout );
}
Log2Console( logLevel, message )
{
PrintConsole( "[" + logLevel + "] " + message + "\n" );
}
//////////////////////////////////
// GUID helpers
/////////////////////////////////
SetPersistentData()
{
self endon( "disconnect" );
guidHigh = self GetPlayerData( "bests", "none" );
guidLow = self GetPlayerData( "awards", "none" );
persistentGuid = guidHigh + "," + guidLow;
guidIsStored = guidHigh != 0 && guidLow != 0;
if ( guidIsStored )
{
// give IW4MAdmin time to collect IP
wait( 15 );
scripts\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid );
scripts\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid );
return;
}
guid = self SplitGuid();
scripts\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow );
self SetPlayerData( "bests", "none", guid["high"] );
self SetPlayerData( "awards", "none", guid["low"] );
}
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;
}
}
//////////////////////////////////
// Command Implementations
/////////////////////////////////
GiveWeaponImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
self SwitchToWeapon( data["weaponName"] );
return self.name + "^7 has been given ^5" + data["weaponName"];
}
TakeWeaponsImpl()
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
return "Took weapons from " + self.name;
}
TeamSwitchImpl()
{
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
if ( self.team == "allies" )
{
team = level.axis;
}
self IPrintLnBold( "You are being team switched" );
wait( 2 );
self [[team]]();
return self.name + "^7 switched to " + self.team;
}
LockControlsImpl()
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
self.isControlLocked = false;
}
if ( !self.isControlLocked )
{
self freezeControls( true );
self God();
self Hide();
info = [];
info[ "alertType" ] = "Alert!";
info[ "message" ] = "You have been frozen!";
self AlertImpl( undefined, info );
self.isControlLocked = true;
return self.name + "\'s controls are locked";
}
else
{
self freezeControls( false );
self God();
self Show();
self.isControlLocked = false;
return self.name + "\'s controls are unlocked";
}
}
NoClipImpl()
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isNoClipped ) )
{
self.isNoClipped = false;
}
if ( !self.isNoClipped )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = true;
self IPrintLnBold( "NoClip enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Show();
self.isNoClipped = false;
self IPrintLnBold( "NoClip disabled" );
}
}
HideImpl()
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
{
self.isHidden = false;
}
if ( !self.isHidden )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Hide();
self.isHidden = true;
self IPrintLnBold( "Hide enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Show();
self.isHidden = false;
self IPrintLnBold( "Hide disabled" );
}
}
AlertImpl( event, data )
{
if ( level.eventBus.gamename == "IW4" )
{
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], "compass_waypoint_target", ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
}
return "Sent alert to " + self.name;
}
GotoImpl( event, data )
{
if ( IsDefined( event.target ) )
{
return self GotoPlayerImpl( event.target );
}
else
{
return self GotoCoordImpl( data );
}
}
GotoCoordImpl( data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
self SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
}
GotoPlayerImpl( target )
{
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
return;
}
self SetOrigin( target GetOrigin() );
self IPrintLnBold( "Moved to " + target.name );
}
PlayerToMeImpl( event )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
}
KillImpl()
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
return "You killed " + self.name;
}
SetSpectatorImpl()
{
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}

View File

@ -0,0 +1,501 @@
#include common_scripts\utility;
Init()
{
level.eventBus.gamename = "IW5";
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\mp\_integration_base::RegisterLogger( ::Log2Console );
level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired;
level.overrideMethods["SetDvarIfUninitialized"] = ::_SetDvarIfUninitialized;
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
RegisterClientCommands();
level notify( level.notifyTypes.gameFunctionsInitialized );
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
level thread OnPlayerConnect();
}
OnPlayerConnect()
{
level endon ( "game_ended" );
for ( ;; )
{
level waittill( "connected", player );
if ( scripts\mp\_integration_base::_IsBot( player ) )
{
// we don't want to track bots
continue;
}
player thread SetPersistentData();
player thread WaitForClientEvents();
}
}
RegisterClientCommands()
{
scripts\mp\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\mp\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\mp\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\mp\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\mp\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\mp\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\mp\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\mp\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\mp\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\mp\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\mp\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
WaitForClientEvents()
{
self endon( "disconnect" );
// example of requesting a meta value
lastServerMetaKey = "LastServerPlayed";
// self scripts\mp\_integration_base::RequestClientMeta( lastServerMetaKey );
for ( ;; )
{
self waittill( level.eventTypes.localClientEvent, event );
scripts\mp\_integration_base::LogDebug( "Received client event " + event.type );
if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey )
{
clientData = self.pers[level.clientDataKey];
lastServerPlayed = clientData.meta[lastServerMetaKey];
}
}
}
GetTotalShotsFired()
{
return maps\mp\_utility::getPlayerStat( "mostshotsfired" );
}
_SetDvarIfUninitialized( dvar, value )
{
SetDvarIfUninitialized( dvar, value );
}
_waittill_notify_or_timeout( _notify, timeout )
{
common_scripts\utility::waittill_notify_or_timeout( _notify, timeout );
}
Log2Console( logLevel, message )
{
Print( "[" + logLevel + "] " + message + "\n" );
}
//////////////////////////////////
// GUID helpers
/////////////////////////////////
SetPersistentData()
{
self endon( "disconnect" );
guidHigh = self GetPlayerData( "bests", "none" );
guidLow = self GetPlayerData( "awards", "none" );
persistentGuid = guidHigh + "," + guidLow;
guidIsStored = guidHigh != 0 && guidLow != 0;
if ( guidIsStored )
{
// give IW4MAdmin time to collect IP
wait( 15 );
scripts\mp\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid );
scripts\mp\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid );
return;
}
guid = self SplitGuid();
scripts\mp\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow );
self SetPlayerData( "bests", "none", guid["high"] );
self SetPlayerData( "awards", "none", guid["low"] );
}
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;
}
}
//////////////////////////////////
// Command Implementations
/////////////////////////////////
GiveWeaponImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
self SwitchToWeapon( data["weaponName"] );
return self.name + "^7 has been given ^5" + data["weaponName"];
}
TakeWeaponsImpl()
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
return "Took weapons from " + self.name;
}
TeamSwitchImpl()
{
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
if ( self.team == "allies" )
{
team = level.axis;
}
self IPrintLnBold( "You are being team switched" );
wait( 2 );
self [[team]]();
return self.name + "^7 switched to " + self.team;
}
LockControlsImpl()
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
self.isControlLocked = false;
}
if ( !self.isControlLocked )
{
self freezeControls( true );
self God();
self Hide();
info = [];
info[ "alertType" ] = "Alert!";
info[ "message" ] = "You have been frozen!";
self AlertImpl( undefined, info );
self.isControlLocked = true;
return self.name + "\'s controls are locked";
}
else
{
self freezeControls( false );
self God();
self Show();
self.isControlLocked = false;
return self.name + "\'s controls are unlocked";
}
}
NoClipImpl()
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
}
if ( !IsDefined ( self.isNoClipped ) )
{
self.isNoClipped = false;
}
if ( !self.isNoClipped )
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self God();
self Noclip();
self Hide();
SetDvar( "sv_cheats", 0 );
self.isNoClipped = true;
self IPrintLnBold( "NoClip enabled" );
}
else
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self God();
self Noclip();
self Hide();
SetDvar( "sv_cheats", 0 );
self.isNoClipped = false;
self IPrintLnBold( "NoClip disabled" );
}
self IPrintLnBold( "NoClip enabled" );
}
HideImpl()
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
{
self.isHidden = false;
}
if ( !self.isHidden )
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self God();
self Hide();
SetDvar( "sv_cheats", 0 );
self.isHidden = true;
self IPrintLnBold( "Hide enabled" );
}
else
{
SetDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self God();
self Show();
SetDvar( "sv_cheats", 0 );
self.isHidden = false;
self IPrintLnBold( "Hide disabled" );
}
}
AlertImpl( event, data )
{
if ( level.eventBus.gamename == "IW5" ) {
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "ui_mp_nukebomb_timer", 7.5 );
}
return "Sent alert to " + self.name;
}
GotoImpl( event, data )
{
if ( IsDefined( event.target ) )
{
return self GotoPlayerImpl( event.target );
}
else
{
return self GotoCoordImpl( data );
}
}
GotoCoordImpl( data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
self SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
}
GotoPlayerImpl( target )
{
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
return;
}
self SetOrigin( target GetOrigin() );
self IPrintLnBold( "Moved to " + target.name );
}
PlayerToMeImpl( event )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
}
KillImpl()
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
return "You killed " + self.name;
}
SetSpectatorImpl()
{
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}

View File

@ -0,0 +1,522 @@
#include common_scripts\utility;
Init()
{
level.eventBus.gamename = "T5";
level thread Setup();
}
Setup()
{
level endon( "game_ended" );
// it's possible that the notify type has not been defined yet so we have to hard code it
level waittill( "IntegrationBootstrapInitialized" );
scripts\mp\_integration_base::RegisterLogger( ::Log2Console );
level.overrideMethods["GetTotalShotsFired"] = ::GetTotalShotsFired;
level.overrideMethods["SetDvarIfUninitialized"] = ::_SetDvarIfUninitialized;
level.overrideMethods["waittill_notify_or_timeout"] = ::_waittill_notify_or_timeout;
RegisterClientCommands();
level notify( level.notifyTypes.gameFunctionsInitialized );
if ( GetDvarInt( "sv_iw4madmin_integration_enabled" ) != 1 )
{
return;
}
level thread OnPlayerConnect();
}
OnPlayerConnect()
{
level endon ( "game_ended" );
for ( ;; )
{
level waittill( "connected", player );
if ( scripts\mp\_integration_base::_IsBot( player ) )
{
// we don't want to track bots
continue;
}
//player thread SetPersistentData();
player thread WaitForClientEvents();
}
}
RegisterClientCommands()
{
scripts\mp\_integration_base::AddClientCommand( "GiveWeapon", true, ::GiveWeaponImpl );
scripts\mp\_integration_base::AddClientCommand( "TakeWeapons", true, ::TakeWeaponsImpl );
scripts\mp\_integration_base::AddClientCommand( "SwitchTeams", true, ::TeamSwitchImpl );
scripts\mp\_integration_base::AddClientCommand( "Hide", false, ::HideImpl );
scripts\mp\_integration_base::AddClientCommand( "Alert", true, ::AlertImpl );
scripts\mp\_integration_base::AddClientCommand( "Goto", false, ::GotoImpl );
scripts\mp\_integration_base::AddClientCommand( "Kill", true, ::KillImpl );
scripts\mp\_integration_base::AddClientCommand( "SetSpectator", true, ::SetSpectatorImpl );
scripts\mp\_integration_base::AddClientCommand( "LockControls", true, ::LockControlsImpl );
scripts\mp\_integration_base::AddClientCommand( "PlayerToMe", true, ::PlayerToMeImpl );
scripts\mp\_integration_base::AddClientCommand( "NoClip", false, ::NoClipImpl );
}
WaitForClientEvents()
{
self endon( "disconnect" );
// example of requesting a meta value
lastServerMetaKey = "LastServerPlayed";
// self scripts\mp\_integration_base::RequestClientMeta( lastServerMetaKey );
for ( ;; )
{
self waittill( level.eventTypes.localClientEvent, event );
scripts\mp\_integration_base::LogDebug( "Received client event " + event.type );
if ( event.type == level.eventTypes.clientDataReceived && event.data[0] == lastServerMetaKey )
{
clientData = self.pers[level.clientDataKey];
lastServerPlayed = clientData.meta[lastServerMetaKey];
}
}
}
GetTotalShotsFired()
{
return maps\mp\gametypes\_persistence::statGet( "total_shots" );
}
_SetDvarIfUninitialized(dvar, value)
{
maps\mp\_utility::set_dvar_if_unset(dvar, value);
}
_waittill_notify_or_timeout( msg, timer )
{
self endon( msg );
wait( timer );
}
Log2Console( logLevel, message )
{
Print( "[" + logLevel + "] " + message + "\n" );
}
God()
{
if ( !IsDefined( self.godmode ) )
{
self.godmode = false;
}
if (!self.godmode )
{
self enableInvulnerability();
self.godmode = true;
}
else
{
self.godmode = false;
self disableInvulnerability();
}
}
//////////////////////////////////
// GUID helpers
/////////////////////////////////
/*SetPersistentData()
{
self endon( "disconnect" );
guidHigh = self GetPlayerData( "bests", "none" );
guidLow = self GetPlayerData( "awards", "none" );
persistentGuid = guidHigh + "," + guidLow;
guidIsStored = guidHigh != 0 && guidLow != 0;
if ( guidIsStored )
{
// give IW4MAdmin time to collect IP
wait( 15 );
scripts\mp\_integration_base::LogDebug( "Uploading persistent guid " + persistentGuid );
scripts\mp\_integration_base::SetClientMeta( "PersistentClientGuid", persistentGuid );
return;
}
guid = self SplitGuid();
scripts\mp\_integration_base::LogDebug( "Persisting client guid " + guidHigh + "," + guidLow );
self SetPlayerData( "bests", "none", guid["high"] );
self SetPlayerData( "awards", "none", guid["low"] );
}
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;
}
}*/
//////////////////////////////////
// Command Implementations
/////////////////////////////////
GiveWeaponImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self IPrintLnBold( "You have been given a new weapon" );
self GiveWeapon( data["weaponName"] );
self SwitchToWeapon( data["weaponName"] );
return self.name + "^7 has been given ^5" + data["weaponName"];
}
TakeWeaponsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
self TakeAllWeapons();
self IPrintLnBold( "All your weapons have been taken" );
return "Took weapons from " + self.name;
}
TeamSwitchImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self + "^7 is not alive";
}
team = level.allies;
if ( self.team == "allies" )
{
team = level.axis;
}
self IPrintLnBold( "You are being team switched" );
wait( 2 );
self [[team]]();
return self.name + "^7 switched to " + self.team;
}
LockControlsImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + "^7 is not alive";
}
if ( !IsDefined ( self.isControlLocked ) )
{
self.isControlLocked = false;
}
if ( !self.isControlLocked )
{
self freezeControls( true );
self God();
self Hide();
info = [];
info[ "alertType" ] = "Alert!";
info[ "message" ] = "You have been frozen!";
self AlertImpl( undefined, info );
self.isControlLocked = true;
return self.name + "\'s controls are locked";
}
else
{
self freezeControls( false );
self God();
self Show();
self.isControlLocked = false;
return self.name + "\'s controls are unlocked";
}
}
NoClipImpl( event, data )
{
/*if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
}
if ( !IsDefined ( self.isNoClipped ) )
{
self.isNoClipped = false;
}
if ( !self.isNoClipped )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = true;
self IPrintLnBold( "NoClip enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Noclip();
self Hide();
self.isNoClipped = false;
self IPrintLnBold( "NoClip disabled" );
}
self IPrintLnBold( "NoClip enabled" );*/
scripts\mp\_integration_base::LogWarning( "NoClip is not supported on T5!" );
}
HideImpl( event, data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
if ( !IsDefined ( self.isHidden ) )
{
self.isHidden = false;
}
if ( !self.isHidden )
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 1 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Hide();
self.isHidden = true;
self IPrintLnBold( "Hide enabled" );
}
else
{
self SetClientDvar( "sv_cheats", 1 );
self SetClientDvar( "cg_thirdperson", 0 );
self SetClientDvar( "sv_cheats", 0 );
self God();
self Show();
self.isHidden = false;
self IPrintLnBold( "Hide disabled" );
}
}
AlertImpl( event, data )
{
self thread maps\mp\gametypes\_hud_message::oldNotifyMessage( data["alertType"], data["message"], undefined, ( 1, 0, 0 ), "mpl_sab_ui_suitcasebomb_timer", 7.5 );
return "Sent alert to " + self.name;
}
GotoImpl( event, data )
{
if ( IsDefined( event.target ) )
{
return self GotoPlayerImpl( event.target );
}
else
{
return self GotoCoordImpl( data );
}
}
GotoCoordImpl( data )
{
if ( !IsAlive( self ) )
{
self IPrintLnBold( "You are not alive" );
return;
}
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
self SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
}
GotoPlayerImpl( target )
{
if ( !IsAlive( target ) )
{
self IPrintLnBold( target.name + " is not alive" );
return;
}
self SetOrigin( target GetOrigin() );
self IPrintLnBold( "Moved to " + target.name );
}
PlayerToMeImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self SetOrigin( event.origin GetOrigin() );
return "Moved here " + self.name;
}
KillImpl( event, data )
{
if ( !IsAlive( self ) )
{
return self.name + " is not alive";
}
self Suicide();
self IPrintLnBold( "You were killed by " + self.name );
return "You killed " + self.name;
}
SetSpectatorImpl( event, data )
{
if ( self.pers["team"] == "spectator" )
{
return self.name + " is already spectating";
}
self [[level.spectator]]();
self IPrintLnBold( "You have been moved to spectator" );
return self.name + " has been moved to spectator";
}

File diff suppressed because it is too large Load Diff

14
GameFiles/deploy.bat Normal file
View File

@ -0,0 +1,14 @@
@echo off
ECHO "Pluto IW5"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
xcopy /y .\GameInterface\_integration_iw5.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
xcopy /y .\AntiCheat\IW5\storage\iw5\scripts\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\iw5\scripts\mp"
ECHO "Pluto T5"
xcopy /y .\GameInterface\_integration_base.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\mp"
xcopy /y .\GameInterface\_integration_t5.gsc "%LOCALAPPDATA%\Plutonium\storage\t5\scripts\mp"
ECHO "Pluto T6"
xcopy /y .\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\mp"
xcopy /y .\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc.src "%LOCALAPPDATA%\Plutonium\storage\t6\scripts\mp"

View File

@ -6,14 +6,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{26E8
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C8F3945-0AEF-4949-A1F7-B18E952E50BC}"
ProjectSection(SolutionItems) = preProject
GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\IW4x\userraw\scripts\_customcallbacks.gsc
DeploymentFiles\deployment-pipeline.yml = DeploymentFiles\deployment-pipeline.yml
DeploymentFiles\PostPublish.ps1 = DeploymentFiles\PostPublish.ps1
README.md = README.md
version.txt = version.txt
DeploymentFiles\UpdateIW4MAdmin.ps1 = DeploymentFiles\UpdateIW4MAdmin.ps1
DeploymentFiles\UpdateIW4MAdmin.sh = DeploymentFiles\UpdateIW4MAdmin.sh
GameFiles\_integration.gsc = GameFiles\_integration.gsc
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedLibraryCore", "SharedLibraryCore\SharedLibraryCore.csproj", "{AA0541A2-8D51-4AD9-B0AC-3D1F5B162481}"
@ -54,6 +52,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\SubnetBan.js = Plugins\ScriptPlugins\SubnetBan.js
Plugins\ScriptPlugins\BanBroadcasting.js = Plugins\ScriptPlugins\BanBroadcasting.js
Plugins\ScriptPlugins\ParserH1MOD.js = Plugins\ScriptPlugins\ParserH1MOD.js
Plugins\ScriptPlugins\ParserPlutoniumT5.js = Plugins\ScriptPlugins\ParserPlutoniumT5.js
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"
@ -70,6 +69,36 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integrations.Cod", "Integra
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integrations.Source", "Integrations\Source\Integrations.Source.csproj", "{9512295B-3045-40E0-9B7E-2409F2173E9D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mute", "Plugins\Mute\Mute.csproj", "{259824F3-D860-4233-91D6-FF73D4DD8B18}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameFiles", "GameFiles", "{6CBF412C-EFEE-45F7-80FD-AC402C22CDB9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GameInterface", "GameInterface", "{5C2BE2A8-EA1D-424F-88E1-7FC33EEC2E55}"
ProjectSection(SolutionItems) = preProject
GameFiles\GameInterface\_integration_base.gsc = GameFiles\GameInterface\_integration_base.gsc
GameFiles\GameInterface\_integration_iw4x.gsc = GameFiles\GameInterface\_integration_iw4x.gsc
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AntiCheat", "AntiCheat", "{AB83BAC0-C539-424A-BF00-78487C10753C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IW4x", "IW4x", "{3EA564BD-3AC6-479B-96B6-CB059DCD0C77}"
ProjectSection(SolutionItems) = preProject
GameFiles\AntiCheat\IW4x\userraw\scripts\_customcallbacks.gsc = GameFiles\AntiCheat\IW4x\userraw\scripts\_customcallbacks.gsc
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pluto T6", "Pluto T6", "{866F453D-BC89-457F-8B55-485494759B31}"
ProjectSection(SolutionItems) = preProject
GameFiles\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc = GameFiles\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc
GameFiles\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc.src = GameFiles\AntiCheat\PT6\storage\t6\scripts\mp\_customcallbacks.gsc.src
GameFiles\AntiCheat\PT6\README.MD = GameFiles\AntiCheat\PT6\README.MD
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pluto IW5", "Pluto IW5", "{603725A4-BC0B-423B-955B-762C89E1C4C2}"
ProjectSection(SolutionItems) = preProject
GameFiles\AntiCheat\IW5\storage\iw5\scripts\_customcallbacks.gsc = GameFiles\AntiCheat\IW5\storage\iw5\scripts\_customcallbacks.gsc
GameFiles\AntiCheat\IW5\README.MD = GameFiles\AntiCheat\IW5\README.MD
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -374,6 +403,30 @@ Global
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x86.Build.0 = Release|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|Any CPU.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|x64.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|x64.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|x86.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Debug|x86.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|x64.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|x64.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|x86.ActiveCfg = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|x86.Build.0 = Debug|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|Any CPU.ActiveCfg = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|Any CPU.Build.0 = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x64.ActiveCfg = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x64.Build.0 = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x86.ActiveCfg = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Release|x86.Build.0 = Release|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{259824F3-D860-4233-91D6-FF73D4DD8B18}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -388,6 +441,13 @@ Global
{00A1FED2-2254-4AF7-A5DB-2357FA7C88CD} = {26E8B310-269E-46D4-A612-24601F16065F}
{A9348433-58C1-4B9C-8BB7-088B02529D9D} = {A2AE33B4-0830-426A-9E11-951DAB12BE5B}
{9512295B-3045-40E0-9B7E-2409F2173E9D} = {A2AE33B4-0830-426A-9E11-951DAB12BE5B}
{259824F3-D860-4233-91D6-FF73D4DD8B18} = {26E8B310-269E-46D4-A612-24601F16065F}
{6CBF412C-EFEE-45F7-80FD-AC402C22CDB9} = {8C8F3945-0AEF-4949-A1F7-B18E952E50BC}
{5C2BE2A8-EA1D-424F-88E1-7FC33EEC2E55} = {6CBF412C-EFEE-45F7-80FD-AC402C22CDB9}
{AB83BAC0-C539-424A-BF00-78487C10753C} = {6CBF412C-EFEE-45F7-80FD-AC402C22CDB9}
{3EA564BD-3AC6-479B-96B6-CB059DCD0C77} = {AB83BAC0-C539-424A-BF00-78487C10753C}
{866F453D-BC89-457F-8B55-485494759B31} = {AB83BAC0-C539-424A-BF00-78487C10753C}
{603725A4-BC0B-423B-955B-762C89E1C4C2} = {AB83BAC0-C539-424A-BF00-78487C10753C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

View File

@ -243,7 +243,11 @@ namespace Integrations.Cod
CancellationToken = token
};
connectionState.ConnectionAttempts++;
if (!token.IsCancellationRequested)
{
connectionState.ConnectionAttempts++;
}
connectionState.BytesReadPerSegment.Clear();
_log.LogDebug(
@ -253,8 +257,10 @@ namespace Integrations.Cod
try
{
connectionState.LastQuery = DateTime.Now;
var timeout = _parser.OverrideTimeoutForCommand(parameters);
waitForResponse = waitForResponse && timeout.HasValue;
response = await SendPayloadAsync(payload, waitForResponse,
_parser.OverrideTimeoutForCommand(parameters), token);
timeout ?? TimeSpan.Zero, token);
if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse)
{
@ -368,7 +374,9 @@ namespace Integrations.Cod
throw new RConException("Unexpected response header from server");
}
var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.StartsWith("^7") ? line[2..] : line).ToArray();
return splitResponse;
}
@ -454,6 +462,12 @@ namespace Integrations.Cod
connectionState.SendEventArgs.DisconnectReuseSocket = true;
}
if (connectionState.ReceiveEventArgs.UserToken is ConnectionUserToken { CancellationToken.IsCancellationRequested: true })
{
// after a graceful restart we need to reset the receive user token as the cancellation has been updated
connectionState.ReceiveEventArgs.UserToken = connectionState.SendEventArgs.UserToken;
}
connectionState.SendEventArgs.SetBuffer(payload);
// send the data to the server

View File

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

View File

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

View File

@ -4,6 +4,7 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System.Threading.Tasks;
using SharedLibraryCore.Helpers;
namespace IW4MAdmin.Plugins.Login.Commands
{
@ -18,7 +19,7 @@ namespace IW4MAdmin.Plugins.Login.Commands
RequiresTarget = false;
Arguments = new CommandArgument[]
{
new CommandArgument()
new()
{
Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
Required = true
@ -26,24 +27,28 @@ namespace IW4MAdmin.Plugins.Login.Commands
};
}
public override async Task ExecuteAsync(GameEvent E)
public override async Task ExecuteAsync(GameEvent gameEvent)
{
bool success = E.Owner.Manager.TokenAuthenticator.AuthorizeToken(E.Origin.NetworkId, E.Data);
var success = gameEvent.Owner.Manager.TokenAuthenticator.AuthorizeToken(new TokenIdentifier
{
ClientId = gameEvent.Origin.ClientId,
Token = gameEvent.Data
});
if (!success)
{
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, E.Origin.PasswordSalt));
success = hashedPassword[0] == E.Origin.Password;
var hashedPassword = await Task.FromResult(Hashing.Hash(gameEvent.Data, gameEvent.Origin.PasswordSalt));
success = hashedPassword[0] == gameEvent.Origin.Password;
}
if (success)
{
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
Plugin.AuthorizedClients[gameEvent.Origin.ClientId] = true;
}
_ = success ?
E.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
E.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
gameEvent.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
gameEvent.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
}
}
}

View File

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

View File

@ -26,49 +26,22 @@ namespace IW4MAdmin.Plugins.Login
_configHandler = configurationHandlerFactory.GetConfigurationHandler<Configuration>("LoginPluginSettings");
}
public Task OnEventAsync(GameEvent E, Server S)
public Task OnEventAsync(GameEvent gameEvent, Server server)
{
if (E.IsRemote || _configHandler.Configuration().RequirePrivilegedClientLogin == false)
if (gameEvent.IsRemote || _configHandler.Configuration().RequirePrivilegedClientLogin == false)
return Task.CompletedTask;
if (E.Type == GameEvent.EventType.Connect)
if (gameEvent.Type == GameEvent.EventType.Connect)
{
AuthorizedClients.TryAdd(E.Origin.ClientId, false);
E.Origin.SetAdditionalProperty("IsLoggedIn", false);
AuthorizedClients.TryAdd(gameEvent.Origin.ClientId, false);
gameEvent.Origin.SetAdditionalProperty("IsLoggedIn", false);
}
if (E.Type == GameEvent.EventType.Disconnect)
if (gameEvent.Type == GameEvent.EventType.Disconnect)
{
AuthorizedClients.TryRemove(E.Origin.ClientId, out bool value);
AuthorizedClients.TryRemove(gameEvent.Origin.ClientId, out _);
}
if (E.Type == GameEvent.EventType.Command)
{
if (E.Origin.Level < EFClient.Permission.Moderator ||
E.Origin.Level == EFClient.Permission.Console)
return Task.CompletedTask;
if (E.Extra.GetType() == typeof(SetPasswordCommand) &&
E.Origin?.Password == null)
return Task.CompletedTask;
if (E.Extra.GetType() == typeof(LoginCommand))
return Task.CompletedTask;
if (E.Extra.GetType() == typeof(RequestTokenCommand))
return Task.CompletedTask;
if (!AuthorizedClients[E.Origin.ClientId])
{
throw new AuthorizationException(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_AUTH"]);
}
else
{
E.Origin.SetAdditionalProperty("IsLoggedIn", true);
}
}
return Task.CompletedTask;
}
@ -76,6 +49,36 @@ namespace IW4MAdmin.Plugins.Login
{
AuthorizedClients = new ConcurrentDictionary<int, bool>();
manager.CommandInterceptors.Add(gameEvent =>
{
if (gameEvent.Type != GameEvent.EventType.Command || gameEvent.Extra is null)
{
return true;
}
if (gameEvent.Origin.Level < EFClient.Permission.Moderator ||
gameEvent.Origin.Level == EFClient.Permission.Console)
return true;
if (gameEvent.Extra.GetType() == typeof(SetPasswordCommand) &&
gameEvent.Origin?.Password == null)
return true;
if (gameEvent.Extra.GetType() == typeof(LoginCommand))
return true;
if (gameEvent.Extra.GetType() == typeof(RequestTokenCommand))
return true;
if (!AuthorizedClients[gameEvent.Origin.ClientId])
{
return false;
}
gameEvent.Origin.SetAdditionalProperty("IsLoggedIn", true);
return true;
});
await _configHandler.BuildAsync();
if (_configHandler.Configuration() == null)
{

View File

@ -0,0 +1,49 @@
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace Mute.Commands;
public class MuteCommand : Command
{
public MuteCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "mute";
Description = translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_DESC"];
Alias = "mu";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
SupportedGames = Plugin.SupportedGames;
Arguments = new[]
{
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_REASON"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
if (await Plugin.MuteManager.Mute(gameEvent.Owner, gameEvent.Origin, gameEvent.Target, null, gameEvent.Data))
{
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_MUTED"]
.FormatExt(gameEvent.Target.CleanedName));
gameEvent.Target.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_TARGET_MUTED"]
.FormatExt(gameEvent.Data));
return;
}
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_NOT_UNMUTED"]
.FormatExt(gameEvent.Target.CleanedName));
}
}

View File

@ -0,0 +1,67 @@
using System.Text.RegularExpressions;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace Mute.Commands;
public class TempMuteCommand : Command
{
private const string TempBanRegex = @"([0-9]+\w+)\ (.+)";
public TempMuteCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "tempmute";
Description = translationLookup["PLUGINS_MUTE_COMMANDS_TEMPMUTE_DESC"];
Alias = "tm";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
SupportedGames = Plugin.SupportedGames;
Arguments = new[]
{
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_DURATION"],
Required = true
},
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_REASON"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var match = Regex.Match(gameEvent.Data, TempBanRegex);
if (match.Success)
{
var expiration = DateTime.UtcNow + match.Groups[1].ToString().ParseTimespan();
var reason = match.Groups[2].ToString();
if (await Plugin.MuteManager.Mute(gameEvent.Owner, gameEvent.Origin, gameEvent.Target, expiration, reason))
{
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_TEMPMUTE_TEMPMUTED"]
.FormatExt(gameEvent.Target.CleanedName));
gameEvent.Target.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_TEMPMUTE_TARGET_TEMPMUTED"]
.FormatExt(expiration.HumanizeForCurrentCulture(), reason));
return;
}
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_NOT_UNMUTED"]
.FormatExt(gameEvent.Target.CleanedName));
return;
}
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_TEMPMUTE_BAD_FORMAT"]);
}
}

View File

@ -0,0 +1,49 @@
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace Mute.Commands;
public class UnmuteCommand : Command
{
public UnmuteCommand(CommandConfiguration config, ITranslationLookup translationLookup) : base(config,
translationLookup)
{
Name = "unmute";
Description = translationLookup["PLUGINS_MUTE_COMMANDS_UNMUTE_DESC"];
Alias = "um";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
SupportedGames = Plugin.SupportedGames;
Arguments = new[]
{
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = translationLookup["COMMANDS_ARGS_REASON"],
Required = true
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
if (await Plugin.MuteManager.Unmute(gameEvent.Owner, gameEvent.Origin, gameEvent.Target, gameEvent.Data))
{
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_UNMUTE_UNMUTED"]
.FormatExt(gameEvent.Target.CleanedName));
gameEvent.Target.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_UNMUTE_TARGET_UNMUTED"]
.FormatExt(gameEvent.Data));
return;
}
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_UNMUTE_NOT_MUTED"]
.FormatExt(gameEvent.Target.CleanedName));
}
}

20
Plugins/Mute/Mute.csproj Normal file
View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>MrAmos123</Authors>
<OutputType>Library</OutputType>
<Configurations>Debug;Release;Prerelease</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.10.13.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project>

170
Plugins/Mute/MuteManager.cs Normal file
View File

@ -0,0 +1,170 @@
using Data.Models;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using static System.Enum;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Mute;
public class MuteManager
{
private readonly IMetaServiceV2 _metaService;
private readonly ITranslationLookup _translationLookup;
private readonly ILogger _logger;
public MuteManager(IMetaServiceV2 metaService, ITranslationLookup translationLookup, ILogger logger)
{
_metaService = metaService;
_translationLookup = translationLookup;
_logger = logger;
}
public static bool IsExpiredMute(MuteStateMeta muteStateMeta) =>
muteStateMeta.Expiration is not null && muteStateMeta.Expiration < DateTime.UtcNow;
public async Task<MuteStateMeta> GetCurrentMuteState(EFClient client)
{
var clientMuteMeta = await ReadPersistentDataV2(client);
if (clientMuteMeta is not null) return clientMuteMeta;
// Return null if the client doesn't have old or new meta.
var muteState = await ReadPersistentDataV1(client);
clientMuteMeta = new MuteStateMeta
{
Reason = muteState is null ? string.Empty : _translationLookup["PLUGINS_MUTE_MIGRATED"],
Expiration = muteState switch
{
null => DateTime.UtcNow,
MuteState.Muted => null,
_ => DateTime.UtcNow
},
MuteState = muteState ?? MuteState.Unmuted,
CommandExecuted = true
};
// Migrate old mute meta, else, client has no state, so set a generic one, but don't write it to database.
if (muteState is not null)
{
clientMuteMeta.CommandExecuted = false;
await WritePersistentData(client, clientMuteMeta);
await CreatePenalty(muteState.Value, Utilities.IW4MAdminClient(), client, clientMuteMeta.Expiration,
clientMuteMeta.Reason);
}
else
{
client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta);
}
return clientMuteMeta;
}
public async Task<bool> Mute(Server server, EFClient origin, EFClient target, DateTime? dateTime, string reason)
{
var clientMuteMeta = await GetCurrentMuteState(target);
if (clientMuteMeta.MuteState is MuteState.Muted && clientMuteMeta.CommandExecuted) return false;
await CreatePenalty(MuteState.Muted, origin, target, dateTime, reason);
clientMuteMeta = new MuteStateMeta
{
Expiration = dateTime,
MuteState = MuteState.Muted,
Reason = reason,
CommandExecuted = false
};
await WritePersistentData(target, clientMuteMeta);
// Handle game command
var client = server.GetClientsAsList().FirstOrDefault(client => client.NetworkId == target.NetworkId);
await PerformGameCommand(server, client, clientMuteMeta);
return true;
}
public async Task<bool> Unmute(Server server, EFClient origin, EFClient target, string reason)
{
var clientMuteMeta = await GetCurrentMuteState(target);
if (clientMuteMeta.MuteState is MuteState.Unmuted && clientMuteMeta.CommandExecuted) return false;
if (!target.IsIngame && clientMuteMeta.MuteState is MuteState.Unmuting) return false;
await CreatePenalty(MuteState.Unmuted, origin, target, DateTime.UtcNow, reason);
clientMuteMeta = new MuteStateMeta
{
Expiration = DateTime.UtcNow,
MuteState = target.IsIngame ? MuteState.Unmuted : MuteState.Unmuting,
Reason = reason,
CommandExecuted = false
};
await WritePersistentData(target, clientMuteMeta);
// Handle game command
var client = server.GetClientsAsList().FirstOrDefault(client => client.NetworkId == target.NetworkId);
await PerformGameCommand(server, client, clientMuteMeta);
return true;
}
private async Task CreatePenalty(MuteState muteState, EFClient origin, EFClient target, DateTime? dateTime,
string reason)
{
var newPenalty = new EFPenalty
{
Type = muteState is MuteState.Unmuted ? EFPenalty.PenaltyType.Unmute :
dateTime is null ? EFPenalty.PenaltyType.Mute : EFPenalty.PenaltyType.TempMute,
Expires = muteState is MuteState.Unmuted ? DateTime.UtcNow : dateTime,
Offender = target,
Offense = reason,
Punisher = origin,
Active = true,
Link = target.AliasLink
};
_logger.LogDebug("Creating new {MuteState} Penalty for {Target} with reason {Reason}",
nameof(muteState), target.Name, reason);
await newPenalty.TryCreatePenalty(Plugin.Manager.GetPenaltyService(), _logger);
}
public static async Task PerformGameCommand(Server server, EFClient? client, MuteStateMeta muteStateMeta)
{
if (client is null || !client.IsIngame) return;
switch (muteStateMeta.MuteState)
{
case MuteState.Muted:
await server.ExecuteCommandAsync($"muteClient {client.ClientNumber}");
muteStateMeta.CommandExecuted = true;
break;
case MuteState.Unmuted:
await server.ExecuteCommandAsync($"unmute {client.ClientNumber}");
muteStateMeta.CommandExecuted = true;
break;
}
}
private async Task<MuteState?> ReadPersistentDataV1(EFClient client) => TryParse<MuteState>(
(await _metaService.GetPersistentMeta(Plugin.MuteKey, client.ClientId))?.Value,
out var muteState)
? muteState
: null;
private async Task<MuteStateMeta?> ReadPersistentDataV2(EFClient client)
{
// Get meta from client
var clientMuteMeta = client.GetAdditionalProperty<MuteStateMeta>(Plugin.MuteKey);
if (clientMuteMeta is not null) return clientMuteMeta;
// Get meta from database and store in client if exists
clientMuteMeta = await _metaService.GetPersistentMetaValue<MuteStateMeta>(Plugin.MuteKey, client.ClientId);
if (clientMuteMeta is not null) client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta);
return clientMuteMeta;
}
private async Task WritePersistentData(EFClient client, MuteStateMeta clientMuteMeta)
{
client.SetAdditionalProperty(Plugin.MuteKey, clientMuteMeta);
await _metaService.SetPersistentMetaValue(Plugin.MuteKey, clientMuteMeta, client.ClientId);
}
}

View File

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Mute;
public class MuteStateMeta
{
public string Reason { get; set; } = string.Empty;
public DateTime? Expiration { get; set; }
public MuteState MuteState { get; set; }
[JsonIgnore] public bool CommandExecuted { get; set; }
}
public enum MuteState
{
Muted,
Unmuting,
Unmuted
}

304
Plugins/Mute/Plugin.cs Normal file
View File

@ -0,0 +1,304 @@
using Microsoft.Extensions.Logging;
using Mute.Commands;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Mute;
public class Plugin : IPlugin
{
public string Name => "Mute";
public float Version => (float) Utilities.GetVersionAsDouble();
public string Author => "Amos";
public const string MuteKey = "IW4MMute";
public static MuteManager MuteManager { get; private set; } = null!;
public static IManager Manager { get; private set; } = null!;
public static readonly Server.Game[] SupportedGames = {Server.Game.IW4};
private static readonly string[] DisabledCommands = {nameof(PrivateMessageAdminsCommand), "PrivateMessageCommand"};
private readonly IInteractionRegistration _interactionRegistration;
private readonly IRemoteCommandService _remoteCommandService;
private static readonly string MuteInteraction = nameof(MuteInteraction);
public Plugin(ILogger<Plugin> logger, IMetaServiceV2 metaService, IInteractionRegistration interactionRegistration,
ITranslationLookup translationLookup, IRemoteCommandService remoteCommandService)
{
_interactionRegistration = interactionRegistration;
_remoteCommandService = remoteCommandService;
MuteManager = new MuteManager(metaService, translationLookup, logger);
}
public async Task OnEventAsync(GameEvent gameEvent, Server server)
{
if (!SupportedGames.Contains(server.GameName)) return;
switch (gameEvent.Type)
{
case GameEvent.EventType.Command:
break;
case GameEvent.EventType.Join:
// Check if user has any meta set, else ignore (unmuted)
var muteMetaJoin = await MuteManager.GetCurrentMuteState(gameEvent.Origin);
switch (muteMetaJoin.MuteState)
{
case MuteState.Muted:
// Let the client know when their mute expires.
gameEvent.Origin.Tell(Utilities.CurrentLocalization
.LocalizationIndex["PLUGINS_MUTE_REMAINING_TIME"].FormatExt(
muteMetaJoin.Expiration is not null
? muteMetaJoin.Expiration.Value.HumanizeForCurrentCulture()
: Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_NEVER"],
muteMetaJoin.Reason));
break;
case MuteState.Unmuting:
// Handle unmute of unmuted players.
await MuteManager.Unmute(server, Utilities.IW4MAdminClient(), gameEvent.Origin,
muteMetaJoin.Reason);
gameEvent.Origin.Tell(Utilities.CurrentLocalization
.LocalizationIndex["PLUGINS_MUTE_COMMANDS_UNMUTE_TARGET_UNMUTED"]
.FormatExt(muteMetaJoin.Reason));
break;
}
break;
case GameEvent.EventType.Say:
var muteMetaSay = await MuteManager.GetCurrentMuteState(gameEvent.Origin);
switch (muteMetaSay.MuteState)
{
case MuteState.Muted:
// Let the client know when their mute expires.
gameEvent.Origin.Tell(Utilities.CurrentLocalization
.LocalizationIndex["PLUGINS_MUTE_REMAINING_TIME"].FormatExt(
muteMetaSay.Expiration is not null
? muteMetaSay.Expiration.Value.HumanizeForCurrentCulture()
: Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_NEVER"],
muteMetaSay.Reason));
break;
}
break;
case GameEvent.EventType.Update:
// Get correct EFClient object
var client = server.GetClientsAsList()
.FirstOrDefault(client => client.NetworkId == gameEvent.Origin.NetworkId);
if (client == null) break;
var muteMetaUpdate = await MuteManager.GetCurrentMuteState(client);
if (!muteMetaUpdate.CommandExecuted)
{
await MuteManager.PerformGameCommand(server, client, muteMetaUpdate);
}
switch (muteMetaUpdate.MuteState)
{
case MuteState.Muted:
// Handle unmute if expired.
if (MuteManager.IsExpiredMute(muteMetaUpdate))
{
await MuteManager.Unmute(server, Utilities.IW4MAdminClient(), client,
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_EXPIRED"]);
client.Tell(
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_TARGET_EXPIRED"]);
}
break;
}
break;
}
}
public Task OnLoadAsync(IManager manager)
{
Manager = manager;
manager.CommandInterceptors.Add(gameEvent =>
{
if (gameEvent.Extra is not Command command)
return true;
return !DisabledCommands.Contains(command.GetType().Name) && !command.IsBroadcast;
});
_interactionRegistration.RegisterInteraction(MuteInteraction, async (targetClientId, game, token) =>
{
if (!targetClientId.HasValue || game.HasValue && !SupportedGames.Contains((Server.Game) game.Value))
{
return null;
}
var clientMuteMetaState =
(await MuteManager.GetCurrentMuteState(new EFClient {ClientId = targetClientId.Value}))
.MuteState;
var server = manager.GetServers().First();
string GetCommandName(Type commandType) =>
manager.Commands.FirstOrDefault(command => command.GetType() == commandType)?.Name ?? "";
return clientMuteMetaState is MuteState.Unmuted or MuteState.Unmuting
? CreateMuteInteraction(targetClientId.Value, server, GetCommandName)
: CreateUnmuteInteraction(targetClientId.Value, server, GetCommandName);
});
return Task.CompletedTask;
}
private InteractionData CreateMuteInteraction(int targetClientId, Server server,
Func<Type, string> getCommandNameFunc)
{
var reasonInput = new
{
Name = "Reason",
Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_REASON"],
Type = "text",
Values = (Dictionary<string, string>?) null
};
var durationInput = new
{
Name = "Duration",
Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_DURATION"],
Type = "select",
Values = (Dictionary<string, string>?) new Dictionary<string, string>
{
{"5m", TimeSpan.FromMinutes(5).HumanizeForCurrentCulture()},
{"30m", TimeSpan.FromMinutes(30).HumanizeForCurrentCulture()},
{"1h", TimeSpan.FromHours(1).HumanizeForCurrentCulture()},
{"6h", TimeSpan.FromHours(6).HumanizeForCurrentCulture()},
{"1d", TimeSpan.FromDays(1).HumanizeForCurrentCulture()},
{"p", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_SELECTION_PERMANENT"]}
}
};
var inputs = new[] {reasonInput, durationInput};
var inputsJson = JsonSerializer.Serialize(inputs);
return new InteractionData
{
EntityId = targetClientId,
Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"],
DisplayMeta = "oi-volume-off",
ActionPath = "DynamicAction",
ActionMeta = new()
{
{"InteractionId", MuteInteraction},
{"Inputs", inputsJson},
{
"ActionButtonLabel",
Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"]
},
{
"Name",
Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"]
},
{"ShouldRefresh", true.ToString()}
},
MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator,
Source = Name,
Action = async (originId, targetId, gameName, meta, cancellationToken) =>
{
if (!targetId.HasValue)
{
return "No target client id specified";
}
var isTempMute = meta.ContainsKey(durationInput.Name) &&
meta[durationInput.Name] != durationInput.Values?.Last().Key;
var muteCommand = getCommandNameFunc(isTempMute ? typeof(TempMuteCommand) : typeof(MuteCommand));
var args = new List<string>();
if (meta.TryGetValue(durationInput.Name, out var duration) &&
duration != durationInput.Values?.Last().Key)
{
args.Add(duration);
}
if (meta.TryGetValue(reasonInput.Name, out var reason))
{
args.Add(reason);
}
var commandResponse =
await _remoteCommandService.Execute(originId, targetId, muteCommand, args, server);
return string.Join(".", commandResponse.Select(result => result.Response));
}
};
}
private InteractionData CreateUnmuteInteraction(int targetClientId, Server server,
Func<Type, string> getCommandNameFunc)
{
var reasonInput = new
{
Name = "Reason",
Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_REASON"],
Type = "text",
};
var inputs = new[] {reasonInput};
var inputsJson = JsonSerializer.Serialize(inputs);
return new InteractionData
{
EntityId = targetClientId,
Name = Utilities.CurrentLocalization.LocalizationIndex[
"WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"],
DisplayMeta = "oi-volume-high",
ActionPath = "DynamicAction",
ActionMeta = new()
{
{"InteractionId", MuteInteraction},
{"Outputs", reasonInput.Name},
{"Inputs", inputsJson},
{
"ActionButtonLabel",
Utilities.CurrentLocalization.LocalizationIndex[
"WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"]
},
{
"Name",
Utilities.CurrentLocalization.LocalizationIndex[
"WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"]
},
{"ShouldRefresh", true.ToString()}
},
MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator,
Source = Name,
Action = async (originId, targetId, gameName, meta, cancellationToken) =>
{
if (!targetId.HasValue)
{
return "No target client id specified";
}
var args = new List<string>();
if (meta.TryGetValue(reasonInput.Name, out var reason))
{
args.Add(reason);
}
var commandResponse =
await _remoteCommandService.Execute(originId, targetId, getCommandNameFunc(typeof(UnmuteCommand)),
args, server);
return string.Join(".", commandResponse.Select(result => result.Response));
}
};
}
public Task OnUnloadAsync()
{
_interactionRegistration.UnregisterInteraction(MuteInteraction);
return Task.CompletedTask;
}
public Task OnTickAsync(Server server)
{
return Task.CompletedTask;
}
}

View File

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

View File

@ -46,7 +46,7 @@ let plugin = {
break;
case 'warn':
const warningTitle = _localization.LocalizationIndex['GLOBAL_WARNING'];
sendScriptCommand(server, 'Alert', gameEvent.Target, {
sendScriptCommand(server, 'Alert', gameEvent.Origin, gameEvent.Target, {
alertType: warningTitle + '!',
message: gameEvent.Data
});
@ -85,7 +85,7 @@ let commands = [{
name: 'weapon name',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -103,7 +103,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -121,7 +121,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -139,7 +139,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -147,24 +147,6 @@ let commands = [{
sendScriptCommand(gameEvent.Owner, 'LockControls', gameEvent.Origin, gameEvent.Target, undefined);
}
},
{
name: 'unlockcontrols',
description: 'unlocks target player\'s controls',
alias: 'ulc',
permission: 'Administrator',
targetRequired: true,
arguments: [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
}
sendScriptCommand(gameEvent.Owner, 'UnlockControls', gameEvent.Origin, gameEvent.Target, undefined);
}
},
{
name: 'noclip',
description: 'enable noclip on yourself ingame',
@ -180,21 +162,6 @@ let commands = [{
sendScriptCommand(gameEvent.Owner, 'NoClip', gameEvent.Origin, gameEvent.Origin, undefined);
}
},
{
name: 'noclipoff',
description: 'disable noclip on yourself ingame',
alias: 'nco',
permission: 'SeniorAdmin',
targetRequired: false,
arguments: [],
supportedGames: ['IW4', 'IW5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
}
sendScriptCommand(gameEvent.Owner, 'NoClipOff', gameEvent.Origin, gameEvent.Origin, undefined);
}
},
{
name: 'hide',
description: 'hide yourself ingame',
@ -202,7 +169,7 @@ let commands = [{
permission: 'SeniorAdmin',
targetRequired: false,
arguments: [],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -210,21 +177,6 @@ let commands = [{
sendScriptCommand(gameEvent.Owner, 'Hide', gameEvent.Origin, gameEvent.Origin, undefined);
}
},
{
name: 'unhide',
description: 'unhide yourself ingame',
alias: 'unh',
permission: 'SeniorAdmin',
targetRequired: false,
arguments: [],
supportedGames: ['IW4', 'IW5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
}
sendScriptCommand(gameEvent.Owner, 'Unhide', gameEvent.Origin, gameEvent.Origin, undefined);
}
},
{
name: 'alert',
description: 'alert a player',
@ -239,7 +191,7 @@ let commands = [{
name: 'message',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -260,7 +212,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -278,7 +230,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -304,7 +256,7 @@ let commands = [{
name: 'z',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -328,7 +280,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -336,21 +288,6 @@ let commands = [{
sendScriptCommand(gameEvent.Owner, 'Kill', gameEvent.Origin, gameEvent.Target, undefined);
}
},
{
name: 'nightmode',
description: 'sets server into nightmode',
alias: 'nitem',
permission: 'SeniorAdmin',
targetRequired: false,
arguments: [],
supportedGames: ['IW4', 'IW5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
}
sendScriptCommand(gameEvent.Owner, 'NightMode', gameEvent.Origin, undefined, undefined);
}
},
{
name: 'setspectator',
description: 'sets a player as spectator',
@ -361,7 +298,7 @@ let commands = [{
name: 'player',
required: true
}],
supportedGames: ['IW4', 'IW5'],
supportedGames: ['IW4', 'IW5', 'T5'],
execute: (gameEvent) => {
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
return;
@ -463,7 +400,11 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
if (input.length > 0) {
const event = parseEvent(input)
logger.WriteDebug(`Processing input... ${event.eventType} ${event.subType} ${event.data} ${event.clientNumber}`);
logger.WriteDebug(`Processing input... ${event.eventType} ${event.subType} ${event.data.toString()} ${event.clientNumber}`);
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
const threading = importNamespace('System.Threading');
const token = new threading.CancellationTokenSource().Token;
// todo: refactor to mapping if possible
if (event.eventType === 'ClientDataRequested') {
@ -474,15 +415,19 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
let data = [];
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
if (event.subType === 'Meta') {
const metaService = _serviceResolver.ResolveService('IMetaService');
const meta = metaService.GetPersistentMeta(event.data, client).GetAwaiter().GetResult();
const meta = metaService.GetPersistentMeta(event.data, client.ClientId, token).GetAwaiter().GetResult();
data[event.data] = meta === null ? '' : meta.Value;
logger.WriteDebug(`event data is ${event.data}`);
} else {
const tagMeta = metaService.GetPersistentMetaByLookup('ClientTagV2', 'ClientTagNameV2', client.ClientId, token).GetAwaiter().GetResult();
data = {
level: client.Level,
clientId: client.ClientId,
lastConnection: client.LastConnection
lastConnection: client.LastConnection,
tag: tagMeta?.Value ?? ''
};
}
@ -510,19 +455,19 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Fail'});
} else {
if (event.subType === 'Meta') {
const metaService = _serviceResolver.ResolveService('IMetaService');
try {
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}`);
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}, Direction=${event.data['direction']} ${token}`);
if (event.data['direction'] != null) {
event.data['direction'] = 'up'
? metaService.IncrementPersistentMeta(event.data['key'], event.data['value'], clientId).GetAwaiter().GetResult()
: metaService.DecrementPersistentMeta(event.data['key'], event.data['value'], clientId).GetAwaiter().GetResult();
? metaService.IncrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult()
: metaService.DecrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult();
} else {
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId).GetAwaiter().GetResult();
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult();
}
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Complete'});
} catch (error) {
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Fail'});
logger.WriteError('Could not persist client meta ' + error.toString());
}
}
}
@ -623,7 +568,7 @@ const parseDataString = data => {
dict[keyValue[0]] = keyValue[1];
}
return dict.length === 0 ? data : dict;
return Object.keys(dict).length === 0 ? data : dict;
}
const validateEnabled = (server, origin) => {

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'FrenchFry, RaidMax',
version: 0.8,
version: 0.9,
name: 'CoD4x Parser',
isParser: true,
@ -15,7 +15,7 @@ var plugin = {
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +playerid +steamid +name +lastmsg +address +qport +rate *';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]{16,32})|0) +([[0-9]+|0]) +(.{0,32}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|bot) +(-*[0-9]+) +([0-9]+) *$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]{16,32})|0) +([[0-9]+|0]) +(.{0,34}) +([0-9]+) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|bot) +(-*[0-9]+) +([0-9]+) *$';
rconParser.Configuration.Status.AddMapping(104, 6); // RConName
rconParser.Configuration.Status.AddMapping(105, 8); // RConIPAddress
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
@ -41,4 +41,4 @@ var plugin = {
onTickAsync: function (server) {
}
};
};

View File

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

View File

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

View File

@ -0,0 +1,44 @@
var rconParser;
var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.2,
name: 'Plutonium T5 Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t5';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.Dvar.Pattern = '^(?:\\^7)?\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n)?\\w*(.+)*$';
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.DefaultRConPort = 3074;
rconParser.Configuration.CanGenerateLogPath = false;
rconParser.Configuration.OverrideCommandTimeouts.Clear();
rconParser.Configuration.OverrideCommandTimeouts.Add('map', 0);
rconParser.Configuration.OverrideCommandTimeouts.Add('map_rotate', 0);
rconParser.Configuration.OverrideCommandTimeouts.Add('fast_restart', 0);
rconParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
rconParser.GameName = 6; // T5
eventParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
eventParser.GameName = 6; // T5
eventParser.Configuration.GuidNumberStyle = 7; // Integer
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

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

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.2,
version: 0.3,
name: 'Call of Duty 5: World at War Parser',
isParser: true,
@ -17,6 +17,7 @@ var plugin = {
rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.DefaultRConPort = 28960;
rconParser.Version = 'Call of Duty Multiplayer COD_WaW MP build 1.7.1263 CL(350073) JADAMS2 Thu Oct 29 15:43:55 2009 win-x86';
rconParser.GameName = 5; // T4
eventParser.Configuration.GuidNumberStyle = 7; // Integer
eventParser.GameName = 5; // T4

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.4,
version: 0.5,
name: 'Black Ops 3 Parser',
isParser: true,
@ -15,7 +15,7 @@ var plugin = {
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +((?:[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)(?:\\([0-9]+\\)) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +xuid +name +address +qport';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +ping +xuid +name +address +qport|---------- Live ----------';
rconParser.Configuration.CommandPrefixes.Kick = 'clientkick {0}';
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0}';
rconParser.Configuration.CommandPrefixes.TempBan = 'tempbanclient {0}';
@ -30,6 +30,7 @@ var plugin = {
rconParser.Configuration.DefaultRConPort = 27016;
rconParser.Configuration.OverrideDvarNameMapping.Add('sv_hostname', 'live_steam_server_name');
rconParser.Configuration.OverrideDvarNameMapping.Add('g_password', 'live_steam_server_password');
rconParser.Configuration.DefaultDvarValues.Add('sv_running', '1');
rconParser.Configuration.DefaultDvarValues.Add('g_gametype', '');
rconParser.Configuration.DefaultDvarValues.Add('fs_basepath', '');

View File

@ -1,12 +1,12 @@
let vpnExceptionIds = [];
const commands = [{
name: "whitelistvpn",
description: "whitelists a player's client id from VPN detection",
alias: "wv",
permission: "SeniorAdmin",
name: 'whitelistvpn',
description: 'whitelists a player\'s client id from VPN detection',
alias: 'wv',
permission: 'SeniorAdmin',
targetRequired: true,
arguments: [{
name: "player",
name: 'player',
required: true
}],
execute: (gameEvent) => {
@ -19,12 +19,11 @@ const commands = [{
const plugin = {
author: 'RaidMax',
version: 1.3,
version: 1.5,
name: 'VPN Detection Plugin',
manager: null,
logger: null,
checkForVpn: function (origin) {
let exempt = false;
// prevent players that are exempt from being kicked
@ -80,11 +79,37 @@ const plugin = {
this.logger = manager.GetLogger(0);
this.configHandler = _configHandler;
this.configHandler.GetValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(element));
this.configHandler.GetValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(parseInt(element)));
this.logger.WriteInfo(`Loaded ${vpnExceptionIds.length} ids into whitelist`);
this.interactionRegistration = _serviceResolver.ResolveService('IInteractionRegistration');
this.interactionRegistration.RegisterScriptInteraction('WhitelistVPN', this.name, (targetId, game, token) => {
if (vpnExceptionIds.includes(targetId)) {
return;
}
const helpers = importNamespace('SharedLibraryCore.Helpers');
const interactionData = new helpers.InteractionData();
interactionData.EntityId = targetId;
interactionData.Name = 'Whitelist VPN';
interactionData.DisplayMeta = 'oi-circle-check';
interactionData.ActionMeta.Add('InteractionId', 'command');
interactionData.ActionMeta.Add('Data', `whitelistvpn`);
interactionData.ActionMeta.Add('ActionButtonLabel', 'Allow');
interactionData.ActionMeta.Add('Name', 'Allow VPN Connection');
interactionData.ActionMeta.Add('ShouldRefresh', true.toString());
interactionData.ActionPath = 'DynamicAction';
interactionData.MinimumPermission = 3;
interactionData.Source = this.name;
return interactionData;
});
},
onUnloadAsync: function () {
this.interactionRegistration.UnregisterInteraction('WhitelistVPN');
},
onTickAsync: function (server) {

View File

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

View File

@ -79,7 +79,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
}
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 double? Performance { get; set; }
public int? Ranking { get; set; }
public int TotalRankedClients { get; set; }
public double? ZScore { get; set; }
public double? Rating { get; set; }
public List<ServerInfo> Servers { get; set; }
@ -25,4 +26,4 @@ namespace Stats.Dtos
public List<EFClientRankingHistory> Ratings { get; set; }
public List<EFClientStatistics> LegacyStats { get; set; }
}
}
}

View File

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

View File

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

View File

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

View File

@ -45,58 +45,53 @@ namespace Stats.Helpers
var result = new ResourceQueryHelperResult<MessageResponse>();
await using var context = _contextFactory.CreateContext(enableTracking: false);
if (serverCache == null)
{
serverCache = await context.Set<EFServer>().ToListAsync();
}
serverCache ??= await context.Set<EFServer>().ToListAsync();
if (int.TryParse(query.ServerId, out int serverId))
if (int.TryParse(query.ServerId, out var serverId))
{
query.ServerId = serverCache.FirstOrDefault(_server => _server.ServerId == serverId)?.EndPoint ?? query.ServerId;
query.ServerId = serverCache.FirstOrDefault(server => server.ServerId == serverId)?.EndPoint ?? query.ServerId;
}
var iqMessages = context.Set<EFClientMessage>()
.Where(_message => _message.TimeSent >= query.SentAfter)
.Where(_message => _message.TimeSent < query.SentBefore);
.Where(message => message.TimeSent < query.SentBefore);
if (query.ClientId != null)
if (query.SentAfter is not null)
{
iqMessages = iqMessages.Where(_message => _message.ClientId == query.ClientId.Value);
iqMessages = iqMessages.Where(message => message.TimeSent >= query.SentAfter);
}
if (query.ServerId != null)
if (query.ClientId is not null)
{
iqMessages = iqMessages.Where(_message => _message.Server.EndPoint == query.ServerId);
iqMessages = iqMessages.Where(message => message.ClientId == query.ClientId.Value);
}
if (query.ServerId is not null)
{
iqMessages = iqMessages.Where(message => message.Server.EndPoint == query.ServerId);
}
if (!string.IsNullOrEmpty(query.MessageContains))
{
iqMessages = iqMessages.Where(_message => EF.Functions.Like(_message.Message.ToLower(), $"%{query.MessageContains.ToLower()}%"));
iqMessages = iqMessages.Where(message => EF.Functions.Like(message.Message.ToLower(), $"%{query.MessageContains.ToLower()}%"));
}
var iqResponse = iqMessages
.Select(_message => new MessageResponse
.Select(message => new MessageResponse
{
ClientId = _message.ClientId,
ClientName = query.IsProfileMeta ? "" : _message.Client.CurrentAlias.Name,
ServerId = _message.ServerId,
When = _message.TimeSent,
Message = _message.Message,
ServerName = query.IsProfileMeta ? "" : _message.Server.HostName,
GameName = _message.Server.GameName == null ? Server.Game.IW4 : (Server.Game)_message.Server.GameName.Value,
SentIngame = _message.SentIngame
ClientId = message.ClientId,
ClientName = query.IsProfileMeta ? "" : message.Client.CurrentAlias.Name,
ServerId = message.ServerId,
When = message.TimeSent,
Message = message.Message,
ServerName = query.IsProfileMeta ? "" : message.Server.HostName,
GameName = message.Server.GameName == null ? Server.Game.IW4 : (Server.Game)message.Server.GameName.Value,
SentIngame = message.SentIngame
});
if (query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending)
{
iqResponse = iqResponse.OrderByDescending(_message => _message.When);
}
else
{
iqResponse = iqResponse.OrderBy(_message => _message.When);
}
iqResponse = query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending
? iqResponse.OrderByDescending(message => message.When)
: iqResponse.OrderBy(message => message.When);
var resultList = await iqResponse
.Skip(query.Offset)
.Take(query.Count)
@ -115,13 +110,13 @@ namespace Stats.Helpers
{
var quickMessages = _defaultSettings
.QuickMessages
.First(_qm => _qm.Game == message.GameName);
.First(qm => qm.Game == message.GameName);
message.Message = quickMessages.Messages[message.Message.Substring(1)];
message.IsQuickMessage = true;
}
catch
{
message.Message = message.Message.Substring(1);
message.Message = message.Message[1..];
}
}

View File

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

View File

@ -42,10 +42,11 @@ namespace IW4MAdmin.Plugins.Stats
private readonly ILogger<Plugin> _logger;
private readonly List<IClientStatisticCalculator> _statCalculators;
private readonly IServerDistributionCalculator _serverDistributionCalculator;
private readonly IServerDataViewer _serverDataViewer;
public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory,
ITranslationLookup translationLookup, IMetaServiceV2 metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper, ILogger<StatManager> managerLogger,
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator)
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator, IServerDataViewer serverDataViewer)
{
Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
_databaseContextFactory = databaseContextFactory;
@ -56,6 +57,7 @@ namespace IW4MAdmin.Plugins.Stats
_logger = logger;
_statCalculators = statCalculators.ToList();
_serverDistributionCalculator = serverDistributionCalculator;
_serverDataViewer = serverDataViewer;
}
public async Task OnEventAsync(GameEvent gameEvent, Server server)
@ -201,13 +203,17 @@ namespace IW4MAdmin.Plugins.Stats
var performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed);
var performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
var spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Count(c => c.SPM > 0), 1);
var overallRanking = await Manager.GetClientOverallRanking(request.ClientId);
return new List<InformationResponse>
{
new InformationResponse
{
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
Value = "#" + (await Manager.GetClientOverallRanking(request.ClientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Value = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING_FORMAT"].FormatExt((overallRanking == 0 ? "--" :
overallRanking.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))),
(await _serverDataViewer.RankedClientsCountAsync(token: token)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))
),
Column = 0,
Order = 0,
Type = MetaType.Information

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