From 48fa9ca0768f7b7300ce0d7a66227d1e9e5517c7 Mon Sep 17 00:00:00 2001 From: Michael Wilson Date: Mon, 19 Feb 2024 18:38:07 +1000 Subject: [PATCH] Command Overhaul (#330) --- managed/CounterStrikeSharp.API/Bootstrap.cs | 12 +- .../CompatibilitySuppressions.xml | 72 ++++++-- .../Core/Application.cs | 65 ++++--- .../CounterStrikeSharp.API/Core/BasePlugin.cs | 119 +++---------- .../Core/Commands/CommandDefinition.cs | 33 ++++ .../Core/Commands/CommandManager.cs | 158 ++++++++++++++++++ .../Core/Commands/ICommandManager.cs | 10 ++ .../Commands/PluginCommandManagerDecorator.cs | 46 +++++ .../CounterStrikeSharp.API/Core/CoreConfig.cs | 17 +- .../CounterStrikeSharp.API/Core/IPlugin.cs | 5 +- .../Core/Plugin/Host/PluginManager.cs | 8 +- .../Core/Plugin/PluginContext.cs | 33 +++- .../Modules/Admin/AdminCommandOverrides.cs | 2 +- .../Modules/Admin/AdminManager.cs | 35 ++-- .../Modules/Admin/BaseRequiresPermissions.cs | 4 +- .../Modules/Utils/StandaloneCommand.cs | 109 ------------ .../ServiceCollectionExtensions.cs | 31 ++++ .../TestPlugin/TestPluginServiceCollection.cs | 9 +- 18 files changed, 481 insertions(+), 287 deletions(-) create mode 100644 managed/CounterStrikeSharp.API/Core/Commands/CommandDefinition.cs create mode 100644 managed/CounterStrikeSharp.API/Core/Commands/CommandManager.cs create mode 100644 managed/CounterStrikeSharp.API/Core/Commands/ICommandManager.cs create mode 100644 managed/CounterStrikeSharp.API/Core/Commands/PluginCommandManagerDecorator.cs delete mode 100644 managed/CounterStrikeSharp.API/Modules/Utils/StandaloneCommand.cs create mode 100644 managed/CounterStrikeSharp.API/ServiceCollectionExtensions.cs diff --git a/managed/CounterStrikeSharp.API/Bootstrap.cs b/managed/CounterStrikeSharp.API/Bootstrap.cs index 1b5fb82e..17f3b1ec 100644 --- a/managed/CounterStrikeSharp.API/Bootstrap.cs +++ b/managed/CounterStrikeSharp.API/Bootstrap.cs @@ -1,14 +1,16 @@ -using System; +using System; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Commands; using CounterStrikeSharp.API.Core.Hosting; using CounterStrikeSharp.API.Core.Logging; using CounterStrikeSharp.API.Core.Plugin; using CounterStrikeSharp.API.Core.Plugin.Host; using CounterStrikeSharp.API.Core.Translations; +using CounterStrikeSharp.API.Modules.Admin; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -42,6 +44,7 @@ public static class Bootstrap services.AddSingleton(); services.AddSingleton(); services.AddScoped(); + services.AddSingleton(); services.Scan(i => i.FromCallingAssembly() .AddClasses(c => c.AssignableTo()) @@ -50,12 +53,13 @@ public static class Bootstrap }) .Build(); - using IServiceScope scope = host.Services.CreateScope(); + using IServiceScope rootScope = host.Services.CreateScope(); // TODO: Improve static singleton access - GameData.GameDataProvider = scope.ServiceProvider.GetRequiredService(); + GameData.GameDataProvider = rootScope.ServiceProvider.GetRequiredService(); + AdminManager.CommandManagerProvider = rootScope.ServiceProvider.GetRequiredService(); - var application = scope.ServiceProvider.GetRequiredService(); + var application = rootScope.ServiceProvider.GetRequiredService(); application.Start(); return 1; diff --git a/managed/CounterStrikeSharp.API/CompatibilitySuppressions.xml b/managed/CounterStrikeSharp.API/CompatibilitySuppressions.xml index 4f3acdab..315a5fa2 100644 --- a/managed/CounterStrikeSharp.API/CompatibilitySuppressions.xml +++ b/managed/CounterStrikeSharp.API/CompatibilitySuppressions.xml @@ -61,6 +61,12 @@ .\ApiCompat\v151.dll obj\Debug\net7.0\CounterStrikeSharp.API.dll + + CP0001 + T:CounterStrikeSharp.API.Modules.Utils.CommandUtils + .\ApiCompat\v151.dll + obj\Debug\net7.0\CounterStrikeSharp.API.dll + CP0002 F:CounterStrikeSharp.API.Core.AnimParamType_t.ANIMPARAM_STRINGTOKEN @@ -169,6 +175,24 @@ .\ApiCompat\v151.dll obj\Debug\net7.0\CounterStrikeSharp.API.dll + + CP0002 + F:CounterStrikeSharp.API.Modules.Memory.VirtualFunctions.CCSPlayerPawn_Respawn + .\ApiCompat\v151.dll + obj\Debug\net7.0\CounterStrikeSharp.API.dll + + + CP0002 + F:CounterStrikeSharp.API.Modules.Memory.VirtualFunctions.CCSPlayerPawn_RespawnFunc + .\ApiCompat\v151.dll + obj\Debug\net7.0\CounterStrikeSharp.API.dll + + + CP0002 + M:CounterStrikeSharp.API.Core.Application.#ctor(Microsoft.Extensions.Logging.ILoggerFactory,CounterStrikeSharp.API.Core.Hosting.IScriptHostConfiguration,CounterStrikeSharp.API.Core.GameDataProvider,CounterStrikeSharp.API.Core.CoreConfig,CounterStrikeSharp.API.Core.Plugin.Host.IPluginManager,CounterStrikeSharp.API.Core.Plugin.Host.IPluginContextQueryHandler,CounterStrikeSharp.API.Core.Translations.IPlayerLanguageManager) + .\ApiCompat\v151.dll + obj\Debug\net7.0\CounterStrikeSharp.API.dll + CP0002 M:CounterStrikeSharp.API.Core.CBaseAnimGraph.get_AnimGraphDirty @@ -277,6 +301,12 @@ .\ApiCompat\v151.dll obj\Debug\net7.0\CounterStrikeSharp.API.dll + + CP0002 + M:CounterStrikeSharp.API.Core.CCSPlayerPawn.Respawn + .\ApiCompat\v151.dll + obj\Debug\net7.0\CounterStrikeSharp.API.dll + CP0002 M:CounterStrikeSharp.API.Core.CDynamicProp.get_AnimateOnServer @@ -313,6 +343,12 @@ .\ApiCompat\v151.dll obj\Debug\net7.0\CounterStrikeSharp.API.dll + + CP0002 + M:CounterStrikeSharp.API.Core.CoreConfig.#ctor(CounterStrikeSharp.API.Core.Hosting.IScriptHostConfiguration,Microsoft.Extensions.Logging.ILogger{CounterStrikeSharp.API.Core.CoreConfig}) + .\ApiCompat\v151.dll + obj\Debug\net7.0\CounterStrikeSharp.API.dll + CP0002 M:CounterStrikeSharp.API.Core.CPlayer_WeaponServices.get_AllowSwitchToNoWeapon @@ -391,6 +427,24 @@ .\ApiCompat\v151.dll obj\Debug\net7.0\CounterStrikeSharp.API.dll + + CP0002 + M:CounterStrikeSharp.API.Core.Plugin.Host.PluginManager.#ctor(CounterStrikeSharp.API.Core.Hosting.IScriptHostConfiguration,Microsoft.Extensions.Logging.ILogger{CounterStrikeSharp.API.Core.Plugin.Host.PluginManager},System.IServiceProvider) + .\ApiCompat\v151.dll + obj\Debug\net7.0\CounterStrikeSharp.API.dll + + + CP0002 + M:CounterStrikeSharp.API.Core.Plugin.PluginContext.#ctor(System.IServiceProvider,CounterStrikeSharp.API.Core.Hosting.IScriptHostConfiguration,System.String,System.Int32) + .\ApiCompat\v151.dll + obj\Debug\net7.0\CounterStrikeSharp.API.dll + + + CP0006 + P:CounterStrikeSharp.API.Core.IPlugin.CommandManager + .\ApiCompat\v151.dll + obj\Debug\net7.0\CounterStrikeSharp.API.dll + CP0010 T:CounterStrikeSharp.API.Core.RenderMultisampleType_t @@ -985,22 +1039,4 @@ .\ApiCompat\v151.dll obj\Debug\net7.0\CounterStrikeSharp.API.dll - - CP0002 - F:CounterStrikeSharp.API.Modules.Memory.VirtualFunctions.CCSPlayerPawn_Respawn - .\ApiCompat\v151.dll - obj\Debug\net7.0\CounterStrikeSharp.API.dll - - - CP0002 - F:CounterStrikeSharp.API.Modules.Memory.VirtualFunctions.CCSPlayerPawn_RespawnFunc - .\ApiCompat\v151.dll - obj\Debug\net7.0\CounterStrikeSharp.API.dll - - - CP0002 - M:CounterStrikeSharp.API.Core.CCSPlayerPawn.Respawn - .\ApiCompat\v151.dll - obj\Debug\net7.0\CounterStrikeSharp.API.dll - \ No newline at end of file diff --git a/managed/CounterStrikeSharp.API/Core/Application.cs b/managed/CounterStrikeSharp.API/Core/Application.cs index c4f3684f..69e00e47 100644 --- a/managed/CounterStrikeSharp.API/Core/Application.cs +++ b/managed/CounterStrikeSharp.API/Core/Application.cs @@ -17,6 +17,7 @@ using System.Globalization; using System.Linq; using System.Text; +using CounterStrikeSharp.API.Core.Commands; using CounterStrikeSharp.API.Core.Hosting; using CounterStrikeSharp.API.Core.Plugin; using CounterStrikeSharp.API.Core.Plugin.Host; @@ -26,6 +27,7 @@ using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.Modules.Menu; using CounterStrikeSharp.API.Modules.Utils; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace CounterStrikeSharp.API.Core @@ -34,6 +36,7 @@ namespace CounterStrikeSharp.API.Core { private static Application _instance = null!; public ILogger Logger { get; } + public static Application Instance => _instance!; public static string RootDirectory => Instance._scriptHostConfiguration.RootPath; @@ -44,10 +47,12 @@ namespace CounterStrikeSharp.API.Core private readonly IPluginManager _pluginManager; private readonly IPluginContextQueryHandler _pluginContextQueryHandler; private readonly IPlayerLanguageManager _playerLanguageManager; + private readonly ICommandManager _commandManager; public Application(ILoggerFactory loggerFactory, IScriptHostConfiguration scriptHostConfiguration, GameDataProvider gameDataProvider, CoreConfig coreConfig, IPluginManager pluginManager, - IPluginContextQueryHandler pluginContextQueryHandler, IPlayerLanguageManager playerLanguageManager) + IPluginContextQueryHandler pluginContextQueryHandler, IPlayerLanguageManager playerLanguageManager, + ICommandManager commandManager) { Logger = loggerFactory.CreateLogger("Core"); _scriptHostConfiguration = scriptHostConfiguration; @@ -56,6 +61,7 @@ namespace CounterStrikeSharp.API.Core _pluginManager = pluginManager; _pluginContextQueryHandler = pluginContextQueryHandler; _playerLanguageManager = playerLanguageManager; + _commandManager = commandManager; _instance = this; } @@ -69,7 +75,7 @@ namespace CounterStrikeSharp.API.Core var adminPath = Path.Combine(_scriptHostConfiguration.RootPath, "configs", "admins.json"); Logger.LogInformation("Loading Admins from {Path}", adminPath); AdminManager.LoadAdminData(adminPath); - + var adminGroupsPath = Path.Combine(_scriptHostConfiguration.RootPath, "configs", "admin_groups.json"); Logger.LogInformation("Loading Admin Groups from {Path}", adminGroupsPath); AdminManager.LoadAdminGroups(adminGroupsPath); @@ -81,24 +87,23 @@ namespace CounterStrikeSharp.API.Core AdminManager.MergeGroupPermsIntoAdmins(); AdminManager.AddCommands(); + RegisterPluginCommands(); + _pluginManager.Load(); for (var i = 1; i <= 9; i++) { - CommandUtils.AddStandaloneCommand($"css_{i}", "Command Key Handler", (player, info) => - { - if (player == null) return; - var key = Convert.ToInt32(info.GetArg(0).Split("_")[1]); - - MenuManager.OnKeyPress(player, key); - }); + _commandManager.RegisterCommand(new($"css_{i}", "Command Key Handler", + (player, info) => + { + if (player == null) return; + var key = Convert.ToInt32(info.GetArg(0).Split("_")[1]); + MenuManager.OnKeyPress(player, key); + })); } - - RegisterPluginCommands(); } [RequiresPermissions("@css/generic")] - [CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] private void OnCSSCommand(CCSPlayerController? caller, CommandInfo info) { var currentVersion = Api.GetVersion(); @@ -112,13 +117,6 @@ namespace CounterStrikeSharp.API.Core } [RequiresPermissions("@css/generic")] - [CommandHelper(minArgs: 1, - usage: "[option]\n" + - " list - List all plugins currently loaded.\n" + - " start / load - Loads a plugin not currently loaded.\n" + - " stop / unload - Unloads a plugin currently loaded.\n" + - " restart / reload - Reloads a plugin currently loaded.", - whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] private void OnCSSPluginCommand(CCSPlayerController? caller, CommandInfo info) { switch (info.GetArg(1)) @@ -253,17 +251,19 @@ namespace CounterStrikeSharp.API.Core } } - [CommandHelper(usage: "[language code, e.g. \"de\", \"pl\", \"en\"]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] + [CommandHelper(usage: "[language code, e.g. \"de\", \"pl\", \"en\"]", + whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] private void OnLangCommand(CCSPlayerController? player, CommandInfo command) { if (player == null) return; - + SteamID steamId = (SteamID)player.SteamID; if (command.ArgCount == 1) { var language = _playerLanguageManager.GetLanguage(steamId); - command.ReplyToCommand(string.Format("Current language is \"{0}\" ({1})", language.Name, language.NativeName)); + command.ReplyToCommand(string.Format("Current language is \"{0}\" ({1})", language.Name, + language.NativeName)); return; } @@ -271,7 +271,7 @@ namespace CounterStrikeSharp.API.Core { return; } - + try { var language = command.GetArg(1); @@ -287,10 +287,21 @@ namespace CounterStrikeSharp.API.Core private void RegisterPluginCommands() { - CommandUtils.AddStandaloneCommand("css", "Counter-Strike Sharp options.", OnCSSCommand); - CommandUtils.AddStandaloneCommand("css_plugins", "Counter-Strike Sharp plugin options.", - OnCSSPluginCommand); - CommandUtils.AddStandaloneCommand("css_lang", "Set Counter-Strike Sharp language.", OnLangCommand); + _commandManager.RegisterCommand(new("css", "Counter-Strike Sharp options.", OnCSSCommand) + { + ExecutableBy = CommandUsage.CLIENT_AND_SERVER, + }); + _commandManager.RegisterCommand(new("css_plugins", "Counter-Strike Sharp plugin options.", + OnCSSPluginCommand) + { + ExecutableBy = CommandUsage.CLIENT_AND_SERVER, + MinArgs = 1, + UsageHint = "[option]\n" + + " list - List all plugins currently loaded.\n" + + " start / load - Loads a plugin not currently loaded.\n" + + " stop / unload - Unloads a plugin currently loaded.\n" + + " restart / reload - Reloads a plugin currently loaded.", + }); } } } \ No newline at end of file diff --git a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs index 89405c05..813e90fe 100644 --- a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs +++ b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs @@ -22,6 +22,7 @@ using System.Reflection; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Core.Translations; +using CounterStrikeSharp.API.Core.Commands; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Events; @@ -59,6 +60,8 @@ namespace CounterStrikeSharp.API.Core public string ModuleDirectory => Path.GetDirectoryName(ModulePath); public ILogger Logger { get; set; } + + public ICommandManager CommandManager { get; set; } public IStringLocalizer Localizer { get; set; } @@ -170,102 +173,13 @@ namespace CounterStrikeSharp.API.Core /// The callback function to be invoked when the command is executed. public void AddCommand(string name, string description, CommandInfo.CommandCallback handler) { - var wrappedHandler = new Action((i, ptr) => - { - var caller = (i != -1) ? new CCSPlayerController(NativeAPI.GetEntityFromIndex(i + 1)) : null; - var command = new CommandInfo(ptr, caller); - - using var temporaryCulture = new WithTemporaryCulture(caller.GetLanguage()); - - var methodInfo = handler?.GetMethodInfo(); - - // We do not need to do permission checks on commands executed from the server console. - // The server will always be allowed to execute commands (unless marked as client only like above) - if (caller != null) - { - // Do not execute command if we do not have the correct permissions. - var adminData = AdminManager.GetPlayerAdminData(caller!.AuthorizedSteamID); - var permissionsToCheck = new List(); - - - // If our command is overriden, we dynamically create a new permissions attribute - // based on the data that is stored in admin_overrides.json. - if (AdminManager.CommandIsOverriden(name)) - { - var data = AdminManager.GetCommandOverrideData(name); - if (data != null) - { - var attrType = (data.CheckType == "all") ? typeof(RequiresPermissions) : typeof(RequiresPermissionsOr); - var attr = (BaseRequiresPermissions)Activator.CreateInstance(attrType, args: AdminManager.GetPermissionOverrides(name)); - - if (attr != null) permissionsToCheck.Add(attr); - } - } - // The permissions for this command are not being overriden here, so we - // grab the permissions to check straight from the attribute. - else - { - var permissions = methodInfo?.GetCustomAttributes(); - if (permissions != null) permissionsToCheck.AddRange(permissions); - } - - foreach (var attr in permissionsToCheck) - { - attr.Command = name; - if (!attr.CanExecuteCommand(caller)) - { - var responseStr = (attr.GetType() == typeof(RequiresPermissions)) ? - "You are missing the correct permissions" : "You do not have one of the correct permissions"; - - var flags = attr.Permissions.Except(adminData?.GetAllFlags() ?? new HashSet()); - flags = flags.Except(adminData?.Groups ?? new HashSet()); - command.ReplyToCommand($"[CSS] {responseStr} ({string.Join(", ", flags)}) to execute this command."); - - return; - } - } - } - - // Do not execute if we shouldn't be calling this command. - var helperAttribute = methodInfo?.GetCustomAttribute(); - if (helperAttribute != null) - { - switch (helperAttribute.WhoCanExcecute) - { - case CommandUsage.CLIENT_AND_SERVER: break; // Allow command through. - case CommandUsage.CLIENT_ONLY: - if (caller == null || !caller.IsValid) { command.ReplyToCommand("[CSS] This command can only be executed by clients."); return; } - break; - case CommandUsage.SERVER_ONLY: - if (caller != null && caller.IsValid) { command.ReplyToCommand("[CSS] This command can only be executed by the server."); return; } - break; - default: throw new ArgumentException("Unrecognised CommandUsage value passed in CommandHelperAttribute."); - } - - // Technically the command itself counts as the first argument, - // but we'll just ignore that for this check. - if (helperAttribute.MinArgs != 0 && command.ArgCount - 1 < helperAttribute.MinArgs) - { - // Remove the "css_" from the beginning of the command name if it's present. - // Most of the time, users will be calling commands from chat. - var commandCalled = command.ArgByIndex(0); - var properCommandName = (commandCalled.StartsWith("css_")) ? commandCalled.Replace("css_", "") : commandCalled; - - command.ReplyToCommand($"[CSS] Expected usage: \"!{properCommandName} {helperAttribute.Usage}\"."); - return; - } - } - - handler?.Invoke(caller, command); - }); - - var methodInfo = handler?.GetMethodInfo(); - var helperAttribute = methodInfo?.GetCustomAttribute(); - - var subscriber = new CallbackSubscriber(handler, wrappedHandler, () => { RemoveCommand(name, handler); }); - NativeAPI.AddCommand(name, description, (helperAttribute?.WhoCanExcecute == CommandUsage.SERVER_ONLY), - (int)ConCommandFlags.FCVAR_LINKED_CONCOMMAND, subscriber.GetInputArgument()); - CommandHandlers[handler] = subscriber; + var definition = new CommandDefinition(name, description, handler); + CommandManager.RegisterCommand(definition); + } + + private void AddCommand(CommandDefinition definition) + { + CommandManager.RegisterCommand(definition); } public void AddCommandListener(string? name, CommandInfo.CommandListenerCallback handler, HookMode mode = HookMode.Pre) @@ -462,10 +376,19 @@ namespace CounterStrikeSharp.API.Core foreach (var eventHandler in eventHandlers) { var attributes = eventHandler.GetCustomAttributes(); + var helperAttribute = eventHandler.GetCustomAttribute(); foreach (var commandInfo in attributes) { - AddCommand(commandInfo.Command, commandInfo.Description, - eventHandler.CreateDelegate(instance)); + var definition = new CommandDefinition() + { + Name = commandInfo.Command, + Description = commandInfo.Description, + Callback = eventHandler.CreateDelegate(instance), + MinArgs = helperAttribute?.MinArgs, + UsageHint = helperAttribute?.Usage, + ExecutableBy = helperAttribute?.WhoCanExcecute ?? CommandUsage.CLIENT_AND_SERVER, + }; + AddCommand(definition); } } } diff --git a/managed/CounterStrikeSharp.API/Core/Commands/CommandDefinition.cs b/managed/CounterStrikeSharp.API/Core/Commands/CommandDefinition.cs new file mode 100644 index 00000000..ed487686 --- /dev/null +++ b/managed/CounterStrikeSharp.API/Core/Commands/CommandDefinition.cs @@ -0,0 +1,33 @@ +using CounterStrikeSharp.API.Modules.Commands; + +namespace CounterStrikeSharp.API.Core.Commands; + +public class CommandDefinition +{ + public CommandDefinition(string name, string description, CommandInfo.CommandCallback callback) + { + Name = name; + Description = description; + Callback = callback; + } + + public CommandDefinition() + { + } + + public string Name { get; init; } + public string Description { get; init; } + public CommandInfo.CommandCallback Callback { get; init; } + + public CommandUsage ExecutableBy { get; init; } = CommandUsage.CLIENT_AND_SERVER; + + public string? UsageHint { get; init; } + + public int? MinArgs { get; init; } + + public override string ToString() + { + return $"Name: {Name}, Description: {Description}, ExecutableBy: {ExecutableBy}, " + + $"UsageHint: {UsageHint}, MinArgs: {MinArgs}"; + } +} \ No newline at end of file diff --git a/managed/CounterStrikeSharp.API/Core/Commands/CommandManager.cs b/managed/CounterStrikeSharp.API/Core/Commands/CommandManager.cs new file mode 100644 index 00000000..bcd97e3a --- /dev/null +++ b/managed/CounterStrikeSharp.API/Core/Commands/CommandManager.cs @@ -0,0 +1,158 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using CounterStrikeSharp.API.Core.Translations; +using CounterStrikeSharp.API.Modules.Admin; +using CounterStrikeSharp.API.Modules.Commands; +using Microsoft.Extensions.Logging; + +namespace CounterStrikeSharp.API.Core.Commands; + +public class CommandManager : ICommandManager +{ + private readonly Dictionary> _commandDefinitions = + new(StringComparer.InvariantCultureIgnoreCase); + + private readonly ILogger _logger; + private readonly FunctionReference _internalFunctionReference; + + public CommandManager(ILogger logger) + { + _logger = logger; + _internalFunctionReference = FunctionReference.Create(HandleCommandInternal); + } + + public void RegisterCommand(CommandDefinition definition) + { + bool isRegistered = true; + if (!_commandDefinitions.ContainsKey(definition.Name)) + { + _commandDefinitions.Add(definition.Name, new List()); + isRegistered = false; + } + + _commandDefinitions[definition.Name].Add(definition); + + _logger.LogDebug("Registering command {Command}", definition.Name); + + if (!isRegistered) + { + NativeAPI.AddCommand(definition.Name, definition.Description, + definition.ExecutableBy == CommandUsage.SERVER_ONLY, + (int)ConCommandFlags.FCVAR_LINKED_CONCOMMAND, _internalFunctionReference); + } + } + + public void RemoveCommand(CommandDefinition definition) + { + _logger.LogDebug("Removing command {Command}", definition.Name); + + if (_commandDefinitions.TryGetValue(definition.Name, out var commandDefinition)) + { + commandDefinition.Remove(definition); + } + + if (_commandDefinitions[definition.Name].Count == 0) + { + NativeAPI.RemoveCommand(definition.Name, _internalFunctionReference); + _commandDefinitions.Remove(definition.Name); + } + } + + private void HandleCommandInternal(int playerSlot, IntPtr commandInfo) + { + var caller = (playerSlot != -1) ? Utilities.GetPlayerFromSlot(playerSlot) : null; + var info = new CommandInfo(commandInfo, caller); + + var name = info.GetArg(0).ToLower(); + + using var temporaryCulture = new WithTemporaryCulture(caller.GetLanguage()); + + if (_commandDefinitions.TryGetValue(name, out var handler)) + { + foreach (var command in handler) + { + var methodInfo = command.Callback?.GetMethodInfo(); + + // We do not need to do permission checks on commands executed from the server console. + // The server will always be allowed to execute commands (unless marked as client only like above) + if (caller != null) + { + // Do not execute command if we do not have the correct permissions. + var adminData = AdminManager.GetPlayerAdminData(caller!.AuthorizedSteamID); + var permissionsToCheck = new List(); + + + // If our command is overriden, we dynamically create a new permissions attribute + // based on the data that is stored in admin_overrides.json. + if (AdminManager.CommandIsOverriden(name)) + { + var data = AdminManager.GetCommandOverrideData(name); + if (data != null) + { + var attrType = (data.CheckType == "all") ? typeof(RequiresPermissions) : typeof(RequiresPermissionsOr); + var attr = (BaseRequiresPermissions)Activator.CreateInstance(attrType, args: AdminManager.GetPermissionOverrides(name)); + + if (attr != null) permissionsToCheck.Add(attr); + } + } + // The permissions for this command are not being overriden here, so we + // grab the permissions to check straight from the attribute. + else + { + var permissions = methodInfo?.GetCustomAttributes(); + if (permissions != null) permissionsToCheck.AddRange(permissions); + } + + foreach (var attr in permissionsToCheck) + { + attr.Command = name; + if (!attr.CanExecuteCommand(caller)) + { + var responseStr = (attr.GetType() == typeof(RequiresPermissions)) ? + "You are missing the correct permissions" : "You do not have one of the correct permissions"; + + var flags = attr.Permissions.Except(adminData?.GetAllFlags() ?? new HashSet()); + flags = flags.Except(adminData?.Groups ?? new HashSet()); + info.ReplyToCommand($"[CSS] {responseStr} ({string.Join(", ", flags)}) to execute this command."); + + return; + } + } + } + + // Do not execute if we shouldn't be calling this command. + var helperAttribute = methodInfo?.GetCustomAttribute(); + if (helperAttribute != null) + { + switch (helperAttribute.WhoCanExcecute) + { + case CommandUsage.CLIENT_AND_SERVER: break; // Allow command through. + case CommandUsage.CLIENT_ONLY: + if (caller == null || !caller.IsValid) { info.ReplyToCommand("[CSS] This command can only be executed by clients."); return; } + break; + case CommandUsage.SERVER_ONLY: + if (caller != null && caller.IsValid) { info.ReplyToCommand("[CSS] This command can only be executed by the server."); return; } + break; + default: throw new ArgumentException("Unrecognised CommandUsage value passed in CommandHelperAttribute."); + } + + // Technically the command itself counts as the first argument, + // but we'll just ignore that for this check. + if (helperAttribute.MinArgs != 0 && info.ArgCount - 1 < helperAttribute.MinArgs) + { + // Remove the "css_" from the beginning of the command name if it's present. + // Most of the time, users will be calling commands from chat. + var commandCalled = info.ArgByIndex(0); + var properCommandName = (commandCalled.StartsWith("css_")) ? commandCalled.Replace("css_", "") : commandCalled; + + info.ReplyToCommand($"[CSS] Expected usage: \"!{properCommandName} {helperAttribute.Usage}\"."); + return; + } + } + + command.Callback?.Invoke(caller, info); + } + } + } +} \ No newline at end of file diff --git a/managed/CounterStrikeSharp.API/Core/Commands/ICommandManager.cs b/managed/CounterStrikeSharp.API/Core/Commands/ICommandManager.cs new file mode 100644 index 00000000..229f1774 --- /dev/null +++ b/managed/CounterStrikeSharp.API/Core/Commands/ICommandManager.cs @@ -0,0 +1,10 @@ +using CounterStrikeSharp.API.Modules.Commands; + +namespace CounterStrikeSharp.API.Core.Commands; + +public interface ICommandManager +{ + void RegisterCommand(CommandDefinition definition); + + void RemoveCommand(CommandDefinition definition); +} \ No newline at end of file diff --git a/managed/CounterStrikeSharp.API/Core/Commands/PluginCommandManagerDecorator.cs b/managed/CounterStrikeSharp.API/Core/Commands/PluginCommandManagerDecorator.cs new file mode 100644 index 00000000..b4f58c41 --- /dev/null +++ b/managed/CounterStrikeSharp.API/Core/Commands/PluginCommandManagerDecorator.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using CounterStrikeSharp.API.Core.Plugin; +using Microsoft.Extensions.Logging; + +namespace CounterStrikeSharp.API.Core.Commands; + +/// +/// Decorator for that tracks registered commands and removes them when disposed. +/// Used for plugins that register commands to ensure they are removed when the plugin is unloaded. +/// +public class PluginCommandManagerDecorator : ICommandManager, IDisposable +{ + private readonly ICommandManager _inner; + private readonly List _trackedCommands = new(); + private readonly IPluginContext _pluginContext; + private readonly ILogger _logger; + + public PluginCommandManagerDecorator(ICommandManager inner, IPluginContext pluginContext, ILogger logger) + { + _pluginContext = pluginContext; + _logger = logger; + _inner = inner; + } + + public void RegisterCommand(CommandDefinition definition) + { + _inner.RegisterCommand(definition); + _trackedCommands.Add(definition); + _logger.LogDebug("Registered command {Command} from plugin {Plugin}", definition.Name, _pluginContext.Plugin.ModuleName); + } + + public void RemoveCommand(CommandDefinition definition) + { + _inner.RemoveCommand(definition); + _trackedCommands.Remove(definition); + _logger.LogDebug("Removed command {Command} from plugin {Plugin}", definition.Name, _pluginContext.Plugin.ModuleName); + } + + public void Dispose() + { + for (int i = _trackedCommands.Count - 1; i >= 0; i--) + { + RemoveCommand(_trackedCommands[i]); + } + } +} \ No newline at end of file diff --git a/managed/CounterStrikeSharp.API/Core/CoreConfig.cs b/managed/CounterStrikeSharp.API/Core/CoreConfig.cs index 710c0e20..bdc0623a 100644 --- a/managed/CounterStrikeSharp.API/Core/CoreConfig.cs +++ b/managed/CounterStrikeSharp.API/Core/CoreConfig.cs @@ -25,6 +25,7 @@ using CounterStrikeSharp.API.Modules.Commands; using System.Collections.Generic; using System.Globalization; using System.Linq; +using CounterStrikeSharp.API.Core.Commands; using CounterStrikeSharp.API.Core.Hosting; using CounterStrikeSharp.API.Core.Logging; using Microsoft.Extensions.Hosting; @@ -101,12 +102,15 @@ namespace CounterStrikeSharp.API.Core { private static CoreConfigData _coreConfig = new CoreConfigData(); + private readonly ICommandManager _commandManager; private readonly ILogger _logger; private readonly string _coreConfigPath; + private bool _commandsRegistered = false; - public CoreConfig(IScriptHostConfiguration scriptHostConfiguration, ILogger logger) + public CoreConfig(IScriptHostConfiguration scriptHostConfiguration, ICommandManager commandManager, ILogger logger) { + _commandManager = commandManager; _logger = logger; _coreConfigPath = Path.Join(scriptHostConfiguration.ConfigsPath, "core.json"); } @@ -120,9 +124,14 @@ namespace CounterStrikeSharp.API.Core public void Load() { - CommandUtils.AddStandaloneCommand("css_core_reload", "Reloads the core configuration file.", - ReloadCoreConfigCommand); - + if (!_commandsRegistered) + { + _commandManager.RegisterCommand(new CommandDefinition("css_core_reload", + "Reloads the core configuration file.", + ReloadCoreConfigCommand)); + _commandsRegistered = true; + } + if (!File.Exists(_coreConfigPath)) { _logger.LogWarning( diff --git a/managed/CounterStrikeSharp.API/Core/IPlugin.cs b/managed/CounterStrikeSharp.API/Core/IPlugin.cs index df2b9bb5..e84dda3a 100644 --- a/managed/CounterStrikeSharp.API/Core/IPlugin.cs +++ b/managed/CounterStrikeSharp.API/Core/IPlugin.cs @@ -1,4 +1,4 @@ -/* +/* * This file is part of CounterStrikeSharp. * CounterStrikeSharp is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,6 +15,7 @@ */ using System; +using CounterStrikeSharp.API.Core.Commands; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; @@ -57,6 +58,8 @@ namespace CounterStrikeSharp.API.Core ILogger Logger { get; set; } IStringLocalizer Localizer { get; set; } + + ICommandManager CommandManager { get; set; } void RegisterAllAttributes(object instance); diff --git a/managed/CounterStrikeSharp.API/Core/Plugin/Host/PluginManager.cs b/managed/CounterStrikeSharp.API/Core/Plugin/Host/PluginManager.cs index b617bf1e..ad6f921d 100644 --- a/managed/CounterStrikeSharp.API/Core/Plugin/Host/PluginManager.cs +++ b/managed/CounterStrikeSharp.API/Core/Plugin/Host/PluginManager.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Linq; +using CounterStrikeSharp.API.Core.Commands; using CounterStrikeSharp.API.Core.Hosting; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace CounterStrikeSharp.API.Core.Plugin.Host; @@ -9,12 +11,14 @@ public class PluginManager : IPluginManager { private readonly HashSet _loadedPluginContexts = new(); private readonly IScriptHostConfiguration _scriptHostConfiguration; + private readonly ICommandManager _commandManager; private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - public PluginManager(IScriptHostConfiguration scriptHostConfiguration, ILogger logger, IServiceProvider serviceProvider) + public PluginManager(IScriptHostConfiguration scriptHostConfiguration, ICommandManager commandManager, ILogger logger, IServiceProvider serviceProvider, IServiceScopeFactory serviceScopeFactory) { _scriptHostConfiguration = scriptHostConfiguration; + _commandManager = commandManager; _logger = logger; _serviceProvider = serviceProvider; } @@ -47,7 +51,7 @@ public class PluginManager : IPluginManager public void LoadPlugin(string path) { - var plugin = new PluginContext(_serviceProvider, _scriptHostConfiguration, path, _loadedPluginContexts.Select(x => x.PluginId).DefaultIfEmpty(0).Max() + 1); + var plugin = new PluginContext(_serviceProvider, _commandManager, _scriptHostConfiguration, path, _loadedPluginContexts.Select(x => x.PluginId).DefaultIfEmpty(0).Max() + 1); _loadedPluginContexts.Add(plugin); plugin.Load(); } diff --git a/managed/CounterStrikeSharp.API/Core/Plugin/PluginContext.cs b/managed/CounterStrikeSharp.API/Core/Plugin/PluginContext.cs index 1b3e01c1..f6a66b0c 100644 --- a/managed/CounterStrikeSharp.API/Core/Plugin/PluginContext.cs +++ b/managed/CounterStrikeSharp.API/Core/Plugin/PluginContext.cs @@ -18,9 +18,11 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using CounterStrikeSharp.API.Core.Attributes; +using CounterStrikeSharp.API.Core.Commands; using CounterStrikeSharp.API.Core.Hosting; using CounterStrikeSharp.API.Core.Logging; using CounterStrikeSharp.API.Core.Translations; +using CounterStrikeSharp.API.Core.Plugin.Host; using McMaster.NETCore.Plugins; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -38,23 +40,27 @@ namespace CounterStrikeSharp.API.Core.Plugin private PluginLoader Loader { get; set; } - private IServiceProvider ServiceProvider { get; set; } + private ServiceProvider ServiceProvider { get; set; } public int PluginId { get; } + private readonly ICommandManager _commandManager; private readonly IScriptHostConfiguration _hostConfiguration; private readonly string _path; private readonly FileSystemWatcher _fileWatcher; private readonly IServiceProvider _applicationServiceProvider; public string FilePath => _path; + private IServiceScope _serviceScope; // TOOD: ServiceCollection private ILogger _logger = CoreLogging.Factory.CreateLogger(); - public PluginContext(IServiceProvider applicationServiceProvider, IScriptHostConfiguration hostConfiguration, string path, int id) + public PluginContext(IServiceProvider applicationServiceProvider, ICommandManager commandManager, + IScriptHostConfiguration hostConfiguration, + string path, int id) { - _applicationServiceProvider = applicationServiceProvider; + _commandManager = commandManager; _hostConfiguration = hostConfiguration; _path = path; PluginId = id; @@ -62,11 +68,13 @@ namespace CounterStrikeSharp.API.Core.Plugin Loader = PluginLoader.CreateFromAssemblyFile(path, new[] { - typeof(IPlugin), typeof(ILogger), typeof(IServiceCollection), typeof(IPluginServiceCollection<>) + typeof(IPlugin), typeof(ILogger), typeof(IServiceCollection), typeof(IPluginServiceCollection<>), + typeof(ICommandManager) }, config => { config.EnableHotReload = true; config.IsUnloadable = true; + config.PreferSharedTypes = true; }); if (CoreConfig.PluginHotReloadEnabled) @@ -168,12 +176,14 @@ namespace CounterStrikeSharp.API.Core.Plugin method?.Invoke(pluginServiceCollection, new object[] { serviceCollection }); } } + + serviceCollection.AddScoped(c => _commandManager); + serviceCollection.DecorateSingleton(); serviceCollection.AddSingleton(this); serviceCollection.TryAddSingleton(); serviceCollection.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>)); serviceCollection.TryAddTransient(typeof(IStringLocalizer), typeof(StringLocalizer)); - ServiceProvider = serviceCollection.BuildServiceProvider(); var minimumApiVersion = pluginType.GetCustomAttribute()?.Version; @@ -186,13 +196,18 @@ namespace CounterStrikeSharp.API.Core.Plugin _logger.LogInformation("Loading plugin {Name}", pluginType.Assembly.GetName().Name); - Plugin = ServiceProvider.GetRequiredService(pluginType) as IPlugin; - + _serviceScope = ServiceProvider.CreateScope(); + + Plugin = _serviceScope.ServiceProvider.GetRequiredService(pluginType) as IPlugin; + if (Plugin == null) throw new Exception("Unable to create plugin instance"); - + State = PluginState.Loading; Plugin.ModulePath = _path; + Plugin.Logger = _serviceScope.ServiceProvider.GetRequiredService() + .CreateLogger(pluginType); + Plugin.CommandManager = _serviceScope.ServiceProvider.GetRequiredService(); Plugin.RegisterAllAttributes(Plugin); Plugin.Localizer = ServiceProvider.GetRequiredService(); Plugin.Logger = ServiceProvider.GetRequiredService().CreateLogger(pluginType); @@ -206,6 +221,7 @@ namespace CounterStrikeSharp.API.Core.Plugin } } + public void Unload(bool hotReload = false) { if (State == PluginState.Unloaded) return; @@ -218,6 +234,7 @@ namespace CounterStrikeSharp.API.Core.Plugin Plugin.Unload(hotReload); Plugin.Dispose(); + _serviceScope.Dispose(); _logger.LogInformation("Finished unloading plugin {Name}", cachedName); } diff --git a/managed/CounterStrikeSharp.API/Modules/Admin/AdminCommandOverrides.cs b/managed/CounterStrikeSharp.API/Modules/Admin/AdminCommandOverrides.cs index c4f43620..4f894670 100644 --- a/managed/CounterStrikeSharp.API/Modules/Admin/AdminCommandOverrides.cs +++ b/managed/CounterStrikeSharp.API/Modules/Admin/AdminCommandOverrides.cs @@ -19,7 +19,7 @@ namespace CounterStrikeSharp.API.Modules.Admin public static partial class AdminManager { - private static Dictionary CommandOverrides = new(); + private static Dictionary CommandOverrides = new(StringComparer.InvariantCultureIgnoreCase); public static void LoadCommandOverrides(string overridePath) { try diff --git a/managed/CounterStrikeSharp.API/Modules/Admin/AdminManager.cs b/managed/CounterStrikeSharp.API/Modules/Admin/AdminManager.cs index 4ce00e0a..64ce5e04 100644 --- a/managed/CounterStrikeSharp.API/Modules/Admin/AdminManager.cs +++ b/managed/CounterStrikeSharp.API/Modules/Admin/AdminManager.cs @@ -2,20 +2,30 @@ using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Utils; using System.Linq; using System.Reflection; +using CounterStrikeSharp.API.Core.Commands; +using CounterStrikeSharp.API.Core.Logging; +using Microsoft.Extensions.Logging; namespace CounterStrikeSharp.API.Modules.Admin { - public static partial class AdminManager { + public static ICommandManager CommandManagerProvider { get; internal set; } = null!; + public static void AddCommands() { - CommandUtils.AddStandaloneCommand("css_admins_reload", "Reloads the admin file.", ReloadAdminsCommand); - CommandUtils.AddStandaloneCommand("css_admins_list", "List admins and their flags.", ListAdminsCommand); - CommandUtils.AddStandaloneCommand("css_groups_reload", "Reloads the admin groups file.", ReloadAdminGroupsCommand); - CommandUtils.AddStandaloneCommand("css_groups_list", "List admin groups and their flags.", ListAdminGroupsCommand); - CommandUtils.AddStandaloneCommand("css_overrides_reload", "Reloads the admin command overrides file.", ReloadAdminOverridesCommand); - CommandUtils.AddStandaloneCommand("css_overrides_list", "List admin command overrides and their flags.", ListAdminOverridesCommand); + CommandManagerProvider.RegisterCommand(new CommandDefinition("css_admins_reload", "Reloads the admin file.", + ReloadAdminsCommand)); + CommandManagerProvider.RegisterCommand(new CommandDefinition("css_admins_list", + "List admins and their flags.", ListAdminsCommand)); + CommandManagerProvider.RegisterCommand(new CommandDefinition("css_groups_reload", + "Reloads the admin groups file.", ReloadAdminGroupsCommand)); + CommandManagerProvider.RegisterCommand(new CommandDefinition("css_groups_list", + "List admin groups and their flags.", ListAdminGroupsCommand)); + CommandManagerProvider.RegisterCommand(new CommandDefinition("css_overrides_reload", + "Reloads the admin command overrides file.", ReloadAdminOverridesCommand)); + CommandManagerProvider.RegisterCommand(new CommandDefinition("css_overrides_list", + "List admin command overrides and their flags.", ListAdminOverridesCommand)); } public static void MergeGroupPermsIntoAdmins() @@ -26,7 +36,7 @@ namespace CounterStrikeSharp.API.Modules.Admin } } - [RequiresPermissions(permissions:"@css/generic")] + [RequiresPermissions(permissions: "@css/generic")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] private static void ReloadAdminsCommand(CCSPlayerController? player, CommandInfo command) { @@ -36,7 +46,7 @@ namespace CounterStrikeSharp.API.Modules.Admin MergeGroupPermsIntoAdmins(); } - [RequiresPermissions(permissions:"@css/generic")] + [RequiresPermissions(permissions: "@css/generic")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] private static void ListAdminsCommand(CCSPlayerController? player, CommandInfo command) { @@ -50,7 +60,7 @@ namespace CounterStrikeSharp.API.Modules.Admin } } - [RequiresPermissions(permissions:"@css/generic")] + [RequiresPermissions(permissions: "@css/generic")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] private static void ReloadAdminGroupsCommand(CCSPlayerController? player, CommandInfo command) { @@ -85,8 +95,9 @@ namespace CounterStrikeSharp.API.Modules.Admin { foreach (var (commandName, commandDef) in CommandOverrides) { - command.ReplyToCommand($"{commandName} (enabled: {commandDef.Enabled.ToString()}) - {string.Join(", ", commandDef.Flags)}"); + command.ReplyToCommand( + $"{commandName} (enabled: {commandDef.Enabled.ToString()}) - {string.Join(", ", commandDef.Flags)}"); } } } -} +} \ No newline at end of file diff --git a/managed/CounterStrikeSharp.API/Modules/Admin/BaseRequiresPermissions.cs b/managed/CounterStrikeSharp.API/Modules/Admin/BaseRequiresPermissions.cs index c8f7f0de..4e729e2b 100644 --- a/managed/CounterStrikeSharp.API/Modules/Admin/BaseRequiresPermissions.cs +++ b/managed/CounterStrikeSharp.API/Modules/Admin/BaseRequiresPermissions.cs @@ -34,9 +34,9 @@ namespace CounterStrikeSharp.API.Modules.Admin if (caller?.AuthorizedSteamID == null) return false; var adminData = AdminManager.GetPlayerAdminData(caller.AuthorizedSteamID); if (adminData == null) return false; - if (adminData.CommandOverrides.ContainsKey(Command)) + if (adminData.CommandOverrides.TryGetValue(Command, out var command)) { - return adminData.CommandOverrides[Command]; + return command; } return true; diff --git a/managed/CounterStrikeSharp.API/Modules/Utils/StandaloneCommand.cs b/managed/CounterStrikeSharp.API/Modules/Utils/StandaloneCommand.cs deleted file mode 100644 index 54b19bd9..00000000 --- a/managed/CounterStrikeSharp.API/Modules/Utils/StandaloneCommand.cs +++ /dev/null @@ -1,109 +0,0 @@ -using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Admin; -using CounterStrikeSharp.API.Modules.Commands; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace CounterStrikeSharp.API.Modules.Utils; - -public class CommandUtils -{ - public static void AddStandaloneCommand(string name, string description, CommandInfo.CommandCallback handler) - { - var wrappedHandler = new Action((i, ptr) => - { - var caller = (i != -1) ? new CCSPlayerController(NativeAPI.GetEntityFromIndex(i + 1)) : null; - var command = new CommandInfo(ptr, caller); - - var methodInfo = handler?.GetMethodInfo(); - - // We do not need to do permission checks on commands executed from the server console. - // The server will always be allowed to execute commands (unless marked as client only like above) - if (caller != null) - { - // Do not execute command if we do not have the correct permissions. - var adminData = AdminManager.GetPlayerAdminData(caller!.AuthorizedSteamID); - var permissionsToCheck = new List(); - - - // If our command is overriden, we dynamically create a new permissions attribute - // based on the data that is stored in admin_overrides.json. - if (AdminManager.CommandIsOverriden(name)) - { - var data = AdminManager.GetCommandOverrideData(name); - if (data != null) - { - var attrType = (data.CheckType == "all") ? typeof(RequiresPermissions) : typeof(RequiresPermissionsOr); - var attr = (BaseRequiresPermissions)Activator.CreateInstance(attrType, args: AdminManager.GetPermissionOverrides(name)); - - if (attr != null) permissionsToCheck.Add(attr); - } - } - // The permissions for this command are not being overriden here, so we - // grab the permissions to check straight from the attribute. - else - { - var permissions = methodInfo?.GetCustomAttributes(); - if (permissions != null) permissionsToCheck.AddRange(permissions); - } - - foreach (var attr in permissionsToCheck) - { - attr.Command = name; - if (!attr.CanExecuteCommand(caller)) - { - var responseStr = (attr.GetType() == typeof(RequiresPermissions)) ? - "You are missing the correct permissions" : "You do not have one of the correct permissions"; - - var flags = attr.Permissions.Except(adminData?.GetAllFlags() ?? new HashSet()); - flags = flags.Except(adminData?.Groups ?? new HashSet()); - command.ReplyToCommand($"[CSS] {responseStr} ({string.Join(", ", flags)}) to execute this command."); - - return; - } - } - } - - // Do not execute if we shouldn't be calling this command. - var helperAttribute = methodInfo?.GetCustomAttribute(); - if (helperAttribute != null) - { - switch (helperAttribute.WhoCanExcecute) - { - case CommandUsage.CLIENT_AND_SERVER: break; // Allow command through. - case CommandUsage.CLIENT_ONLY: - if (caller == null || !caller.IsValid) { command.ReplyToCommand("[CSS] This command can only be executed by clients."); return; } - break; - case CommandUsage.SERVER_ONLY: - if (caller != null && caller.IsValid) { command.ReplyToCommand("[CSS] This command can only be executed by the server."); return; } - break; - default: throw new ArgumentException("Unrecognised CommandUsage value passed in CommandHelperAttribute."); - } - - // Technically the command itself counts as the first argument, - // but we'll just ignore that for this check. - if (helperAttribute.MinArgs != 0 && command.ArgCount - 1 < helperAttribute.MinArgs) - { - // Remove the "css_" from the beginning of the command name if it's present. - // Most of the time, users will be calling commands from chat. - var commandCalled = command.ArgByIndex(0); - var properCommandName = (commandCalled.StartsWith("css_")) ? commandCalled.Replace("css_", "") : commandCalled; - - command.ReplyToCommand($"[CSS] Expected usage: \"!{properCommandName} {helperAttribute.Usage}\"."); - return; - } - } - - handler?.Invoke(caller, command); - }); - - var methodInfo = handler?.GetMethodInfo(); - var helperAttribute = methodInfo?.GetCustomAttribute(); - - var subscriber = new BasePlugin.CallbackSubscriber(handler, wrappedHandler, () => { }); - NativeAPI.AddCommand(name, description, (helperAttribute?.WhoCanExcecute == CommandUsage.SERVER_ONLY), - (int)ConCommandFlags.FCVAR_LINKED_CONCOMMAND, subscriber.GetInputArgument()); - } -} diff --git a/managed/CounterStrikeSharp.API/ServiceCollectionExtensions.cs b/managed/CounterStrikeSharp.API/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..22b7ea48 --- /dev/null +++ b/managed/CounterStrikeSharp.API/ServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; + +namespace CounterStrikeSharp.API; + +public static class ServiceCollectionExtensions +{ + /// + /// Decorates a given interface with a decorator class, keeping a local copy for singleton access. + /// + /// + /// + /// + /// + public static IServiceCollection DecorateSingleton(this IServiceCollection services) where TService : class + where TDecoratedService : class, TService + { + TService? localCopy = default(TService?); + services.Decorate((inner, provider) => + { + if (localCopy == null) + { + localCopy = ActivatorUtilities.CreateInstance(provider, inner); + } + + return localCopy; + }); + + return services; + } +} \ No newline at end of file diff --git a/managed/TestPlugin/TestPluginServiceCollection.cs b/managed/TestPlugin/TestPluginServiceCollection.cs index a3b9a968..4afd2cee 100644 --- a/managed/TestPlugin/TestPluginServiceCollection.cs +++ b/managed/TestPlugin/TestPluginServiceCollection.cs @@ -1,5 +1,6 @@ using System; using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Commands; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -8,15 +9,21 @@ namespace TestPlugin; public class TestInjectedClass { private readonly ILogger _logger; + private readonly ICommandManager _commandManager; - public TestInjectedClass(ILogger logger) + public TestInjectedClass(ILogger logger, ICommandManager commandManager) { _logger = logger; + _commandManager = commandManager; } public void Hello() { _logger.LogInformation("Hello World from Test Injected Class"); + _commandManager.RegisterCommand(new CommandDefinition("cssharp_helloworld", "Hello World!", (player, info) => + { + info.ReplyToCommand("Hello!"); + })); } }