Compare commits

...

55 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
91 changed files with 4526 additions and 1663 deletions

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

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

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

@ -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;
@ -157,8 +158,6 @@ namespace IW4MAdmin
await E.Origin.Lock();
}
var canExecuteCommand = true;
try
{
if (!await ProcessEvent(E))
@ -187,32 +186,31 @@ namespace IW4MAdmin
}
}
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 cmd &&
(canExecuteCommand || E.Origin?.Level == Permission.Console))
{
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name, E.Origin.ToString());
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);
@ -771,17 +769,28 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.MetaUpdated)
{
if (E.Extra is "PersistentStatClientId" && int.TryParse(E.Data, out var persistentClientId))
if (E.Extra is "PersistentClientGuid")
{
var penalties = await Manager.GetPenaltyService().GetActivePenaltiesByClientId(persistentClientId);
var banPenalty = penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
var parts = E.Data.Split(",");
if (banPenalty is not null && E.Origin.Level != Permission.Banned)
if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
int.TryParse(parts[1], out var low))
{
ServerLogger.LogInformation(
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
E.Origin.ToString(), persistentClientId);
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(persistentClientId), Utilities.IW4MAdminClient(this), true);
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);
}
}
}
}
@ -1289,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

View File

@ -451,6 +451,8 @@ namespace IW4MAdmin.Application
.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

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

@ -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,13 +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(CancellationTokenSource).Assembly
typeof(CancellationTokenSource).Assembly,
typeof(Data.Models.Client.EFClient).Assembly,
typeof(IW4MAdmin.Plugins.Stats.Plugin).Assembly
})
.CatchClrExceptions()
.AddObjectConverter(new PermissionLevelToStringConverter()));
@ -338,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>
@ -356,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;
@ -439,7 +483,6 @@ namespace IW4MAdmin.Application.Misc
throw new PluginException("An error occured while executing action for script plugin");
}
finally
{
if (_onProcessing.CurrentCount == 0)
@ -458,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[]
{
@ -479,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();
@ -493,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[]
{
@ -513,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

@ -53,7 +53,7 @@ namespace IW4MAdmin.Application.RConParsers
Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7);
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n?(?:latched: \"(.+)?\"\n?)? *(.+)$";
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n?(?:latched: \"(.+)?\"\n?)? *(.+)?$";
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
@ -147,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
{
@ -164,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)
@ -176,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;
}
@ -193,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();
}
}
@ -227,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
{
@ -244,7 +244,7 @@ namespace IW4MAdmin.Application.RConParsers
AsyncState = false
});
}
}, token);
}, CancellationToken.None);
}
private List<EFClient> ClientsFromStatus(string[] Status)

View File

@ -7,8 +7,6 @@ namespace Data.Models.Client.Stats
{
public class EFClientRankingHistory: AuditFields
{
public const int MaxRankingCount = 1728;
[Key]
public long ClientRankingHistoryId { get; set; }

View File

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

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}"
@ -71,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
@ -375,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
@ -389,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(

View File

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

View File

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

View File

@ -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;
@ -478,15 +415,19 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
let data = [];
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
if (event.subType === 'Meta') {
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
const meta = metaService.GetPersistentMeta(event.data, client, token).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 ?? ''
};
}
@ -518,8 +459,8 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
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, token).GetAwaiter().GetResult()
: metaService.DecrementPersistentMeta(event.data['key'], event.data['value'], clientId, token).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, token).GetAwaiter().GetResult();
}
@ -627,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

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

@ -78,7 +78,7 @@ 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();

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,7 +213,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.ToListAsync();
var finished = statsInfo
.OrderByDescending(stat => rankingsDict[stat.ClientId].Last().PerformanceMetric)
.OrderByDescending(stat => rankingsDict[stat.ClientId].First().PerformanceMetric)
.Select((s, index) => new TopStatsInfo
{
ClientId = s.ClientId,
@ -195,24 +221,23 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Deaths = s.Deaths,
Kills = s.Kills,
KDR = Math.Round(s.KDR, 2),
LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection))
LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].First().LastConnection))
.HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false),
LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection),
LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].First().LastConnection),
Name = rankingsDict[s.ClientId].First().Name,
Performance = Math.Round(rankingsDict[s.ClientId].Last().PerformanceMetric ?? 0, 2),
RatingChange = (rankingsDict[s.ClientId].First().Ranking -
rankingsDict[s.ClientId].Last().Ranking) ?? 0,
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].Last().ZScore,
ZScore = rankingsDict[s.ClientId].First().ZScore,
ServerId = serverId
})
.OrderBy(r => r.Ranking)
.Take(60)
.ToList();
return finished;
@ -224,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>()
@ -267,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>()
@ -281,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),
});
@ -379,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
};
@ -389,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();
}
@ -482,7 +507,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
Active = true,
HitCount = 0,
Location = (int) hl
Location = (int)hl
}).ToList()
};
@ -502,7 +527,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
Active = true,
HitCount = 0,
Location = (int) hl
Location = (int)hl
})
.ToList();
@ -534,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());
@ -657,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),
@ -673,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;
@ -706,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;
}
@ -851,7 +876,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (KeyNotFoundException)
{
}
try
{
if (!gameDetectionTypes[server.GameName].Contains(detectionType))
@ -883,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}",
}
};
@ -900,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>()
@ -939,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)
{
@ -964,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;
@ -1055,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)
@ -1228,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)
@ -1236,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,
@ -1267,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))
@ -1287,7 +1313,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
clientStats.Client?.ToString(), aggregateZScore);
return;
}
var aggregateRankingSnapshot = new EFClientRankingHistory
{
ClientId = clientId,
@ -1310,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)
@ -1322,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);
}
}
}
@ -1338,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;
@ -1364,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);
@ -1374,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;
}
@ -1413,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
@ -1434,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));
@ -1455,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;
}
@ -1496,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();
@ -1562,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

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

View File

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

View File

@ -0,0 +1,13 @@
using System;
using System.Text.Json.Serialization;
namespace SharedLibraryCore.Dtos.Meta.Responses;
public class ClientNoteMetaResponse
{
public string Note { get; set; }
public int OriginEntityId { get; set; }
[JsonIgnore]
public string OriginEntityName { get; set; }
public DateTime ModifiedDate { get; set; }
}

View File

@ -7,6 +7,7 @@ namespace SharedLibraryCore.Dtos
public class PenaltyInfo : SharedInfo
{
public string OffenderName { get; set; }
public Permission OffenderLevel { get; set; }
public int OffenderId { get; set; }
public ulong OffenderNetworkId { get; set; }
public string OffenderIPAddress { get; set; }
@ -38,4 +39,4 @@ namespace SharedLibraryCore.Dtos
public string AdditionalPenaltyInformation =>
$"{(!string.IsNullOrEmpty(AutomatedOffense) ? $" ({AutomatedOffense})" : "")}{(IsEvade ? $" ({Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PENALTY_EVADE"]})" : "")}";
}
}
}

View File

