using System.Reflection; using Microsoft.Extensions.DependencyInjection; using VpSharp.ClientExtensions; using VpSharp.Commands.Attributes; using VpSharp.Commands.Attributes.ExecutionChecks; using VpSharp.Entities; using VpSharp.EventData; using VpSharp.Internal; namespace VpSharp.Commands; /// /// Implements the Commands extension for . /// public sealed class CommandsExtension : VirtualParadiseClientExtension { private const BindingFlags BindingFlags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance; private readonly Dictionary _commandMap = new(StringComparer.OrdinalIgnoreCase); private readonly CommandsExtensionConfiguration _configuration; /// /// Initializes a new instance of the class. /// /// The owning client. /// The configuration to use. /// /// or is . /// public CommandsExtension(VirtualParadiseClient client, CommandsExtensionConfiguration configuration) : base(client) { ArgumentNullException.ThrowIfNull(client); ArgumentNullException.ThrowIfNull(configuration); _configuration = configuration; _configuration.Services ??= client.Services; } /// /// Registers all commands from all modules in the specified assembly. /// /// The assembly whose command modules to register. /// /// A module in the specified assembly does not have a public constructor, or has more than one public constructor. /// /// A command module could not be instantiated. /// /// A command in the specified assembly does not have a as its first parameter. /// -or- /// A command in the specified assembly contains a duplicate name or alias for a command. /// -or- /// /// A command in the specified assembly has on a parameter that is not the last in /// the parameter list. /// /// -or- /// /// A module is expecting services but no was registered. /// /// public void RegisterCommands(Assembly assembly) { ArgumentNullException.ThrowIfNull(assembly); foreach (Type type in assembly.GetTypes()) { if (!type.IsAbstract && type.IsSubclassOf(typeof(CommandModule))) { RegisterCommands(type); } } } /// /// Registers the commands defined in the specified type. /// /// /// refers to a type that does not inherit . /// -or- /// /// does not have a public constructor, or has more than one public constructor. /// /// /// could not be instantiated. /// /// A command in the specified module does not have a as its first parameter. /// -or- /// A command in the specified module contains a duplicate name or alias for a command. /// -or- /// /// A command in the specified module has on a parameter that is not the last in the /// parameter list. /// /// -or- /// /// The module is expecting services but no was registered. /// /// public void RegisterCommands() where T : CommandModule { RegisterCommands(typeof(T)); } /// /// Registers the commands defined in the specified type. /// /// The type whose command methods to register. /// is . /// /// refers to an abstract type. /// -or- /// refers to a type that does not inherit . /// -or- /// /// does not have a public constructor, or has more than one public constructor. /// /// /// could not be instantiated. /// /// A command in the specified module does not have a as its first parameter. /// -or- /// A command in the specified module contains a duplicate name or alias for a command. /// -or- /// /// A command in the specified module has on a parameter that is not the last in the /// parameter list. /// /// -or- /// /// The module is expecting services but no was registered. /// /// public void RegisterCommands(Type moduleType) { ArgumentNullException.ThrowIfNull(moduleType); if (moduleType.IsAbstract) { throw new ArgumentException("Module type cannot be abstract"); } if (!moduleType.IsSubclassOf(typeof(CommandModule))) { throw new ArgumentException($"Module type is not a subclass of {typeof(CommandModule)}"); } IServiceProvider? serviceProvider = _configuration.Services; object instance = DependencyInjectionUtility.CreateInstance(moduleType, serviceProvider); if (instance is not CommandModule module) { throw new TypeInitializationException(moduleType.FullName, null); } foreach (MethodInfo method in moduleType.GetMethods(BindingFlags)) { RegisterCommandMethod(module, method); } } /// protected internal override Task OnMessageReceived(MessageReceivedEventArgs args) { ArgumentNullException.ThrowIfNull(args); VirtualParadiseMessage message = args.Message; if (message.Type != MessageType.ChatMessage) { return base.OnMessageReceived(args); } foreach (ReadOnlySpan prefix in _configuration.Prefixes) { ReadOnlySpan content = message.Content; if (!content.StartsWith(prefix)) { continue; } ReadOnlySpan commandName, rawArguments; int spaceIndex = content.IndexOf(' '); if (spaceIndex == -1) { commandName = content[prefix.Length..]; rawArguments = ReadOnlySpan.Empty; } else { commandName = content[prefix.Length..spaceIndex]; rawArguments = content[(spaceIndex + 1)..]; } var commandNameString = commandName.ToString(); if (!_commandMap.TryGetValue(commandNameString, out Command? command)) { return base.OnMessageReceived(args); } var context = new CommandContext(Client, message.Author, command.Name, commandNameString, rawArguments.ToString()); foreach (var attribute in command.Method.GetCustomAttributes()) { if (!attribute.PerformAsync(context).ConfigureAwait(false).GetAwaiter().GetResult()) { return base.OnMessageReceived(args); } } object?[] arguments = {context}; if (rawArguments.Length > 0) { spaceIndex = rawArguments.IndexOf(' '); if (spaceIndex == -1) { Array.Resize(ref arguments, 2); arguments[1] = rawArguments.ToString(); } else { var appendLast = true; for (var argumentIndex = 1; spaceIndex > -1; argumentIndex++) { Array.Resize(ref arguments, argumentIndex + 1); if (argumentIndex == command.Parameters.Length) { if (command.Parameters[argumentIndex - 1].GetCustomAttribute() is not null) { appendLast = false; arguments[argumentIndex] = rawArguments.ToString(); break; } } arguments[argumentIndex] = rawArguments[..spaceIndex].ToString(); rawArguments = rawArguments[(spaceIndex + 1)..]; spaceIndex = rawArguments.IndexOf(' '); argumentIndex++; } if (appendLast) { Array.Resize(ref arguments, arguments.Length + 1); arguments[^1] = rawArguments.ToString(); } } } if (command.Method.GetParameters().Length != arguments.Length) { return base.OnMessageReceived(args); } object? returnValue = command.Method.Invoke(command.Module, arguments); if (returnValue is Task task) { return task; } return base.OnMessageReceived(args); } return base.OnMessageReceived(args); } private void RegisterCommandMethod(CommandModule module, MethodInfo methodInfo) { var commandAttribute = methodInfo.GetCustomAttribute(); if (commandAttribute is null) { return; } var aliasesAttribute = methodInfo.GetCustomAttribute(); ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length == 0 || parameters[0].ParameterType != typeof(CommandContext)) { throw new InvalidOperationException($"Command method must have a {typeof(CommandContext)} parameter at index 0."); } for (var index = 0; index < parameters.Length - 1; index++) { if (parameters[index].GetCustomAttribute() is not null) { throw new InvalidOperationException($"{typeof(RemainderAttribute)} can only be placed on the last parameter."); } } var types = new Type[parameters.Length - 1]; for (var index = 1; index < parameters.Length; index++) { types[index - 1] = parameters[index].ParameterType; } var command = new Command( commandAttribute.Name, aliasesAttribute?.Aliases ?? ArraySegment.Empty, methodInfo, module ); if (_commandMap.ContainsKey(command.Name)) { throw new InvalidOperationException($"Duplicate command name registered ({command.Name})"); } _commandMap[command.Name] = command; foreach (string alias in command.Aliases) { if (_commandMap.ContainsKey(alias)) { throw new InvalidOperationException($"Duplicate command name registered ({alias})"); } _commandMap[alias] = command; } } }