using System.Globalization;
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)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
if (configuration is null)
{
throw new ArgumentNullException(nameof(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)
{
if (assembly is null)
{
throw new ArgumentNullException(nameof(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)
{
if (moduleType is null)
{
throw new ArgumentNullException(nameof(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(VirtualParadiseMessage message)
{
if (message is null)
{
throw new ArgumentNullException(nameof(message));
}
if (message.Type != MessageType.ChatMessage)
{
return base.OnMessageReceived(message);
}
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(message);
}
var context = new CommandContext(Client, message.Author, command.Name, commandNameString, rawArguments.ToString());
MethodInfo commandMethod = command.Method;
foreach (var attribute in commandMethod.GetCustomAttributes())
{
if (!attribute.PerformAsync(context).ConfigureAwait(false).GetAwaiter().GetResult())
{
return base.OnMessageReceived(message);
}
}
object?[] arguments = {context};
arguments = ParseArguments(rawArguments, arguments, command);
ParameterInfo[] parameters = commandMethod.GetParameters();
if (parameters.Length != arguments.Length || parameters[arguments.Length..].Any(p => !p.IsOptional))
{
return base.OnMessageReceived(message);
}
for (var index = 0; index < arguments.Length; index++)
{
Type parameterType = parameters[index].ParameterType;
if (parameterType.IsEnum)
{
var argumentString = arguments[index]?.ToString();
Type enumUnderlyingType = parameterType.GetEnumUnderlyingType();
if (enumUnderlyingType == typeof(int) && int.TryParse(argumentString, out int enumInt))
{
arguments[index] = Enum.ToObject(parameterType, enumInt);
}
else if (enumUnderlyingType == typeof(long) && long.TryParse(argumentString, out long enumLong))
{
arguments[index] = Enum.ToObject(parameterType, enumLong);
}
else if (Enum.TryParse(parameterType, argumentString, true, out object? enumValue))
{
arguments[index] = enumValue;
}
}
else
{
arguments[index] = Convert.ChangeType(arguments[index], parameterType, CultureInfo.InvariantCulture);
}
}
for (int index = arguments.Length; index < parameters.Length; index++)
{
arguments[index] = parameters[index].DefaultValue;
}
object? returnValue = commandMethod.Invoke(command.Module, arguments);
if (returnValue is Task task)
{
return task;
}
return base.OnMessageReceived(message);
}
return base.OnMessageReceived(message);
}
private static object?[] ParseArguments(ReadOnlySpan rawArguments, object?[] arguments, Command command)
{
if (rawArguments.Length <= 0)
{
return arguments;
}
int spaceIndex = rawArguments.IndexOf(' ');
if (spaceIndex == -1)
{
Array.Resize(ref arguments, 2);
arguments[1] = rawArguments.ToString();
return arguments;
}
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();
}
return arguments;
}
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;
}
}
}