@ -33,5 +33,7 @@ namespace SharedLibraryCore.Dtos
public string ConnectProtocolUrl { get;set; }
public string CurrentServerName { get; set; }
public IGeoLocationResult GeoLocationInfo { get; set; }
public ClientNoteMetaResponse NoteMeta { get; set; }
public List<IInteractionData> Interactions { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Data.Models.Client;
using SharedLibraryCore.Interfaces;
using InteractionCallback = System.Func<int, int?, Data.Models.Reference.Game?, System.Collections.Generic.IDictionary<string,string>, System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>;
namespace SharedLibraryCore.Helpers;
public class InteractionData : IInteractionData
{
public int? EntityId { get; set; }
public bool Enabled { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string DisplayMeta { get; set; }
public string ActionPath { get; set; }
public Dictionary<string, string> ActionMeta { get; set; } = new();
public string ActionUri => ActionPath + "?" + string.Join('&', ActionMeta.Select(kvp => $"{kvp.Key}={kvp.Value}"));
public EFClient.Permission? MinimumPermission { get; set; }
public string PermissionEntity { get; set; } = "Interaction";
public string PermissionAccess { get; set; } = "Read";
public string Source { get; set; }
public InteractionCallback Action { get; set; }
public Delegate ScriptAction { get; set; }
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using Data.Models.Client;
using InteractionCallback = System.Func<int, int?, Data.Models.Reference.Game?, System.Collections.Generic.IDictionary<string,string>, System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>;
namespace SharedLibraryCore.Interfaces;
public interface IInteractionData
{
int? EntityId { get; }
bool Enabled { get; }
string Name { get; }
string Description { get; }
string DisplayMeta { get; }
string ActionPath { get; }
Dictionary<string, string> ActionMeta { get; }
string ActionUri { get; }
EFClient.Permission? MinimumPermission { get; }
string PermissionEntity { get; }
string PermissionAccess { get; }
string Source { get; }
InteractionCallback Action { get; }
Delegate ScriptAction { get; }
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
namespace SharedLibraryCore.Interfaces;
public interface IInteractionRegistration
{
void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration);
void RegisterInteraction(string interactionName, Func<int?, Reference.Game?, CancellationToken, Task<IInteractionData>> interactionRegistration);
void UnregisterInteraction(string interactionName);
Task<IEnumerable<IInteractionData>> GetInteractions(int? clientId = null,
Reference.Game? game = null, CancellationToken token = default);
Task<string> ProcessInteraction(string interactionId, int originId, int? targetId = null, Reference.Game? game = null, IDictionary<string, string> meta = null, CancellationToken token = default);
}

View File

@ -23,6 +23,7 @@ namespace SharedLibraryCore.Interfaces
IList<IRConParser> AdditionalRConParsers { get; }
IList<IEventParser> AdditionalEventParsers { get; }
IMiddlewareActionHandler MiddlewareActionHandler { get; }
IList<Func<GameEvent, bool>> CommandInterceptors { get; }
string Version { get; }
ITokenAuthentication TokenAuthenticator { get; }
string ExternalIPAddress { get; }
@ -102,7 +103,7 @@ namespace SharedLibraryCore.Interfaces
/// event executed when event has finished executing
/// </summary>
event EventHandler<GameEvent> OnGameEventExecuted;
IAlertManager AlertManager { get; }
}
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using SharedLibraryCore.Dtos;
namespace SharedLibraryCore.Interfaces;
public interface IRemoteCommandService
{
Task<IEnumerable<CommandResponseInfo>> Execute(int originId, int? targetId, string command, IEnumerable<string> arguments, Server server);
}

View File

@ -286,7 +286,10 @@ namespace SharedLibraryCore.Services
entity.PasswordSalt = temporalClient.PasswordSalt;
}
entity.GameName = temporalClient.GameName;
if (entity.GameName == Reference.Game.UKN && temporalClient.GameName != entity.GameName)
{
entity.GameName = temporalClient.GameName;
}
// update in database
await context.SaveChangesAsync();
@ -938,6 +941,14 @@ namespace SharedLibraryCore.Services
return clientList;
}
public async Task<string> GetClientNameById(int clientId)
{
await using var context = _contextFactory.CreateContext();
var match = await context.Clients.Select(client => new { client.CurrentAlias.Name, client.ClientId })
.FirstOrDefaultAsync(client => client.ClientId == clientId);
return match?.Name;
}
#endregion
}
}

View File

