From 574b503e9e87caee54c6df8143ada14268f4dcbb Mon Sep 17 00:00:00 2001 From: roridev Date: Fri, 27 Nov 2020 13:38:00 -0300 Subject: [PATCH 1/7] Moves CalculateScore function to outer scope. --- src/Discord.Net.Commands/CommandService.cs | 37 +++++++++++----------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 1d4b0e15a..15f0c866d 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -511,7 +511,6 @@ namespace Discord.Commands await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); return searchResult; } - var commands = searchResult.Commands; var preconditionResults = new Dictionary(); @@ -559,24 +558,6 @@ namespace Discord.Commands 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) - { - 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; - } - //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)); @@ -603,6 +584,24 @@ namespace Discord.Commands return result; } + // 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) + { + 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; + } + protected virtual void Dispose(bool disposing) { if (!_isDisposed) From 7955a0909043b964875796142bcfc5d9997e4798 Mon Sep 17 00:00:00 2001 From: roridev Date: Fri, 27 Nov 2020 13:52:53 -0300 Subject: [PATCH 2/7] Creates ValidateAndGetBestMatch function This function will validate all commands from a SearchResult and return the result of said validation, along with the command matched, if a valid match was found. --- src/Discord.Net.Commands/CommandService.cs | 77 ++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 15f0c866d..be83b955f 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -602,6 +602,83 @@ namespace Discord.Commands return match.Command.Priority + totalArgsScore * 0.99f; } + /// + /// Validates and gets the best from a specified + /// + /// The SearchResult. + /// The context of the command. + /// The service provider to be used on the command's dependency injection. + /// The handling mode when multiple command matches are found. + /// A task that represents the asynchronous validation operation. The task result contains the result of the + /// command validation and the command matched, if a match was found. + public async Task<(IResult, Optional)> ValidateAndGetBestMatch(SearchResult matches, ICommandContext context, IServiceProvider provider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + { + if (!matches.IsSuccess) + return (matches, Optional.Create()); + + var commands = matches.Commands; + var preconditionResults = new Dictionary(); + + foreach (var command in commands) + { + preconditionResults[command] = await command.CheckPreconditionsAsync(context, provider); + } + + var successfulPreconditions = preconditionResults + .Where(x => x.Value.IsSuccess) + .ToArray(); + + if (successfulPreconditions.Length == 0) + { + var bestCandidate = preconditionResults + .OrderByDescending(x => x.Key.Command.Priority) + .FirstOrDefault(x => !x.Value.IsSuccess); + + return (bestCandidate.Value, bestCandidate.Key); + } + + var parseResults = new Dictionary(); + + foreach (var pair in successfulPreconditions) + { + var parseResult = await pair.Key.ParseAsync(context, matches, pair.Value, provider).ConfigureAwait(false); + + if (parseResult.Error == CommandError.MultipleMatches) + { + IReadOnlyList 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; + } + } + + parseResults[pair.Key] = parseResult; + } + + var weightedParseResults = parseResults + .OrderByDescending(x => CalculateScore(x.Key, x.Value)); + + var successfulParses = weightedParseResults + .Where(x => x.Value.IsSuccess) + .ToArray(); + + if(successfulParses.Length == 0) + { + var bestMatch = parseResults + .FirstOrDefault(x => !x.Value.IsSuccess); + + return (bestMatch.Value, bestMatch.Key); + } + + var chosenOverload = successfulParses[0]; + + return (chosenOverload.Value, chosenOverload.Key); + } + protected virtual void Dispose(bool disposing) { if (!_isDisposed) From c455b5033123825c639d8f18e52033b69b5595ba Mon Sep 17 00:00:00 2001 From: roridev Date: Fri, 27 Nov 2020 14:10:39 -0300 Subject: [PATCH 3/7] Make use of new ValidateAndGetBestMatch api on ExecuteAsync --- src/Discord.Net.Commands/CommandService.cs | 80 ++++------------------ 1 file changed, 12 insertions(+), 68 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index be83b955f..6fbf0a9bd 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -506,82 +506,26 @@ namespace Discord.Commands services = services ?? EmptyServiceProvider.Instance; var searchResult = Search(input); - if (!searchResult.IsSuccess) - { - await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); - return searchResult; - } - var commands = searchResult.Commands; - var preconditionResults = new Dictionary(); - - foreach (var match in commands) - { - preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); - } - - var successfulPreconditions = preconditionResults - .Where(x => x.Value.IsSuccess) - .ToArray(); + //Since ValidateAndGetBestMatch is deterministic on the return type, we can use pattern matching on the type for infering the code flow. + var (validationResult, commandMatch) = await ValidateAndGetBestMatch(searchResult, context, services, multiMatchHandling); - if (successfulPreconditions.Length == 0) + if(validationResult is SearchResult) { - //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); - - await _commandExecutedEvent.InvokeAsync(bestCandidate.Key.Command, context, bestCandidate.Value).ConfigureAwait(false); - return bestCandidate.Value; + await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); } - - //If we get this far, at least one precondition was successful. - - var parseResultsDict = new Dictionary(); - foreach (var pair in successfulPreconditions) + else if(validationResult is not ParseResult parseResult) { - var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); - - if (parseResult.Error == CommandError.MultipleMatches) - { - IReadOnlyList 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; + await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command,context,validationResult).ConfigureAwait(false); } - - //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(); - - if (successfulParses.Length == 0) + else { - //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); - - await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false); - return bestMatch.Value; + var result = await commandMatch.Value.Command.ExecuteAsync(context, parseResult, services).ConfigureAwait(false); + if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) + await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command, context, result); + return result; } - - //If we get this far, at least one parse was successful. Execute the most likely overload. - var chosenOverload = successfulParses[0]; - var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); - if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) - await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); - return result; + return validationResult; } // Calculates the 'score' of a command given a parse result From 56d16397f7da77df31b6e1e2a1fd1a318c081cfd Mon Sep 17 00:00:00 2001 From: roridev Date: Fri, 27 Nov 2020 18:42:23 -0300 Subject: [PATCH 4/7] Fixes Azure linux build failing due to a CS8652. --- src/Discord.Net.Commands/CommandService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 6fbf0a9bd..f54072811 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -514,17 +514,17 @@ namespace Discord.Commands { await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); } - else if(validationResult is not ParseResult parseResult) - { - await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command,context,validationResult).ConfigureAwait(false); - } - else + else if(validationResult is ParseResult parseResult) { var result = await commandMatch.Value.Command.ExecuteAsync(context, parseResult, services).ConfigureAwait(false); if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command, context, result); return result; } + else + { + await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command, context, validationResult).ConfigureAwait(false); + } return validationResult; } From d1b31c8f529033afda8d4a9e320788de0d034693 Mon Sep 17 00:00:00 2001 From: roridev Date: Thu, 25 Nov 2021 15:31:48 -0300 Subject: [PATCH 5/7] Add `MatchResult` --- .../Results/MatchResult.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/Discord.Net.Commands/Results/MatchResult.cs diff --git a/src/Discord.Net.Commands/Results/MatchResult.cs b/src/Discord.Net.Commands/Results/MatchResult.cs new file mode 100644 index 000000000..fb266efa6 --- /dev/null +++ b/src/Discord.Net.Commands/Results/MatchResult.cs @@ -0,0 +1,47 @@ +using System; + +namespace Discord.Commands +{ + public class MatchResult : IResult + { + /// + /// Gets the command that may have matched during the command execution. + /// + public CommandMatch? Match { get; } + + /// + /// Gets on which pipeline stage the command may have matched or failed. + /// + public IResult? Pipeline { get; } + + /// + public CommandError? Error { get; } + /// + public string ErrorReason { get; } + /// + public bool IsSuccess => !Error.HasValue; + + private MatchResult(CommandMatch? match, IResult? pipeline, CommandError? error, string errorReason) + { + Match = match; + Error = error; + Pipeline = pipeline; + ErrorReason = errorReason; + } + + public static MatchResult FromSuccess(CommandMatch match, IResult pipeline) + => new MatchResult(match,pipeline,null, null); + public static MatchResult FromError(CommandError error, string reason) + => new MatchResult(null,null,error, reason); + public static MatchResult FromError(Exception ex) + => FromError(CommandError.Exception, ex.Message); + public static MatchResult FromError(IResult result) + => new MatchResult(null, null,result.Error, result.ErrorReason); + public static MatchResult FromError(IResult pipeline, CommandError error, string reason) + => new MatchResult(null, pipeline, error, reason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + + } +} From a92ec56d884ffe41f74c1f6b9541c1d70d6d823d Mon Sep 17 00:00:00 2001 From: roridev Date: Thu, 25 Nov 2021 16:42:18 -0300 Subject: [PATCH 6/7] Add requested changes Changes: - Use IResult instead of Optional CommandMatch - Rework branching workflow --- src/Discord.Net.Commands/CommandService.cs | 55 ++++++++++++++-------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index f54072811..18f553559 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -507,27 +507,44 @@ namespace Discord.Commands var searchResult = Search(input); - //Since ValidateAndGetBestMatch is deterministic on the return type, we can use pattern matching on the type for infering the code flow. - var (validationResult, commandMatch) = await ValidateAndGetBestMatch(searchResult, context, services, multiMatchHandling); + var validationResult = await ValidateAndGetBestMatch(searchResult, context, services, multiMatchHandling); - if(validationResult is SearchResult) + if (validationResult is SearchResult result) { - await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); - } - else if(validationResult is ParseResult parseResult) - { - var result = await commandMatch.Value.Command.ExecuteAsync(context, parseResult, services).ConfigureAwait(false); - if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) - await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command, context, result); + await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, result).ConfigureAwait(false); return result; } - else + + if (validationResult is MatchResult matchResult) { - await _commandExecutedEvent.InvokeAsync(commandMatch.Value.Command, context, validationResult).ConfigureAwait(false); + return await handleCommandPipeline(matchResult, context, services); } + return validationResult; } + private async Task handleCommandPipeline(MatchResult matchResult, ICommandContext context, IServiceProvider services) + { + if (!matchResult.IsSuccess) + return matchResult; + + if (matchResult.Pipeline is ParseResult parseResult) + { + var executeResult = await matchResult.Match.Value.ExecuteAsync(context, parseResult, services); + + if (!executeResult.IsSuccess && !(executeResult is RuntimeResult || executeResult is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) + await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, executeResult); + return executeResult; + } + + if (matchResult.Pipeline is PreconditionResult preconditionResult) + { + await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, preconditionResult).ConfigureAwait(false); + } + + return matchResult; + } + // Calculates the 'score' of a command given a parse result float CalculateScore(CommandMatch match, ParseResult parseResult) { @@ -554,11 +571,11 @@ namespace Discord.Commands /// The service provider to be used on the command's dependency injection. /// The handling mode when multiple command matches are found. /// A task that represents the asynchronous validation operation. The task result contains the result of the - /// command validation and the command matched, if a match was found. - public async Task<(IResult, Optional)> ValidateAndGetBestMatch(SearchResult matches, ICommandContext context, IServiceProvider provider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + /// command validation as a or a if no matches were found. + public async Task ValidateAndGetBestMatch(SearchResult matches, ICommandContext context, IServiceProvider provider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { if (!matches.IsSuccess) - return (matches, Optional.Create()); + return matches; var commands = matches.Commands; var preconditionResults = new Dictionary(); @@ -574,11 +591,11 @@ namespace Discord.Commands 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, bestCandidate.Key); + return MatchResult.FromSuccess(bestCandidate.Key,bestCandidate.Value); } var parseResults = new Dictionary(); @@ -615,12 +632,12 @@ namespace Discord.Commands var bestMatch = parseResults .FirstOrDefault(x => !x.Value.IsSuccess); - return (bestMatch.Value, bestMatch.Key); + return MatchResult.FromSuccess(bestMatch.Key,bestMatch.Value); } var chosenOverload = successfulParses[0]; - return (chosenOverload.Value, chosenOverload.Key); + return MatchResult.FromSuccess(chosenOverload.Key,chosenOverload.Value); } protected virtual void Dispose(bool disposing) From adf3a9c459958254cd7241acf27a8c2afbeea822 Mon Sep 17 00:00:00 2001 From: roridev Date: Fri, 26 Nov 2021 09:26:53 -0300 Subject: [PATCH 7/7] Fix incorrect casing on `HandleCommandPipeline` --- src/Discord.Net.Commands/CommandService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 18f553559..7d03c4059 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -517,13 +517,13 @@ namespace Discord.Commands if (validationResult is MatchResult matchResult) { - return await handleCommandPipeline(matchResult, context, services); + return await HandleCommandPipeline(matchResult, context, services); } return validationResult; } - private async Task handleCommandPipeline(MatchResult matchResult, ICommandContext context, IServiceProvider services) + private async Task HandleCommandPipeline(MatchResult matchResult, ICommandContext context, IServiceProvider services) { if (!matchResult.IsSuccess) return matchResult;