Command Overhaul (#330)

This commit is contained in:
Michael Wilson
2024-02-19 18:38:07 +10:00
committed by GitHub
parent 607e6c61f5
commit 48fa9ca076
18 changed files with 481 additions and 287 deletions

View File

@@ -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<IPluginManager, PluginManager>();
services.AddSingleton<IPlayerLanguageManager, PlayerLanguageManager>();
services.AddScoped<IPluginContextQueryHandler, PluginContextQueryHandler>();
services.AddSingleton<ICommandManager, CommandManager>();
services.Scan(i => i.FromCallingAssembly()
.AddClasses(c => c.AssignableTo<IStartupService>())
@@ -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<GameDataProvider>();
GameData.GameDataProvider = rootScope.ServiceProvider.GetRequiredService<GameDataProvider>();
AdminManager.CommandManagerProvider = rootScope.ServiceProvider.GetRequiredService<ICommandManager>();
var application = scope.ServiceProvider.GetRequiredService<Application>();
var application = rootScope.ServiceProvider.GetRequiredService<Application>();
application.Start();
return 1;

View File

@@ -61,6 +61,12 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:CounterStrikeSharp.API.Modules.Utils.CommandUtils</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:CounterStrikeSharp.API.Core.AnimParamType_t.ANIMPARAM_STRINGTOKEN</Target>
@@ -169,6 +175,24 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:CounterStrikeSharp.API.Modules.Memory.VirtualFunctions.CCSPlayerPawn_Respawn</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:CounterStrikeSharp.API.Modules.Memory.VirtualFunctions.CCSPlayerPawn_RespawnFunc</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>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)</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.CBaseAnimGraph.get_AnimGraphDirty</Target>
@@ -277,6 +301,12 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.CCSPlayerPawn.Respawn</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.CDynamicProp.get_AnimateOnServer</Target>
@@ -313,6 +343,12 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.CoreConfig.#ctor(CounterStrikeSharp.API.Core.Hosting.IScriptHostConfiguration,Microsoft.Extensions.Logging.ILogger{CounterStrikeSharp.API.Core.CoreConfig})</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.CPlayer_WeaponServices.get_AllowSwitchToNoWeapon</Target>
@@ -391,6 +427,24 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>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)</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.Plugin.PluginContext.#ctor(System.IServiceProvider,CounterStrikeSharp.API.Core.Hosting.IScriptHostConfiguration,System.String,System.Int32)</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:CounterStrikeSharp.API.Core.IPlugin.CommandManager</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0010</DiagnosticId>
<Target>T:CounterStrikeSharp.API.Core.RenderMultisampleType_t</Target>
@@ -985,22 +1039,4 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:CounterStrikeSharp.API.Modules.Memory.VirtualFunctions.CCSPlayerPawn_Respawn</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>F:CounterStrikeSharp.API.Modules.Memory.VirtualFunctions.CCSPlayerPawn_RespawnFunc</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.CCSPlayerPawn.Respawn</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
</Suppressions>

View File

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

View File

@@ -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
/// <param name="handler">The callback function to be invoked when the command is executed.</param>
public void AddCommand(string name, string description, CommandInfo.CommandCallback handler)
{
var wrappedHandler = new Action<int, IntPtr>((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<BaseRequiresPermissions>();
// 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<BaseRequiresPermissions>();
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<string>());
flags = flags.Except(adminData?.Groups ?? new HashSet<string>());
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<CommandHelperAttribute>();
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<CommandHelperAttribute>();
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<ConsoleCommandAttribute>();
var helperAttribute = eventHandler.GetCustomAttribute<CommandHelperAttribute>();
foreach (var commandInfo in attributes)
{
AddCommand(commandInfo.Command, commandInfo.Description,
eventHandler.CreateDelegate<CommandInfo.CommandCallback>(instance));
var definition = new CommandDefinition()
{
Name = commandInfo.Command,
Description = commandInfo.Description,
Callback = eventHandler.CreateDelegate<CommandInfo.CommandCallback>(instance),
MinArgs = helperAttribute?.MinArgs,
UsageHint = helperAttribute?.Usage,
ExecutableBy = helperAttribute?.WhoCanExcecute ?? CommandUsage.CLIENT_AND_SERVER,
};
AddCommand(definition);
}
}
}

View File

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

View File

@@ -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<string, IList<CommandDefinition>> _commandDefinitions =
new(StringComparer.InvariantCultureIgnoreCase);
private readonly ILogger<CommandManager> _logger;
private readonly FunctionReference _internalFunctionReference;
public CommandManager(ILogger<CommandManager> 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<CommandDefinition>());
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<BaseRequiresPermissions>();
// 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<BaseRequiresPermissions>();
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<string>());
flags = flags.Except(adminData?.Groups ?? new HashSet<string>());
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<CommandHelperAttribute>();
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);
}
}
}
}