@ -117,6 +117,7 @@ namespace SharedLibraryCore.Services
AutomatedOffense = _penalty.AutomatedOffense,
OffenderId = _penalty.OffenderId,
OffenderName = _penalty.Offender.CurrentAlias.Name,
OffenderLevel = _penalty.Offender.Level,
PunisherId = _penalty.PunisherId,
PunisherName = _penalty.Punisher.CurrentAlias.Name,
PunisherLevel = _penalty.Punisher.Level,
@ -192,17 +193,7 @@ namespace SharedLibraryCore.Services
.Where(FilterById);
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync();
}
public async Task<List<EFPenalty>> GetActivePenaltiesByClientId(int clientId)
{
await using var context = _contextFactory.CreateContext(false);
return await context.PenaltyIdentifiers
.Where(identifier => identifier.Penalty.Offender.ClientId == clientId)
.Select(identifier => identifier.Penalty)
.Where(Filter)
.ToListAsync();
}
public async Task<List<EFPenalty>> ActivePenaltiesByRecentIdentifiers(int linkId)
{
await using var context = _contextFactory.CreateContext(false);

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2022.6.16.1</Version>
<Version>2022.10.13.1</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2022.6.16.1</PackageVersion>
<PackageVersion>2022.10.13.1</PackageVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
@ -34,23 +34,23 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="10.3.6" />
<PackageReference Include="Humanizer.Core" Version="2.13.14" />
<PackageReference Include="Humanizer.Core.ru" Version="2.13.14" />
<PackageReference Include="Humanizer.Core.de" Version="2.13.14" />
<PackageReference Include="Humanizer.Core.es" Version="2.13.14" />
<PackageReference Include="Humanizer.Core.pt" Version="2.13.14" />
<PackageReference Include="FluentValidation" Version="11.2.1" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.ru" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.de" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.es" Version="2.14.1" />
<PackageReference Include="Humanizer.Core.pt" Version="2.14.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup>

View File

@ -2,17 +2,21 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using WebfrontCore.Permissions;
using WebfrontCore.ViewModels;
namespace WebfrontCore.Controllers
@ -20,6 +24,9 @@ namespace WebfrontCore.Controllers
public class ActionController : BaseController
{
private readonly ApplicationConfiguration _appConfig;
private readonly IMetaServiceV2 _metaService;
private readonly IInteractionRegistration _interactionRegistration;
private readonly IRemoteCommandService _remoteCommandService;
private readonly string _banCommandName;
private readonly string _tempbanCommandName;
private readonly string _unbanCommandName;
@ -29,11 +36,17 @@ namespace WebfrontCore.Controllers
private readonly string _flagCommandName;
private readonly string _unflagCommandName;
private readonly string _setLevelCommandName;
private readonly string _setClientTagCommandName;
private readonly string _addClientNoteCommandName;
public ActionController(IManager manager, IEnumerable<IManagerCommand> registeredCommands,
ApplicationConfiguration appConfig) : base(manager)
ApplicationConfiguration appConfig, IMetaServiceV2 metaService,
IInteractionRegistration interactionRegistration, IRemoteCommandService remoteCommandService) : base(manager)
{
_appConfig = appConfig;
_metaService = metaService;
_interactionRegistration = interactionRegistration;
_remoteCommandService = remoteCommandService;
foreach (var cmd in registeredCommands)
{
@ -69,10 +82,139 @@ namespace WebfrontCore.Controllers
case "OfflineMessageCommand":
_offlineMessageCommandName = cmd.Name;
break;
case "SetClientTagCommand":
_setClientTagCommandName = cmd.Name;
break;
case "AddClientNoteCommand":
_addClientNoteCommandName = cmd.Name;
break;
}
}
}
public IActionResult DynamicActionForm(int? id, string meta)
{
var metaDict = JsonSerializer.Deserialize<Dictionary<string, string>>(meta);
if (metaDict is null)
{
return BadRequest();
}
metaDict.TryGetValue(nameof(ActionInfo.ActionButtonLabel), out var label);
metaDict.TryGetValue(nameof(ActionInfo.Name), out var name);
metaDict.TryGetValue(nameof(ActionInfo.ShouldRefresh), out var refresh);
metaDict.TryGetValue("Data", out var data);
metaDict.TryGetValue("InteractionId", out var interactionId);
metaDict.TryGetValue("Inputs", out var template);
List<InputInfo> additionalInputs = null;
var inputKeys = string.Empty;
if (!string.IsNullOrWhiteSpace(template))
{
additionalInputs = JsonSerializer.Deserialize<List<InputInfo>>(template);
}
if (additionalInputs is not null)
{
inputKeys = string.Join(",", additionalInputs.Select(input => input.Name));
}
bool.TryParse(refresh, out var shouldRefresh);
var inputs = new List<InputInfo>
{
new()
{
Name = "InteractionId",
Value = interactionId,
Type = "hidden"
},
new()
{
Name = "data",
Value = data,
Type = "hidden"
},
new()
{
Name = "TargetId",
Value = id?.ToString(),
Type = "hidden"
},
new()
{
Name = "CustomInputKeys",
Value = inputKeys,
Type = "hidden"
}
};
if (additionalInputs?.Any() ?? false)
{
inputs.AddRange(additionalInputs);
}
var info = new ActionInfo
{
ActionButtonLabel = label,
Name = name,
Action = nameof(DynamicActionAsync),
ShouldRefresh = shouldRefresh,
Inputs = inputs
};
return View("_ActionForm", info);
}
public async Task<IActionResult> DynamicActionAsync(CancellationToken token = default)
{
HttpContext.Request.Query.TryGetValue("InteractionId", out var interactionId);
HttpContext.Request.Query.TryGetValue("CustomInputKeys", out var inputKeys);
HttpContext.Request.Query.TryGetValue("Data", out var data);
HttpContext.Request.Query.TryGetValue("TargetId", out var targetIdString);
var inputs = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(inputKeys.ToString()))
{
foreach (var key in inputKeys.ToString().Split(","))
{
HttpContext.Request.Query.TryGetValue(key, out var input);
if (string.IsNullOrWhiteSpace(input))
{
continue;
}
inputs.Add(key, HttpContext.Request.Query[key]);
}
}
var game = (Reference.Game?)null;
var targetId = (int?)null;
if (int.TryParse(targetIdString.ToString().Split(",").Last(), out var parsedTargetId))
{
targetId = parsedTargetId;
}
if (targetId.HasValue)
{
game = (await Manager.GetClientService().Get(targetId.Value))?.GameName;
}
if (interactionId.ToString() != "command")
{
return Ok(await _interactionRegistration.ProcessInteraction(interactionId, Client.ClientId, targetId, game, inputs,
token));
}
var server = Manager.GetServers().First();
return Ok(await _remoteCommandService.Execute(Client.ClientId, targetId, data, inputs.Values.Select(input => input), server));
}
public IActionResult BanForm()
{
var info = new ActionInfo
@ -279,7 +421,7 @@ namespace WebfrontCore.Controllers
{
ClientId = Client.ClientId
});
return string.Format(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_SUCCESS"],
state.Token,
$"{state.RemainingTime} {Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_MINUTES"]}",
@ -582,6 +724,103 @@ namespace WebfrontCore.Controllers
}));
}
public async Task<IActionResult> SetClientTagForm(int id, CancellationToken token)
{
var tags = await _metaService.GetPersistentMetaValue<List<LookupValue<string>>>(EFMeta.ClientTagNameV2,
token) ?? new List<LookupValue<string>>();
var existingTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
EFMeta.ClientTagNameV2, id, Manager.CancellationToken);
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_SUBMIT"],
Name = Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TAG"],
Inputs = new List<InputInfo>
{
new()
{
Name = "clientTag",
Type = "select",
Label = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_FORM_TAG"],
Values = tags.ToDictionary(
item => item.Value == existingTag?.Value ? $"!selected!{item.Value}" : item.Value,
item => item.Value)
}
},
Action = nameof(SetClientTag),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> SetClientTag(int targetId, string clientTag)
{
if (targetId <= 0 || string.IsNullOrWhiteSpace(clientTag))
{
return Json(new[]
{
new CommandResponseInfo
{
Response = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_NONE"]
}
});
}
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command =
$"{_appConfig.CommandPrefix}{_setClientTagCommandName} @{targetId} {clientTag}"
}));
}
public async Task<IActionResult> AddClientNoteForm(int id)
{
var existingNote = await _metaService.GetPersistentMetaValue<ClientNoteMetaResponse>("ClientNotes", id);
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_CONFIGURATION_BUTTON_SAVE"],
Name = Localization["WEBFRONT_PROFILE_CONTEXT_MENU_NOTE"],
Inputs = new List<InputInfo>
{
new()
{
Name = "note",
Label = Localization["WEBFRONT_ACTION_NOTE_FORM_NOTE"],
Value = existingNote?.Note,
Type = "textarea"
}
},
Action = nameof(AddClientNote),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> AddClientNote(int targetId, string note)
{
if (note?.Length > 350 || note?.Count(c => c == '\n') > 4)
{
return StatusCode(StatusCodes.Status400BadRequest, new[]
{
new CommandResponseInfo
{
Response = Localization["WEBFRONT_ACTION_NOTE_INVALID_LENGTH"]
}
});
}
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command =
$"{_appConfig.CommandPrefix}{_addClientNoteCommandName} @{targetId} {note}"
}));
}
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
.Concat(_appConfig.GlobalRules)
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))

View File

