static IEnumerable <CommandAttribute> GetCommandMethods(this CommandModule self, MyPromoteLevel maxLevel) { foreach (var method in self.GetType().GetMethods()) { if (!method.TryGetAttribute <CommandAttribute>(out var command)) { continue; } if (!method.TryGetAttribute <PermissionAttribute>(out var permission)) { continue; } if (permission.PromoteLevel > maxLevel) { continue; } yield return(command); } }
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; } } } }