/// <summary> /// Register an argument converter. /// </summary> /// <param name="type"> The type of the argument converter to register. </param> /// <exception cref="ArgumentNullException"> The given type is null. </exception> /// <exception cref="InvalidOperationException"> The given type does not implement IArgumentConverter. </exception> /// <exception cref="InvalidOperationException"> Failed to instantiate argument converter, or the converter returned a null converted type. </exception> public void RegisterArgumentConverter(Type type) { if (type == null) { throw new ArgumentNullException(nameof(type)); } if (!typeof(IArgumentConverter).IsAssignableFrom(type)) { throw new InvalidOperationException("Type must implement IArgumentConverter."); } if (!type.IsDefined(typeof(ArgumentConverterAttribute))) { throw new InvalidOperationException( $"Argument converter '{type.FullName}' does not have an [ArgumentConverter] attribute! Please add it as s&box requires it."); } if (Library.Create <IArgumentConverter>(type) is not { } converter) { throw new InvalidOperationException($"Could not instantiate argument converter of type '{type.Name}'."); } var convertedType = converter.GetConvertedType(); if (convertedType == null) { throw new InvalidOperationException($"Converter '{type.FullName}' returned null converted type."); } if (_converters.ContainsKey(convertedType)) { AdvisorLog.Warning($"Cannot register converter '{type.FullName}' as a converter for type '{convertedType.FullName}' already exists."); return; } AdvisorLog.Debug($"Registered argument converter '{type.FullName}' for type '{converter.GetConvertedType().FullName}'"); _converters.Add(convertedType, converter); }
private void InitializeAdvisor() { AdvisorLog.Info("Initializing Advisor Core..."); var sw = new Stopwatch(); _services = new AdvisorServiceContainer(this); _configuration = new ConfigurationService(); _configuration.LoadConfiguration(); _services.AddService(_configuration); _commandRegistry = new CommandRegistry(this); _services.AddService(_commandRegistry); // Register Advisor's argument converters and commands. _commandRegistry.RegisterArgumentConverters(Assembly.GetExecutingAssembly()); _commandRegistry.RegisterCommandModules(Assembly.GetExecutingAssembly()); _commandHandler = new CommandHandler(this); _services.AddService(_commandHandler); AdvisorLog.Info($"Successfully initialized Advisor Core in {sw.ElapsedMilliseconds} ms"); }
private void RegisterCommand(CommandModule module, MethodInfo info) { if (module == null) { throw new ArgumentNullException(nameof(module)); } if (info == null) { throw new ArgumentNullException(nameof(info)); } // Check that the given method is actually in the module. if (info.DeclaringType != module.GetType()) { throw new InvalidOperationException( "Cannot register a MethodInfo in a CommandModule that isn't its declaring type."); } // Check if the method has the command attribute. This should be the case here, but never trust others. var commandAttr = info.GetCustomAttribute <CommandAttribute>(); if (commandAttr == null) { throw new InvalidOperationException($"Method {info.Name} has no CommandAttribute"); } // Check that the command's name is valid. if (!commandAttr.HasValidCommandName()) { throw new ArgumentException("Command name can only consist of characters A-Z, 0-9, -, _"); } if (module.Prefix == null) { // Make sure that there's no existing command with that name. if (_rootCommands.ContainsKey(commandAttr.Name.ToLower())) { AdvisorLog.Warning($"Cannot register command '{commandAttr.Name}' from module '{module.GetType().Name}' as another root command with that name exists."); return; } } else { if (_categorizedCommands.ContainsKey(module.Prefix)) { var dict = _categorizedCommands[module.Prefix]; if (dict.ContainsKey(commandAttr.Name.ToLower())) { AdvisorLog.Warning($"Cannot register command '{module.Prefix} {commandAttr.Name}' from module '{module.GetType().Name}' as another command with that name exists."); return; } } } // Check that the return type is void. if (info.ReturnType != typeof(void)) { throw new InvalidOperationException($"Command '{info.Name}' must return void."); } // Check that the first parameter is a CommandContext. var cmdParameters = info.GetParameters(); if (cmdParameters.Length == 0 || cmdParameters[0].ParameterType != typeof(CommandContext)) { throw new InvalidOperationException( "Commands must always have at least one parameter, and start with a CommandContext."); } if (cmdParameters[0].IsOut || cmdParameters[0].IsIn) { throw new InvalidOperationException("Commands do not support in/out parameters."); } var commandArguments = new List <CommandArgument>(); // Verify that we have argument converters for every single one of these types. if (cmdParameters.Length > 1) { for (int i = 1; i < cmdParameters.Length; i++) { var param = cmdParameters[i]; if (param.IsIn || param.IsOut) { throw new InvalidOperationException("Commands do not support in/out parameters."); } var remainder = param.IsDefined(typeof(RemainderAttribute)); // Only the last argument can be catch all. if (remainder && i != cmdParameters.Length - 1) { throw new InvalidOperationException( $"Only the last parameter of a command can have the RemainderAttribute (in command '{info.Name}' of module '{module.GetType().Name}')"); } if (remainder && param.ParameterType != typeof(string)) { throw new InvalidOperationException( $"Only an argument of type string can have the RemainderAttribute (in command '{info.Name}' of module '{module.GetType().Name}')"); } var argType = param.ParameterType; var isParams = false; if (param.IsDefined(typeof(ParamArrayAttribute))) { isParams = true; argType = param.ParameterType.GetElementType(); if (argType == null) { throw new InvalidOperationException( $"Command '{info.Name}' of module '{module.GetType().Name}' has a params argument with an invalid element type."); } } if (!_converters.ContainsKey(argType)) { throw new InvalidOperationException( $"Command '{info.Name}' of module '{module.GetType().Name}' has parameter '{param.Name ?? "null"}' with no IArgumentConverter registered for its type ({argType.Name})"); } var arg = new CommandArgument { ArgumentType = argType, Converter = _converters[argType], Parameter = param, IsParams = isParams, Remainder = remainder, }; commandArguments.Add(arg); } } // Create the command object. var cmd = new Command { Arguments = commandArguments, ParentModule = module, Name = commandAttr.Name, Method = info, CommandAttribute = commandAttr, }; cmd.FullName = !string.IsNullOrWhiteSpace(module.Prefix) ? $"{module.Prefix} {cmd.Name}".ToLower() : cmd.Name.ToLower(); // Populate any additional attributes. foreach (var attr in info.GetCustomAttributes()) { switch (attr) { case DescriptionAttribute desc: cmd.Description = desc.Description; break; case AliasAttribute alias: cmd.Aliases = alias.Aliases; break; } } // TODO: Maybe add a CommandContext, TargetPlayer overload? If the performance is worth it over dyn invoke. if (commandArguments.Count > 0) { // Dynamically generate the delegate for the command's method. var paramTypes = info.GetParameters() .Select(p => p.ParameterType) .Append(info.ReturnType) .ToArray(); // TODO: System.Linq.Expressions // Not as fast as a known delegate signature, and requires executing the command with DynamicInvoke(). // var delType = Expression.GetDelegateType(paramTypes); // cmd.MethodDelegate = Delegate.CreateDelegate(delType, module, info); } else { // No arguments means we know exactly the type of the delegate and can build it that way. // Faster to execute than DynamicInvoke on an arbitrary delegate. cmd.MethodDelegateNoParams = info.CreateDelegate <Action <CommandContext> >(module); } AdvisorLog.Debug($"Registered command '{cmd.Name}' with {cmd.Arguments.Count} arguments."); module.InternalCommands.Add(cmd); if (module.Prefix == null) { _rootCommands.Add(cmd.FullName.ToLower(), cmd); foreach (string alias in cmd.Aliases) { var a = alias.ToLower(); if (!_rootCommands.ContainsKey(a)) { _rootCommands.Add(a, cmd); } else { AdvisorLog.Error($"Command '{cmd.Name}' in module 'w{module.GetType().Name}' has an alias '{alias}' that conflicts with another command!"); return; } } } else { var prefix = module.Prefix.ToLower(); if (!_categorizedCommands.ContainsKey(prefix)) { _categorizedCommands.Add(prefix, new Dictionary <string, Command>()); } _categorizedCommands[prefix].Add(cmd.Name.ToLower(), cmd); foreach (string alias in cmd.Aliases) { var a = alias.ToLower(); if (!_categorizedCommands[prefix].ContainsKey(a)) { _categorizedCommands[prefix].Add(a, cmd); } else { AdvisorLog.Error($"Command '{module.Prefix} {cmd.Name}' in module {module.GetType().Name}' has an alias '{alias}' that conflicts with another command!"); return; } } } }
public void TestCommand(CommandContext ctx) { AdvisorLog.Error("Hello, world!"); }