@ -12,6 +12,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using SharedLibraryCore.Services;
using Stats.Config;
using WebfrontCore.Permissions;
using WebfrontCore.ViewComponents;
@ -23,13 +24,17 @@ namespace WebfrontCore.Controllers
private readonly IMetaServiceV2 _metaService;
private readonly StatsConfiguration _config;
private readonly IGeoLocationService _geoLocationService;
private readonly ClientService _clientService;
private readonly IInteractionRegistration _interactionRegistration;
public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config,
IGeoLocationService geoLocationService) : base(manager)
IGeoLocationService geoLocationService, ClientService clientService, IInteractionRegistration interactionRegistration) : base(manager)
{
_metaService = metaService;
_config = config;
_geoLocationService = geoLocationService;
_clientService = clientService;
_interactionRegistration = interactionRegistration;
}
[Obsolete]
@ -53,22 +58,32 @@ namespace WebfrontCore.Controllers
{
_metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2, EFMeta.ClientTagNameV2, client.ClientId,
token),
_metaService.GetPersistentMeta("GravatarEmail", client.ClientId, token)
_metaService.GetPersistentMeta("GravatarEmail", client.ClientId, token),
};
var persistentMeta = await Task.WhenAll(persistentMetaTask);
var tag = persistentMeta[0];
var gravatar = persistentMeta[1];
var note = await _metaService.GetPersistentMetaValue<ClientNoteMetaResponse>("ClientNotes", client.ClientId,
token);
if (tag?.Value != null)
{
client.SetAdditionalProperty(EFMeta.ClientTagV2, tag.Value);
}
if (!string.IsNullOrWhiteSpace(note?.Note))
{
note.OriginEntityName = await _clientService.GetClientNameById(note.OriginEntityId);
}
var interactions = await _interactionRegistration.GetInteractions(id, client.GameName, token);
// even though we haven't set their level to "banned" yet
// (ie they haven't reconnected with the infringing player identifier)
// we want to show them as banned as to not confuse people.
if (activePenalties.Any(penalty => penalty.Type == EFPenalty.PenaltyType.Ban))
var hasActiveBan = activePenalties.Any(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
if (hasActiveBan)
{
client.Level = Data.Models.Client.EFClient.Permission.Banned;
}
@ -76,7 +91,9 @@ namespace WebfrontCore.Controllers
var displayLevelInt = (int)client.Level;
var displayLevel = client.Level.ToLocalizedLevelName();
if (!Authorized && client.Level.ShouldHideLevel())
// if a linked ban has been revoked but they haven't reconnected, we should not show them as still banned
var shouldHideBanLevel = !hasActiveBan && client.Level == Data.Models.Client.EFClient.Permission.Banned;
if (!Authorized && client.Level.ShouldHideLevel() || shouldHideBanLevel)
{
displayLevelInt = (int)Data.Models.Client.EFClient.Permission.User;
displayLevel = Data.Models.Client.EFClient.Permission.User.ToLocalizedLevelName();
@ -123,7 +140,9 @@ namespace WebfrontCore.Controllers
: ingameClient.CurrentServer.IP,
ingameClient.CurrentServer.Port),
CurrentServerName = ingameClient?.CurrentServer?.Hostname,
GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString)
GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString),
NoteMeta = string.IsNullOrWhiteSpace(note?.Note) ? null: note,
Interactions = interactions.ToList()
};
var meta = await _metaService.GetRuntimeMeta<InformationResponse>(new ClientPaginationRequest

View File

@ -1,22 +1,19 @@
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Linq;
using System.Threading.Tasks;
using Data.Models;
namespace WebfrontCore.Controllers
{
public class ConsoleController : BaseController
{
private readonly ApplicationConfiguration _appconfig;
private readonly IRemoteCommandService _remoteCommandService;
public ConsoleController(IManager manager) : base(manager)
public ConsoleController(IManager manager, IRemoteCommandService remoteCommandService) : base(manager)
{
_appconfig = manager.GetApplicationSettings().Configuration();
_remoteCommandService = remoteCommandService;
}
public IActionResult Index()
@ -37,75 +34,8 @@ namespace WebfrontCore.Controllers
public async Task<IActionResult> Execute(long serverId, string command)
{
var server = Manager.GetServers().First(s => s.EndPoint == serverId);
var client = new EFClient
{
ClientId = Client.ClientId,
Level = Client.Level,
NetworkId = Client.NetworkId,
CurrentServer = server,
CurrentAlias = new EFAlias()
{
Name = Client.Name
}
};
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
};
Manager.AddEvent(remoteEvent);
CommandResponseInfo[] response = null;
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 remoteEvent.Failed ? StatusCode(400, response) : Ok(response);
var response = await _remoteCommandService.Execute(Client.ClientId, null, command, Enumerable.Empty<string>(), server);
return !response.Any() ? StatusCode(400, response) : Ok(response);
}
}
}

View File