View File

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

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
using CounterStrikeSharp.API.Core.Plugin;
using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core.Commands;
/// <summary>
/// Decorator for <see cref="ICommandManager"/> 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.
/// </summary>
public class PluginCommandManagerDecorator : ICommandManager, IDisposable
{
private readonly ICommandManager _inner;
private readonly List<CommandDefinition> _trackedCommands = new();
private readonly IPluginContext _pluginContext;
private readonly ILogger<PluginCommandManagerDecorator> _logger;
public PluginCommandManagerDecorator(ICommandManager inner, IPluginContext pluginContext, ILogger<PluginCommandManagerDecorator> 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]);
}
}
}

View File

@@ -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<CoreConfig> _logger;
private readonly string _coreConfigPath;
private bool _commandsRegistered = false;
public CoreConfig(IScriptHostConfiguration scriptHostConfiguration, ILogger<CoreConfig> logger)
public CoreConfig(IScriptHostConfiguration scriptHostConfiguration, ICommandManager commandManager, ILogger<CoreConfig> 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(

View File

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

View File

@@ -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<PluginContext> _loadedPluginContexts = new();
private readonly IScriptHostConfiguration _scriptHostConfiguration;
private readonly ICommandManager _commandManager;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<PluginManager> _logger;
public PluginManager(IScriptHostConfiguration scriptHostConfiguration, ILogger<PluginManager> logger, IServiceProvider serviceProvider)
public PluginManager(IScriptHostConfiguration scriptHostConfiguration, ICommandManager commandManager, ILogger<PluginManager> 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();
}

View File

@@ -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<PluginContext>();
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<ICommandManager>(c => _commandManager);
serviceCollection.DecorateSingleton<ICommandManager, PluginCommandManagerDecorator>();
serviceCollection.AddSingleton<IPluginContext>(this);
serviceCollection.TryAddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactory>();
serviceCollection.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
serviceCollection.TryAddTransient(typeof(IStringLocalizer), typeof(StringLocalizer));
ServiceProvider = serviceCollection.BuildServiceProvider();
var minimumApiVersion = pluginType.GetCustomAttribute<MinimumApiVersion>()?.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<ILoggerFactory>()
.CreateLogger(pluginType);
Plugin.CommandManager = _serviceScope.ServiceProvider.GetRequiredService<ICommandManager>();
Plugin.RegisterAllAttributes(Plugin);
Plugin.Localizer = ServiceProvider.GetRequiredService<IStringLocalizer>();
Plugin.Logger = ServiceProvider.GetRequiredService<ILoggerFactory>().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);
}

View File

@@ -19,7 +19,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
public static partial class AdminManager
{
private static Dictionary<string, CommandData> CommandOverrides = new();
private static Dictionary<string, CommandData> CommandOverrides = new(StringComparer.InvariantCultureIgnoreCase);
public static void LoadCommandOverrides(string overridePath)
{
try

View File

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

View File

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

View File

@@ -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<int, IntPtr>((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<BaseRequiresPermissions>();
// 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<BaseRequiresPermissions>();
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<string>());
flags = flags.Except(adminData?.Groups ?? new HashSet<string>());
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<CommandHelperAttribute>();
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<CommandHelperAttribute>();
var subscriber = new BasePlugin.CallbackSubscriber(handler, wrappedHandler, () => { });
NativeAPI.AddCommand(name, description, (helperAttribute?.WhoCanExcecute == CommandUsage.SERVER_ONLY),
(int)ConCommandFlags.FCVAR_LINKED_CONCOMMAND, subscriber.GetInputArgument());
}
}

View File

@@ -0,0 +1,31 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
namespace CounterStrikeSharp.API;
public static class ServiceCollectionExtensions
{
/// <summary>
/// Decorates a given interface with a decorator class, keeping a local copy for singleton access.
/// </summary>
/// <param name="services"></param>
/// <typeparam name="TService"></typeparam>
/// <typeparam name="TDecoratedService"></typeparam>
/// <returns></returns>
public static IServiceCollection DecorateSingleton<TService, TDecoratedService>(this IServiceCollection services) where TService : class
where TDecoratedService : class, TService
{
TService? localCopy = default(TService?);
services.Decorate<TService>((inner, provider) =>
{
if (localCopy == null)
{
localCopy = ActivatorUtilities.CreateInstance<TDecoratedService>(provider, inner);
}
return localCopy;
});
return services;
}
}

View File

@@ -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<TestInjectedClass> _logger;
private readonly ICommandManager _commandManager;
public TestInjectedClass(ILogger<TestInjectedClass> logger)
public TestInjectedClass(ILogger<TestInjectedClass> 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!");
}));
}
}