|
|
@@ -28,6 +28,8 @@ namespace Discord.Commands |
|
|
|
private readonly HashSet<ModuleInfo> _moduleDefs; |
|
|
|
private readonly CommandMap _map; |
|
|
|
|
|
|
|
internal readonly IServiceProvider _serviceProvider; |
|
|
|
|
|
|
|
internal readonly bool _caseSensitive, _throwOnError, _ignoreExtraArgs; |
|
|
|
internal readonly char _separatorChar; |
|
|
|
internal readonly RunMode _defaultRunMode; |
|
|
@@ -41,6 +43,7 @@ namespace Discord.Commands |
|
|
|
public CommandService() : this(new CommandServiceConfig()) { } |
|
|
|
public CommandService(CommandServiceConfig config) |
|
|
|
{ |
|
|
|
_serviceProvider = config.ServiceProvider ?? EmptyServiceProvider.Instance; |
|
|
|
_caseSensitive = config.CaseSensitiveCommands; |
|
|
|
_throwOnError = config.ThrowOnError; |
|
|
|
_ignoreExtraArgs = config.IgnoreExtraArgs; |
|
|
@@ -86,18 +89,18 @@ namespace Discord.Commands |
|
|
|
var builder = new ModuleBuilder(this, null, primaryAlias); |
|
|
|
buildFunc(builder); |
|
|
|
|
|
|
|
var module = builder.Build(this); |
|
|
|
var module = builder.Build(this, null); |
|
|
|
|
|
|
|
//should be fine to pass null here since it'll never get checked from this path anyway |
|
|
|
return LoadModuleInternal(module, null); |
|
|
|
return LoadModuleInternal(module); |
|
|
|
} |
|
|
|
finally |
|
|
|
{ |
|
|
|
_moduleLock.Release(); |
|
|
|
} |
|
|
|
} |
|
|
|
public Task<ModuleInfo> AddModuleAsync<T>(IServiceProvider services) => AddModuleAsync(typeof(T), services); |
|
|
|
public async Task<ModuleInfo> AddModuleAsync(Type type, IServiceProvider services) |
|
|
|
public Task<ModuleInfo> AddModuleAsync<T>() => AddModuleAsync(typeof(T)); |
|
|
|
public async Task<ModuleInfo> AddModuleAsync(Type type) |
|
|
|
{ |
|
|
|
await _moduleLock.WaitAsync().ConfigureAwait(false); |
|
|
|
try |
|
|
@@ -114,14 +117,14 @@ namespace Discord.Commands |
|
|
|
|
|
|
|
_typedModuleDefs[module.Key] = module.Value; |
|
|
|
|
|
|
|
return LoadModuleInternal(module.Value, services); |
|
|
|
return LoadModuleInternal(module.Value); |
|
|
|
} |
|
|
|
finally |
|
|
|
{ |
|
|
|
_moduleLock.Release(); |
|
|
|
} |
|
|
|
} |
|
|
|
public async Task<IEnumerable<ModuleInfo>> AddModulesAsync(Assembly assembly, IServiceProvider services) |
|
|
|
public async Task<IEnumerable<ModuleInfo>> AddModulesAsync(Assembly assembly) |
|
|
|
{ |
|
|
|
await _moduleLock.WaitAsync().ConfigureAwait(false); |
|
|
|
try |
|
|
@@ -132,7 +135,7 @@ namespace Discord.Commands |
|
|
|
foreach (var info in moduleDefs) |
|
|
|
{ |
|
|
|
_typedModuleDefs[info.Key] = info.Value; |
|
|
|
LoadModuleInternal(info.Value, services); |
|
|
|
LoadModuleInternal(info.Value); |
|
|
|
} |
|
|
|
|
|
|
|
return moduleDefs.Select(x => x.Value).ToImmutableArray(); |
|
|
@@ -142,31 +145,31 @@ namespace Discord.Commands |
|
|
|
_moduleLock.Release(); |
|
|
|
} |
|
|
|
} |
|
|
|
private ModuleInfo LoadModuleInternal(ModuleInfo module, IServiceProvider services) |
|
|
|
private ModuleInfo LoadModuleInternal(ModuleInfo module) |
|
|
|
{ |
|
|
|
_moduleDefs.Add(module); |
|
|
|
|
|
|
|
if (module.TypeInfo.IsSpecified) |
|
|
|
{ |
|
|
|
//keep this for safety? |
|
|
|
services = services ?? EmptyServiceProvider.Instance; |
|
|
|
try |
|
|
|
{ |
|
|
|
var moduleInstance = ReflectionUtils.CreateObject<IModuleBase>(module.TypeInfo.Value, this, services); |
|
|
|
moduleInstance.OnModuleBuilding(this); |
|
|
|
} |
|
|
|
catch(Exception) |
|
|
|
{ |
|
|
|
//unsure of what to do here |
|
|
|
throw; |
|
|
|
} |
|
|
|
} |
|
|
|
//if (module.TypeInfo.IsSpecified) |
|
|
|
//{ |
|
|
|
// //keep this for safety? |
|
|
|
// services = services ?? EmptyServiceProvider.Instance; |
|
|
|
// try |
|
|
|
// { |
|
|
|
// var moduleInstance = ReflectionUtils.CreateObject<IModuleBase>(module.TypeInfo.Value, this, services); |
|
|
|
// moduleInstance.OnModuleBuilding(this); |
|
|
|
// } |
|
|
|
// catch(Exception) |
|
|
|
// { |
|
|
|
// //unsure of what to do here |
|
|
|
// throw; |
|
|
|
// } |
|
|
|
//} |
|
|
|
|
|
|
|
foreach (var command in module.Commands) |
|
|
|
_map.AddCommand(command); |
|
|
|
|
|
|
|
foreach (var submodule in module.Submodules) |
|
|
|
LoadModuleInternal(submodule, services); |
|
|
|
LoadModuleInternal(submodule); |
|
|
|
|
|
|
|
return module; |
|
|
|
} |
|
|
@@ -243,7 +246,7 @@ namespace Discord.Commands |
|
|
|
var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary<Type, TypeReader>()); |
|
|
|
var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); |
|
|
|
readers[nullableReader.GetType()] = nullableReader; |
|
|
|
} |
|
|
|
} |
|
|
|
internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) |
|
|
|
{ |
|
|
|
if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) |
|
|
@@ -291,97 +294,99 @@ namespace Discord.Commands |
|
|
|
return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); |
|
|
|
} |
|
|
|
|
|
|
|
public Task<IResult> ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) |
|
|
|
=> ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); |
|
|
|
public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) |
|
|
|
public Task<IResult> ExecuteAsync(ICommandContext context, int argPos, /*IServiceProvider services = null,*/ MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) |
|
|
|
=> ExecuteAsync(context, context.Message.Content.Substring(argPos), /*services,*/ multiMatchHandling); |
|
|
|
public async Task<IResult> ExecuteAsync(ICommandContext context, string input, /*IServiceProvider services = null,*/ MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) |
|
|
|
{ |
|
|
|
services = services ?? EmptyServiceProvider.Instance; |
|
|
|
|
|
|
|
var searchResult = Search(context, input); |
|
|
|
if (!searchResult.IsSuccess) |
|
|
|
return searchResult; |
|
|
|
|
|
|
|
var commands = searchResult.Commands; |
|
|
|
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); |
|
|
|
|
|
|
|
foreach (var match in commands) |
|
|
|
//services = services ?? EmptyServiceProvider.Instance; |
|
|
|
using (var scope = _serviceProvider.CreateScope()) |
|
|
|
{ |
|
|
|
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); |
|
|
|
} |
|
|
|
var searchResult = Search(context, input); |
|
|
|
if (!searchResult.IsSuccess) |
|
|
|
return searchResult; |
|
|
|
|
|
|
|
var successfulPreconditions = preconditionResults |
|
|
|
.Where(x => x.Value.IsSuccess) |
|
|
|
.ToArray(); |
|
|
|
var commands = searchResult.Commands; |
|
|
|
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); |
|
|
|
|
|
|
|
if (successfulPreconditions.Length == 0) |
|
|
|
{ |
|
|
|
//All preconditions failed, return the one from the highest priority command |
|
|
|
var bestCandidate = preconditionResults |
|
|
|
.OrderByDescending(x => x.Key.Command.Priority) |
|
|
|
.FirstOrDefault(x => !x.Value.IsSuccess); |
|
|
|
return bestCandidate.Value; |
|
|
|
} |
|
|
|
foreach (var match in commands) |
|
|
|
{ |
|
|
|
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, scope.ServiceProvider).ConfigureAwait(false); |
|
|
|
} |
|
|
|
|
|
|
|
//If we get this far, at least one precondition was successful. |
|
|
|
var successfulPreconditions = preconditionResults |
|
|
|
.Where(x => x.Value.IsSuccess) |
|
|
|
.ToArray(); |
|
|
|
|
|
|
|
var parseResultsDict = new Dictionary<CommandMatch, ParseResult>(); |
|
|
|
foreach (var pair in successfulPreconditions) |
|
|
|
{ |
|
|
|
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); |
|
|
|
if (successfulPreconditions.Length == 0) |
|
|
|
{ |
|
|
|
//All preconditions failed, return the one from the highest priority command |
|
|
|
var bestCandidate = preconditionResults |
|
|
|
.OrderByDescending(x => x.Key.Command.Priority) |
|
|
|
.FirstOrDefault(x => !x.Value.IsSuccess); |
|
|
|
return bestCandidate.Value; |
|
|
|
} |
|
|
|
|
|
|
|
//If we get this far, at least one precondition was successful. |
|
|
|
|
|
|
|
if (parseResult.Error == CommandError.MultipleMatches) |
|
|
|
var parseResultsDict = new Dictionary<CommandMatch, ParseResult>(); |
|
|
|
foreach (var pair in successfulPreconditions) |
|
|
|
{ |
|
|
|
IReadOnlyList<TypeReaderValue> argList, paramList; |
|
|
|
switch (multiMatchHandling) |
|
|
|
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, scope.ServiceProvider).ConfigureAwait(false); |
|
|
|
|
|
|
|
if (parseResult.Error == CommandError.MultipleMatches) |
|
|
|
{ |
|
|
|
case MultiMatchHandling.Best: |
|
|
|
argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); |
|
|
|
paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); |
|
|
|
parseResult = ParseResult.FromSuccess(argList, paramList); |
|
|
|
break; |
|
|
|
IReadOnlyList<TypeReaderValue> argList, paramList; |
|
|
|
switch (multiMatchHandling) |
|
|
|
{ |
|
|
|
case MultiMatchHandling.Best: |
|
|
|
argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); |
|
|
|
paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); |
|
|
|
parseResult = ParseResult.FromSuccess(argList, paramList); |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
parseResultsDict[pair.Key] = parseResult; |
|
|
|
} |
|
|
|
parseResultsDict[pair.Key] = parseResult; |
|
|
|
} |
|
|
|
|
|
|
|
// Calculates the 'score' of a command given a parse result |
|
|
|
float CalculateScore(CommandMatch match, ParseResult parseResult) |
|
|
|
{ |
|
|
|
float argValuesScore = 0, paramValuesScore = 0; |
|
|
|
|
|
|
|
if (match.Command.Parameters.Count > 0) |
|
|
|
// Calculates the 'score' of a command given a parse result |
|
|
|
float CalculateScore(CommandMatch match, ParseResult parseResult) |
|
|
|
{ |
|
|
|
var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; |
|
|
|
var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; |
|
|
|
float argValuesScore = 0, paramValuesScore = 0; |
|
|
|
|
|
|
|
argValuesScore = argValuesSum / match.Command.Parameters.Count; |
|
|
|
paramValuesScore = paramValuesSum / match.Command.Parameters.Count; |
|
|
|
if (match.Command.Parameters.Count > 0) |
|
|
|
{ |
|
|
|
var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; |
|
|
|
var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; |
|
|
|
|
|
|
|
argValuesScore = argValuesSum / match.Command.Parameters.Count; |
|
|
|
paramValuesScore = paramValuesSum / match.Command.Parameters.Count; |
|
|
|
} |
|
|
|
|
|
|
|
var totalArgsScore = (argValuesScore + paramValuesScore) / 2; |
|
|
|
return match.Command.Priority + totalArgsScore * 0.99f; |
|
|
|
} |
|
|
|
|
|
|
|
var totalArgsScore = (argValuesScore + paramValuesScore) / 2; |
|
|
|
return match.Command.Priority + totalArgsScore * 0.99f; |
|
|
|
} |
|
|
|
//Order the parse results by their score so that we choose the most likely result to execute |
|
|
|
var parseResults = parseResultsDict |
|
|
|
.OrderByDescending(x => CalculateScore(x.Key, x.Value)); |
|
|
|
|
|
|
|
//Order the parse results by their score so that we choose the most likely result to execute |
|
|
|
var parseResults = parseResultsDict |
|
|
|
.OrderByDescending(x => CalculateScore(x.Key, x.Value)); |
|
|
|
var successfulParses = parseResults |
|
|
|
.Where(x => x.Value.IsSuccess) |
|
|
|
.ToArray(); |
|
|
|
|
|
|
|
var successfulParses = parseResults |
|
|
|
.Where(x => x.Value.IsSuccess) |
|
|
|
.ToArray(); |
|
|
|
if (successfulParses.Length == 0) |
|
|
|
{ |
|
|
|
//All parses failed, return the one from the highest priority command, using score as a tie breaker |
|
|
|
var bestMatch = parseResults |
|
|
|
.FirstOrDefault(x => !x.Value.IsSuccess); |
|
|
|
return bestMatch.Value; |
|
|
|
} |
|
|
|
|
|
|
|
if (successfulParses.Length == 0) |
|
|
|
{ |
|
|
|
//All parses failed, return the one from the highest priority command, using score as a tie breaker |
|
|
|
var bestMatch = parseResults |
|
|
|
.FirstOrDefault(x => !x.Value.IsSuccess); |
|
|
|
return bestMatch.Value; |
|
|
|
//If we get this far, at least one parse was successful. Execute the most likely overload. |
|
|
|
var chosenOverload = successfulParses[0]; |
|
|
|
return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, scope.ServiceProvider).ConfigureAwait(false); |
|
|
|
} |
|
|
|
|
|
|
|
//If we get this far, at least one parse was successful. Execute the most likely overload. |
|
|
|
var chosenOverload = successfulParses[0]; |
|
|
|
return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); |
|
|
|
} |
|
|
|
} |
|
|
|
} |