@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Authentication;
using System;
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
@ -8,6 +10,7 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Data.Models.Client;
using Microsoft.AspNetCore.Authentication.Cookies;
using SharedLibraryCore.Commands;
using static SharedLibraryCore.GameEvent;
namespace WebfrontCore.Middleware
@ -18,68 +21,25 @@ namespace WebfrontCore.Middleware
internal class ClaimsPermissionRemoval
{
private readonly IManager _manager;
private readonly List<int> _privilegedClientIds;
private static readonly ConcurrentDictionary<int, (ClaimsState, DateTimeOffset?)> PrivilegedClientIds = new();
private readonly RequestDelegate _nextRequest;
private enum ClaimsState
{
Current,
Tainted
}
public ClaimsPermissionRemoval(RequestDelegate nextRequest, IManager manager)
{
_manager = manager;
_manager.OnGameEventExecuted += OnGameEvent;
_privilegedClientIds = new List<int>();
_nextRequest = nextRequest;
}
/// <summary>
/// Callback for the game event
/// </summary>
/// <param name="sender"></param>
/// <param name="gameEvent"></param>
private void OnGameEvent(object sender, GameEvent gameEvent)
{
if (gameEvent.Type != EventType.ChangePermission || gameEvent.Extra is not EFClient.Permission perm)
{
return;
}
lock (_privilegedClientIds)
{
switch (perm)
{
// we want to remove the claims when the client is demoted
case < EFClient.Permission.Trusted:
{
_privilegedClientIds.RemoveAll(id => id == gameEvent.Target.ClientId);
break;
}
// and add if promoted
case > EFClient.Permission.Trusted when !_privilegedClientIds.Contains(gameEvent.Target.ClientId):
{
_privilegedClientIds.Add(gameEvent.Target.ClientId);
break;
}
}
}
}
public async Task Invoke(HttpContext context)
{
// we want to load the initial list of privileged clients
bool hasAny;
lock (_privilegedClientIds)
{
hasAny = _privilegedClientIds.Any();
}
if (!hasAny)
{
var ids = (await _manager.GetClientService().GetPrivilegedClients())
.Select(client => client.ClientId);
lock (_privilegedClientIds)
{
_privilegedClientIds.AddRange(ids);
}
}
await Initialize();
// sid stores the clientId
var claimsId = context.User.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value;
@ -87,14 +47,16 @@ namespace WebfrontCore.Middleware
if (!string.IsNullOrEmpty(claimsId))
{
var clientId = int.Parse(claimsId);
bool hasKey;
lock (_privilegedClientIds)
bool isTainted;
bool hasPrivilege;
lock (PrivilegedClientIds)
{
hasKey = _privilegedClientIds.Contains(clientId);
hasPrivilege = PrivilegedClientIds.ContainsKey(clientId);
isTainted = hasPrivilege && PrivilegedClientIds[clientId].Item1 == ClaimsState.Tainted;
}
// they've been removed
if (!hasKey && clientId != 1)
if (!hasPrivilege || isTainted)
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
@ -102,5 +64,141 @@ namespace WebfrontCore.Middleware
await _nextRequest.Invoke(context);
}
private void OnGameEvent(object sender, GameEvent gameEvent)
{
if (gameEvent.Extra?.GetType() == typeof(SetPasswordCommand))
{
lock (PrivilegedClientIds)
{
PrivilegedClientIds[gameEvent.Origin.ClientId] = (ClaimsState.Tainted, DateTimeOffset.UtcNow);
}
return;
}
if (gameEvent.Type != EventType.ChangePermission || gameEvent.Extra is not EFClient.Permission perm)
{
return;
}
lock (PrivilegedClientIds)
{
switch (perm)
{
// we want to remove the claims when the client is demoted
case < EFClient.Permission.Trusted when PrivilegedClientIds.ContainsKey(gameEvent.Target.ClientId):
{
PrivilegedClientIds.Remove(gameEvent.Target.ClientId, out _);
break;
}
// and add if promoted
case > EFClient.Permission.Trusted:
{
if (!PrivilegedClientIds.ContainsKey(gameEvent.Target.ClientId))
{
PrivilegedClientIds.TryAdd(gameEvent.Target.ClientId, (ClaimsState.Current, null));
}
else
{
// they've been intra-moted, so we need to taint their claims
PrivilegedClientIds[gameEvent.Target.ClientId] =
(ClaimsState.Tainted, DateTimeOffset.UtcNow);
}
break;
}
}
}
}
private async Task Initialize()
{
// we want to load the initial list of privileged clients
bool hasAny;
lock (PrivilegedClientIds)
{
hasAny = PrivilegedClientIds.Any();
}
if (!hasAny)
{
var ids = (await _manager.GetClientService().GetPrivilegedClients())
.Select(client => client.ClientId);
lock (PrivilegedClientIds)
{
foreach (var id in ids)
{
PrivilegedClientIds.TryAdd(id, (ClaimsState.Current, null));
}
}
}
}
public static async Task ValidateAsync(CookieValidatePrincipalContext context)
{
if (context.Principal is null)
{
return;
}
var claimsId = context.Principal.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value;
if (string.IsNullOrEmpty(claimsId))
{
return;
}
var clientId = int.Parse(claimsId);
bool shouldSignOut;
lock (PrivilegedClientIds)
{
// we want to log them out if they aren't in the privileged clients list
// or the token is tainted or the taint event occured after the token was generated
shouldSignOut = PrivilegedClientIds.ContainsKey(clientId) &&
(PrivilegedClientIds[clientId].Item1 == ClaimsState.Tainted ||
PrivilegedClientIds[clientId].Item2 is not null &&
PrivilegedClientIds[clientId].Item2.Value - context.Properties.IssuedUtc >
TimeSpan.FromSeconds(30));
}
if (shouldSignOut)
{
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
public static Task OnSignedIn(CookieSignedInContext context)
{
if (context.Principal is null)
{
return Task.CompletedTask;
}
var claimsId = context.Principal.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value;
if (string.IsNullOrEmpty(claimsId))
{
return Task.CompletedTask;
}
var clientId = int.Parse(claimsId);
lock (PrivilegedClientIds)
{
if (PrivilegedClientIds.ContainsKey(clientId))
{
PrivilegedClientIds[clientId] = PrivilegedClientIds[clientId].Item1 == ClaimsState.Tainted
? (ClaimsState.Current, DateTimeOffset.UtcNow)
: (ClaimsState.Current, null);
}
}
return Task.CompletedTask;
}
}
}

View File

@ -14,13 +14,14 @@ public enum WebfrontEntity
AuditPage,
RecentPlayersPage,
ProfilePage,
AdminMenu
AdminMenu,
ClientNote,
Interaction
}
public enum WebfrontPermission
{
Read,
Create,
Update,
Write,
Delete
}

View File

@ -105,6 +105,8 @@ namespace WebfrontCore
{
options.AccessDeniedPath = "/";
options.LoginPath = "/";
options.Events.OnValidatePrincipal += ClaimsPermissionRemoval.ValidateAsync;
options.Events.OnSignedIn += ClaimsPermissionRemoval.OnSignedIn;
});
services.AddSingleton(Program.Manager);
@ -138,6 +140,8 @@ namespace WebfrontCore
services.AddSingleton(Program.ApplicationServiceProvider
.GetRequiredService<StatsConfiguration>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IServerDataViewer>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IInteractionRegistration>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IRemoteCommandService>());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Data.Models.Client;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Razor.TagHelpers;
using SharedLibraryCore;
@ -30,9 +31,9 @@ public class HasPermission : TagHelper
{
output.TagName = null;
var permissionLevel = _contextAccessor?.HttpContext?.User.Claims
.FirstOrDefault(claim => claim.Type == ClaimTypes.Role)?.Value;
.FirstOrDefault(claim => claim.Type == ClaimTypes.Role)?.Value ?? EFClient.Permission.User.ToString();
var hasPermission = permissionLevel != null && _permissionSets.ContainsKey(permissionLevel) &&
var hasPermission = _permissionSets.ContainsKey(permissionLevel) &&
_permissionSets[permissionLevel].HasPermission(Entity, Permission);
if (!hasPermission)
{

View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace WebfrontCore.ViewModels
{

View File

@ -12,6 +12,7 @@ public class SideContextMenuItem
public string Icon { get; set; }
public string Tooltip { get; set; }
public int? EntityId { get; set; }
public string Meta { get; set; }
}

View File

@ -76,7 +76,7 @@
@foreach (var rule in rules)
{
<div class="rule">
@if (!rule.StartsWith("#") && !Regex.IsMatch(rule, @"^\d+(.|\))"))
@if (!rule.StartsWith("#") && !Regex.IsMatch(rule.StripColors(), @"^(Rule ?#?)?\d+(.|\))"))
{
<span>@start.</span>
}

View File

@ -3,7 +3,7 @@
@{
Layout = null;
}
<h5 class="modal-title mb-10">@Model.Name.Titleize()</h5>
<h5 class="modal-title mb-10">@Model.Name?.Titleize()</h5>
@if (Model.Inputs.Any(input => input.Type != "hidden"))
{
<hr class="mb-10"/>
@ -25,11 +25,20 @@
@if (inputType == "select")
{
<select name="@input.Name" class="form-control" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">
@foreach (var item in input.Values)
@foreach (var (key, item) in input.Values)
{
<option value="@item.Key">
<color-code value="@item.Value"></color-code>
</option>
if (key.StartsWith("!selected!"))
{
<option value="@key.Replace("!selected!", "")" selected>
<color-code value="@item"></color-code>
</option>
}
else
{
<option value="@key">
<color-code value="@item"></color-code>
</option>
}
}
</select>
}
@ -42,6 +51,11 @@
</label>
</div>
}
else if (inputType == "textarea")
{
<textarea name="@input.Name" class="form-control @(input.Required ? "required" : "")" placeholder="@input.Placeholder" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">@value</textarea>
}
else
{

View File

@ -30,7 +30,7 @@
</td>
<td>
@info.Data
<td>
<td class="text-force-break font-weight-light">
@info.NewValue
</td>
<td class="text-right">
@ -40,32 +40,36 @@
<!-- mobile -->
<tr class="d-table-row d-lg-none d-flex bg-dark-dm bg-light-lm">
<td class="bg-primary text-light text-right flex-grow-0">
<td class="bg-primary text-light text-right flex-grow-0 w-quarter d-flex flex-column">
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_INFO"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_CURRENT"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</div>
<div class="mt-5 mb-5 mt-auto">@loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</div>
</td>
<td>
<td class="w-three-quarter d-flex flex-column">
<div class="mt-5 mb-5">@info.Action</div>
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.OriginId" class="link-inverse">
<color-code value="@info.OriginName"></color-code>
</a>
<div class="mt-5 mb-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.OriginId" class="link-inverse">
<color-code value="@info.OriginName"></color-code>
</a>
</div>
@if (info.TargetId != null)
{
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.TargetId" class="mt-5 mb-5">
<color-code value="@info.TargetName"></color-code>
</a>
<div class="mt-5 mb-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.TargetId">
<color-code value="@info.TargetName"></color-code>
</a>
</div>
}
else
{
<div class="mt-5 mb-5">&ndash;</div>
}
<div class="mt-5 mb-5">@info.Data</div>
<div class="mt-5 mb-5">@info.NewValue</div>
<div class="mt-5 mb-5">@info.When</div>
<div class="mt-5 mb-5 text-force-break">@info.NewValue</div>
<div class="mt-5 mb-5 text-muted">@info.When.ToStandardFormat()</div>
</td>
</tr>
}

View File

@ -22,7 +22,7 @@
EFPenalty.PenaltyType.Flag => "alert-secondary",
EFPenalty.PenaltyType.TempBan => "alert-secondary",
_ => "alert"
};
};
}
string ClassForProfileBackground()
@ -173,6 +173,27 @@
</div>
</div>
@if (!string.IsNullOrWhiteSpace(Model.NoteMeta?.Note))
{
<has-permission entity="ClientNote" required-permission="Read">
<div class="rounded border p-10 m-10 d-flex flex-column flex-md-row" style="border-style: dashed !important">
<i class="align-self-center oi oi-clipboard"></i>
<div class="align-self-center font-size-12 font-weight-light pl-10 pr-10">
@foreach (var line in Model.NoteMeta.Note.Split("\n"))
{
<div class="text-force-break">@line.TrimEnd('\r')</div>
}
<div class="mt-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.NoteMeta.OriginEntityId" class="no-decoration ">
<color-code value="@Model.NoteMeta.OriginEntityName"></color-code>
</a>
<span>&mdash; @Model.NoteMeta.ModifiedDate.HumanizeForCurrentCulture()</span>
</div>
</div>
</div>
</has-permission>
}
<div class="flex-fill d-flex justify-content-center justify-content-md-end mt-10 mt-md-0">
<!-- country flag -->
<div id="ipGeoDropdown" class="dropdown with-arrow align-self-center">
@ -279,9 +300,30 @@
EntityId = Model.ClientId
});
}
if (ViewBag.Authorized)
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TAG"],
IsButton = true,
Reference = "SetClientTag",
Icon = "oi-tag",
EntityId = Model.ClientId
});
if ((ViewBag.PermissionsSet as IEnumerable<string>).HasPermission(WebfrontEntity.ClientNote, WebfrontPermission.Write))
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_NOTE"],
IsButton = true,
Reference = "AddClientNote",
Icon = "oi-clipboard",
EntityId = Model.ClientId
});
}
menuItems.Items.Add(new SideContextMenuItem
{
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MESSAGE"],
@ -349,6 +391,20 @@
});
}
foreach (var interaction in Model.Interactions.Where(i => (int)ViewBag.User.Level >= ((int?)i.MinimumPermission ?? 0)))
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = interaction.Name,
Tooltip = interaction.Description,
EntityId = interaction.EntityId,
Icon = interaction.DisplayMeta,
Reference = interaction.ActionPath,
Meta = System.Text.Json.JsonSerializer.Serialize(interaction.ActionMeta),
IsButton = true
});
}
}
<partial name="_SideContextMenu" for="@menuItems"></partial>

View File

@ -124,11 +124,12 @@
var spm = Model.ServerId != null ? serverLegacyStat?.SPM.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.SPM), 0).ToNumericalString();
var performanceHistory = Model.Ratings
.OrderBy(rating => rating.CreatedDateTime)
.Select(rating => new PerformanceHistory { Performance = rating.PerformanceMetric, OccurredAt = rating.CreatedDateTime });
if (performance != null)
if (performance != null && performance != Model.Ratings.FirstOrDefault().PerformanceMetric)
{
performanceHistory = performanceHistory.Append(new PerformanceHistory { Performance = performance.Value, OccurredAt = DateTime.UtcNow });
performanceHistory = performanceHistory.Append(new PerformanceHistory { Performance = performance.Value, OccurredAt = Model.Ratings.FirstOrDefault()?.CreatedDateTime ?? DateTime.UtcNow });
}
var score = allPerServer.Any()
@ -285,7 +286,7 @@
<!-- history graph -->
@if (performanceHistory.Count() > 5)
{
<div class="w-half m-auto ml-lg-auto " id="client_performance_history_container">
<div class="w-full w-lg-half m-auto ml-lg-auto" id="client_performance_history_container">
<canvas id="client_performance_history" data-history="@(JsonSerializer.Serialize(performanceHistory))"></canvas>
</div>
}

View File

@ -1,5 +1,4 @@
@using IW4MAdmin.Plugins.Stats
@using System.Text.Json.Serialization
@using System.Text.Json
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
@{
@ -86,7 +85,7 @@
</div>
</div>
<div class="w-full w-md-half client-rating-graph pt-10 pb-10">
<canvas id="rating_history_@(stat.ClientId + "_" + stat.Id)" data-history="@(JsonSerializer.Serialize(stat.PerformanceHistory))"></canvas>
<canvas id="rating_history_@(stat.ClientId + "_" + stat.Id)" data-history="@(JsonSerializer.Serialize(stat.PerformanceHistory.OrderBy(perf => perf.OccurredAt)))"></canvas>
</div>
<div class="w-quarter align-self-center d-flex justify-content-center">
<img class="w-100 h-100" src="~/images/stats/ranks/rank_@(stat.ZScore.RankIconIndexForZScore()).png" alt="@stat.Performance"/>

View File

@ -5,54 +5,107 @@
<div class="content mt-20">
@foreach (var (pluginName, commandList) in Model)
{
<h2 class="content-title mb-lg-20 mt-20 ">@(pluginName == "Native" ? ViewBag.Localization["WEBFRONT_HELP_COMMANDS_NATIVE_TITLE"] : pluginName)</h2>
<table class="table rounded">
<thead>
<!-- desktop -->
<tr class="bg-primary text-light d-none d-lg-table-row ">
<th>@loc["WEBFRONT_HELP_COMMAND_DESC_NAME"]</th>
<th>@loc["WEBFRONT_HELP_COMMAND_DESC_ALIAS"]</th>
<th>@loc["WEBFRONT_HELP_COMMAND_DESC_DESCRIPTION"]</th>
<th>@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRES_TARGET"]</th>
<th>@loc["WEBFRONT_HELP_COMMAND_DESC_SYNTAX"]</th>
<th class="text-right">@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRED_LEVEL"]</th>
</tr>
</thead>
<tbody>
@foreach (var command in commandList)
{
<div class="command-assembly-container">
<h2 class="content-title mb-lg-20 mt-20 ">@(pluginName == "Native" ? ViewBag.Localization["WEBFRONT_HELP_COMMANDS_NATIVE_TITLE"] : pluginName)</h2>
<table class="table rounded">
<thead>
<!-- desktop -->
<tr class="d-none d-lg-table-row bg-dark-dm bg-light-lm">
<td>@command.Name</td>
<td>@command.Alias</td>
<td>@command.Description</td>
<td>@command.RequiresTarget</td>
<td>@ViewBag.CommandPrefix@command.Syntax.Split(ViewBag.CommandPrefix)[1]</td>
<td class="text-right level-color-@((int)command.Permission)">@command.Permission.ToLocalizedLevelName()</td>
<tr class="bg-primary text-light d-none d-lg-table-row ">
<th>@loc["WEBFRONT_HELP_COMMAND_DESC_NAME"]</th>
<th>@loc["WEBFRONT_HELP_COMMAND_DESC_ALIAS"]</th>
<th>@loc["WEBFRONT_HELP_COMMAND_DESC_DESCRIPTION"]</th>
<th>@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRES_TARGET"]</th>
<th>@loc["WEBFRONT_HELP_COMMAND_DESC_SYNTAX"]</th>
<th>@loc["WEBFRONT_HELP_SUPPORTED_GAMES"]</th>
<th class="text-right">@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRED_LEVEL"]</th>
</tr>
<!-- mobile -->
<tr class="d-table-row d-lg-none d-flex bg-dark-dm bg-light-lm">
<td class="bg-primary text-right" style="min-width: 124px;">
<div class="mt-5 mb-5">@loc["WEBFRONT_HELP_COMMAND_DESC_NAME"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_HELP_COMMAND_DESC_ALIAS"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_HELP_COMMAND_DESC_DESCRIPTION"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRES_TARGET"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_HELP_COMMAND_DESC_SYNTAX"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRED_LEVEL"]</div>
</td>
<td>
<div class="mt-5 mb-5">@command.Name</div>
<div class="mt-5 mb-5">@command.Alias</div>
<div class="mt-5 mb-5">@command.Description</div>
<div class="mt-5 mb-5">@command.RequiresTarget</div>
<div class="mt-5 mb-5">@ViewBag.CommandPrefix@command.Syntax.Split(ViewBag.CommandPrefix)[1]</div>
<div class="mt-5 mb-5 @($"level-color-{(int)command.Permission}")">@command.Permission.ToLocalizedLevelName()</div>
</td>
</tr>
}
</tbody>
</table>
</thead>
<tbody>
@foreach (var command in commandList)
{
<!-- desktop -->
<tr class="d-none d-lg-table-row bg-dark-dm bg-light-lm">
<td class="font-weight-bold">@command.Name</td>
<td>@command.Alias</td>
<td class="text-muted">@command.Description</td>
<td class="@(command.RequiresTarget ? "text-success" : "text-danger")">@command.RequiresTarget</td>
<td class="text-muted">@ViewBag.CommandPrefix@command.Syntax.Split(ViewBag.CommandPrefix)[1]</td>
<td>
@if (command.SupportedGames is not null && command.SupportedGames.Any())
{
<div class="d-flex">
@foreach (var game in command.SupportedGames)
{
<div class="mr-5" data-toggle="tooltip" data-title="@loc[$"GAME_{game.ToString().ToUpper()}"]">
<div class="badge">@game.ToString()</div>
</div>
}
</div>
}
else
{
<div class="badge">@loc["WEBFRONT_HELP_ALL_GAMES"]</div>
}
</td>
<td class="text-right level-color-@((int)command.Permission)">@command.Permission.ToLocalizedLevelName()</td>
</tr>
}
</tbody>
</table>
<!-- mobile -->
<table class="table d-lg-none rounded no-cell-divider bg-dark-dm bg-light-lm" style="border-collapse: collapse">
@foreach (var command in commandList)
{
<tr>
<th class="bg-primary text-right text-light first-row" style="width:40%">@loc["WEBFRONT_HELP_COMMAND_DESC_NAME"]</th>
<td class="first-row font-weight-bold">@command.Name</td>
</tr>
<tr>
<th class="bg-primary text-right text-light">@loc["WEBFRONT_HELP_COMMAND_DESC_ALIAS"]</th>
<td>@command.Alias</td>
</tr>
<tr>
<th class="bg-primary text-right text-light align-top">@loc["WEBFRONT_HELP_COMMAND_DESC_DESCRIPTION"]</th>
<td class="text-muted">@command.Description</td>
</tr>
<tr>
<th class="bg-primary text-right text-light">@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRES_TARGET"]</th>
<td class="@(command.RequiresTarget ? "text-success" : "text-danger") align-top">@command.RequiresTarget</td>
</tr>
<tr>
<th class="bg-primary text-right text-light align-top">@loc["WEBFRONT_HELP_COMMAND_DESC_SYNTAX"]</th>
<td class="text-muted">@ViewBag.CommandPrefix@command.Syntax.Split(ViewBag.CommandPrefix)[1]</td>
</tr>
<tr>
<th class="bg-primary text-right text-light">@loc["WEBFRONT_HELP_SUPPORTED_GAMES"]</th>
<td>
@if (command.SupportedGames is not null && command.SupportedGames.Any())
{
<div class="d-flex">
@foreach (var game in command.SupportedGames)
{
<div class="mr-5" data-toggle="tooltip" data-title="@loc[$"GAME_{game.ToString().ToUpper()}"]">
<div class="badge">@game.ToString()</div>
</div>
}
</div>
}
else
{
<div class="badge">@loc["WEBFRONT_HELP_ALL_GAMES"]</div>
}
</td>
</tr>
<tr class="border-bottom">
<th class="bg-primary text-right text-light last-row">@loc["WEBFRONT_HELP_COMMAND_DESC_REQUIRED_LEVEL"]</th>
<td class="@($"level-color-{(int)command.Permission}") last-row">@command.Permission.ToLocalizedLevelName()</td>
</tr>
}
</table>
</div>
}
</div>

View File

@ -3,7 +3,8 @@
var loc = Utilities.CurrentLocalization.LocalizationIndex;
var canSeeLevel = (ViewBag.PermissionsSet as IEnumerable<string>).HasPermission(WebfrontEntity.ClientLevel, WebfrontPermission.Read) && Model.PunisherLevel != 0;
var punisherLevelClass = canSeeLevel ? $"level-color-{(int)Model.PunisherLevel}" : "text-light-dm text-dark-lm";
var punisherLevelClass = canSeeLevel ? $"level-color-{Model.PunisherLevel.ToString().ToLower()}" : "text-light-dm text-dark-lm";
var offenderLevelClass = canSeeLevel ? $"level-color-{Model.OffenderLevel.ToString().ToLower()}" : "text-light-dm text-dark-lm";
}
@using WebfrontCore.Permissions
@ -12,7 +13,7 @@
<!-- desktop -->
<tr class="d-none d-lg-table-row">
<td colspan="20%">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.OffenderId" class="link-inverse">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.OffenderId" class="@offenderLevelClass">
<color-code value="@Model.OffenderName"></color-code>
</a>
</td>
@ -41,14 +42,14 @@
<!-- mobile -->
<tr class="d-table-row d-lg-none d-flex border-bottom">
<td class="bg-primary text-light text-right">
<td class="bg-primary text-light text-right d-flex flex-column w-quarter">
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_OFFENSE"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</div>
<div class="mt-5 mb-5 mt-auto">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_TIME"]</div>
</td>
<td class="flex-fill">
<td class=" d-flex flex-column w-three-quarter">
<div class="mt-5 mb-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.OffenderId" >
<color-code value="@Model.OffenderName"></color-code>
@ -57,13 +58,13 @@
<div class="mt-5 mb-5 penalties-color-@Model.PenaltyTypeText.ToLower()">
@ViewBag.Localization[$"WEBFRONT_PENALTY_{Model.PenaltyType.ToString().ToUpper()}"]
</div>
<div class="mt-5 mb-5">
<div class="mt-5 mb-5 text-force-break">
<color-code value="@($"{Model.Offense}{(ViewBag.Authorized ? Model.AdditionalPenaltyInformation : "")}")"></color-code>
</div>
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.PunisherId" class="mt-5 mb-5 @((!ViewBag.Authorized && ViewBag.EnablePrivilegedUserPrivacy) || Model.PunisherLevel == 0 ? "text-dark-lm text-light-dm" : "level-color-" + (int)Model.PunisherLevel)">
<color-code value="@Model.PunisherName"></color-code>
</a>
<div class="mt-5 mb-5">
<div class="mt-5 mb-5 text-muted">
@if (Model.Expired)
{
<span>@Model.TimePunishedString</span>

View File

@ -59,7 +59,7 @@
{
var levelColorClass = !ViewBag.Authorized || client.client.LevelInt == 0 ? "text-light-dm text-dark-lm" : $"level-color-{client.client.LevelInt}";
<div class="d-flex @(clientIndex.index == 1 ? "justify-content-start flex-row-reverse" : "")">
<has-permission entity="AdminMenu" required-permission="Update">
<has-permission entity="AdminMenu" required-permission="Write">
<a href="#actionModal" class="profile-action" data-action="kick" data-action-id="@client.client.ClientId" aria-hidden="true">
<i class="oi oi-circle-x font-size-12 @levelColorClass"></i>
</a>

View File

@ -31,7 +31,7 @@
<a href="@Model.ConnectProtocolUrl" class="text-light align-self-center server-join-button" title="@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_HOME_JOIN_DESC"]">
<i class="oi oi-play-circle ml-5 mr-5"></i>
</a>
<has-permission entity="AdminMenu" required-permission="Update">
<has-permission entity="AdminMenu" required-permission="Write">
<!-- send message button -->
<a href="#actionModal" class="profile-action text-light align-self-center" data-action="chat" data-action-id="@Model.ID">
<i class="oi oi-chat ml-5 mr-5"></i>

View File

@ -8,7 +8,7 @@
@foreach (var item in Model.Items)
{
<a href="@(item.IsLink ? item.Reference : "#")" class="@(item.IsLink ? "" : "profile-action")" data-action="@(item.IsLink ? "" : item.Reference)" data-action-id="@item.EntityId">
<a href="@(item.IsLink ? item.Reference : "#")" class="@(item.IsLink ? "" : "profile-action")" data-action="@(item.IsLink ? "" : item.Reference)" data-action-id="@item.EntityId" data-action-meta="@item.Meta">
<div class="@(item.IsButton ? "btn btn-block" : "")" data-title="@item.Tooltip" data-placement="left" data-toggle="@(string.IsNullOrEmpty(item.Tooltip) ? "" : "tooltip")">
<i class="@(string.IsNullOrEmpty(item.Icon) ? "" : $"oi {item.Icon}") mr-5 font-size-12"></i>
<span class="@(item.IsActive ? "text-primary" : "") text-truncate">@item.Title</span>
@ -28,7 +28,7 @@
@foreach (var item in Model.Items)
{
<div class="mt-15 mb-15">
<a href="@(item.IsLink ? item.Reference : "#")" class="@(item.IsLink ? "" : "profile-action") no-decoration" data-action="@(item.IsLink ? "" : item.Reference)" data-action-id="@item.EntityId">
<a href="@(item.IsLink ? item.Reference : "#")" class="@(item.IsLink ? "" : "profile-action") no-decoration" data-action="@(item.IsLink ? "" : item.Reference)" data-action-id="@item.EntityId" data-action-meta="@item.Meta">
<div class="btn btn-block btn-lg @(item.IsActive ? "btn-primary" : "") text-truncate" data-title="@item.Tooltip" data-toggle="@(string.IsNullOrEmpty(item.Tooltip) ? "" : "tooltip")">
<i class="@(string.IsNullOrEmpty(item.Icon) ? "" : $"oi {item.Icon}") mr-5 font-size-12"></i>
<span>@item.Title</span>

View File

@ -46,9 +46,9 @@
<ItemGroup>
<PackageReference Include="BuildWebCompiler" Version="1.12.405" />
<PackageReference Include="FluentValidation.AspNetCore" Version="10.3.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.1" />
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.161" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.8" />
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" />
</ItemGroup>
<ItemGroup>

View File

@ -12,7 +12,7 @@
"outputFileName": "wwwroot/js/global.min.js",
"inputFiles": [
"wwwroot/lib/jquery/dist/jquery.js",
"wwwroot/lib/moment.js/moment-with-locales.min.js",
"wwwroot/lib/moment.js/min/moment-with-locales.min.js",
"wwwroot/lib/moment-timezone/moment-timezone.min.js",
"wwwroot/lib/chart.js/dist/Chart.bundle.min.js",
"wwwroot/lib/halfmoon/js/halfmoon.min.js",

View File

@ -84,10 +84,6 @@
border-bottom-color: var(--secondary-color);
}
#penalty_filter_selection {
border: none !important;
}
@-webkit-keyframes rotation {
from {
-webkit-transform: rotate(359deg);
@ -456,3 +452,22 @@ img.social-icon {
min-width: 1.2rem;
margin-right: 0.75rem;
}
table.no-cell-divider tr {
//border: none !important;
border-color: transparent !important;
}
table.no-cell-divider td, table.no-cell-divider th {
padding: 0.25rem 1.5rem;
font-weight: normal;
}
table.no-cell-divider td.first-row, table.no-cell-divider th.first-row {
padding-top: 1.5rem;
}
table.no-cell-divider td.last-row, table.no-cell-divider th.last-row {
padding-bottom: 1.5rem;
}

View File

@ -107,6 +107,10 @@
color: rgb(140, 154, 132);
}
.penalties-color-unmute {
color: #989f94;
}
.penalties-color-report {
color: #b3ae8f;
}
@ -121,6 +125,10 @@
color: rgba(254, 126, 76, 0.85);
}
.penalties-color-tempmute {
color: #df9476;
}
.penalties-bgcolor-tempban {
background-color: #fe7e4c;
background-color: rgba(254, 126, 76, 0.85);
@ -136,6 +144,10 @@
color: rgba(255, 69, 69, 0.85);
}
.penalties-color-mute {
color: #e77171;
}
#profile_aliases_btn {
cursor: pointer;
}

View File

@ -91,12 +91,18 @@ $(document).ready(function () {
$(document).off('click', '.profile-action');
$(document).on('click', '.profile-action', function (e) {
e.preventDefault();
const actionType = $(this).data('action');
const action = $(this).data('action');
const actionId = $(this).data('action-id');
const actionMeta = $(this).data('action-meta');
const responseDuration = $(this).data('response-duration') || 5000;
const actionIdKey = actionId === undefined ? '' : `?id=${actionId}`;
let actionKeys = actionId === undefined ? '' : `?id=${actionId}`;
if (actionMeta !== undefined) {
actionKeys = actionKeys + '&meta=' + JSON.stringify(actionMeta);
}
showLoader();
$.get(`/Action/${actionType}Form/${actionIdKey}`)
$.get(`/Action/${action}Form/${actionKeys}`)
.done(function (response) {
$('#actionModal .modal-message').fadeOut('fast')
$('#actionModal').attr('data-response-duration', responseDuration);

View File

@ -26,7 +26,7 @@ function setupPerformanceGraph() {
}
const chart = $('#client_performance_history');
const container = $('#client_performance_history_container');
chart.attr('height', summary.height());
chart.attr('height', summary.height() * 1.5);
chart.attr('width', container.width());
renderPerformanceChart();
}
@ -394,7 +394,7 @@ function renderPerformanceChart() {
position: 'right',
ticks: {
precision: 0,
stepSize: 3,
stepSize: max - min / 2,
callback: function (value, index, values) {
if (index === values.length - 1) {
return min;

View File

@ -88,7 +88,7 @@ function getStatsChart(id) {
position: 'right',
ticks: {
precision: 0,
stepSize: 3,
stepSize: max - min / 2,
callback: function (value, index, values) {
if (index === values.length - 1) {
return min;