Command parsing cleanup + preparation for suggestion & advanced caching

This commit is contained in:
themode 2021-03-10 01:21:33 +01:00
parent 94526b218f
commit bca2434cff
5 changed files with 238 additions and 148 deletions

View File

@ -15,7 +15,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
* Class responsible for parsing {@link Command}.
@ -124,11 +123,8 @@ public class CommandDispatcher {
return CommandResult.of(CommandResult.Type.UNKNOWN, commandName);
}
// Removes the command's name + the space after
String[] args = commandString.replaceFirst(Pattern.quote(commandName), "").trim().split(StringUtils.SPACE);
if (args.length == 1 && args[0].length() == 0) {
args = new String[0];
}
String[] args = new String[parts.length - 1];
System.arraycopy(parts, 1, args, 0, args.length);
CommandResult result = new CommandResult();
result.input = commandString;

View File

@ -0,0 +1,9 @@
package net.minestom.server.command.builder.parser;
import net.minestom.server.command.builder.arguments.Argument;
public class ArgumentQueryResult {
public Argument<?> argument;
public int start;
public String input;
}

View File

@ -2,6 +2,7 @@ package net.minestom.server.command.builder.parser;
import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap;
import net.minestom.server.command.builder.Arguments;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandSyntax;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
@ -9,108 +10,43 @@ import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
public class CommandParser {
public static void parse(@Nullable CommandSyntax syntax, @NotNull Argument<?>[] commandArguments, @NotNull String[] inputArguments,
@Nullable List<ValidSyntaxHolder> validSyntaxes,
@Nullable Int2ObjectRBTreeMap<CommandSuggestionHolder> syntaxesSuggestions) {
final Map<Argument<?>, Object> argsValues = new HashMap<>();
final Map<Argument<?>, Object> argumentValueMap = new HashMap<>();
boolean syntaxCorrect = true;
// The current index in the raw command string arguments
int splitIndex = 0;
int inputIndex = 0;
boolean useRemaining = false;
// Check the validity of the arguments...
for (int argCount = 0; argCount < commandArguments.length; argCount++) {
final boolean lastArgumentIteration = argCount + 1 == commandArguments.length;
final Argument<?> argument = commandArguments[argCount];
useRemaining = argument.useRemaining();
final boolean end = splitIndex == inputArguments.length;
if (end) // True if there is no input to analyze left
for (int argIndex = 0; argIndex < commandArguments.length; argIndex++) {
ArgumentResult argumentResult = validate(commandArguments, argIndex, inputArguments, inputIndex);
if (argumentResult == null) {
break;
// the parsed argument value, null if incorrect
Object parsedValue;
// the argument exception, null if the input is correct
ArgumentSyntaxException argumentSyntaxException = null;
// true if the arg is valid, false otherwise
boolean correct = false;
// the raw string representing the correct argument syntax
StringBuilder argValue = new StringBuilder();
if (useRemaining) {
final boolean hasArgs = inputArguments.length > splitIndex;
// Verify if there is any string part available
if (hasArgs) {
// Argument is supposed to take the rest of the command input
for (int i = splitIndex; i < inputArguments.length; i++) {
final String arg = inputArguments[i];
if (argValue.length() > 0)
argValue.append(StringUtils.SPACE);
argValue.append(arg);
}
final String argValueString = argValue.toString();
try {
parsedValue = argument.parse(argValueString);
correct = true;
argsValues.put(argument, parsedValue);
} catch (ArgumentSyntaxException exception) {
argumentSyntaxException = exception;
}
}
} else {
// Argument is either single-word or can accept optional delimited space(s)
for (int i = splitIndex; i < inputArguments.length; i++) {
final String rawArg = inputArguments[i];
argValue.append(rawArg);
final String argValueString = argValue.toString();
try {
parsedValue = argument.parse(argValueString);
// Prevent quitting the parsing too soon if the argument
// does not allow space
if (lastArgumentIteration && i + 1 < inputArguments.length) {
if (!argument.allowSpace())
break;
argValue.append(StringUtils.SPACE);
continue;
}
correct = true;
argsValues.put(argument, parsedValue);
splitIndex = i + 1;
break;
} catch (ArgumentSyntaxException exception) {
argumentSyntaxException = exception;
if (!argument.allowSpace())
break;
argValue.append(StringUtils.SPACE);
}
}
}
if (!correct) {
// Update local var
useRemaining = argumentResult.useRemaining;
inputIndex = argumentResult.inputIndex;
if (argumentResult.correct) {
argumentValueMap.put(argumentResult.argument, argumentResult.parsedValue);
} else {
// Argument is not correct, add it to the syntax suggestion with the number
// of correct argument(s) and do not check the next syntax argument
syntaxCorrect = false;
if (syntaxesSuggestions != null) {
CommandSuggestionHolder suggestionHolder = new CommandSuggestionHolder();
suggestionHolder.syntax = syntax;
suggestionHolder.argumentSyntaxException = argumentSyntaxException;
suggestionHolder.argIndex = argCount;
syntaxesSuggestions.put(argCount, suggestionHolder);
suggestionHolder.argumentSyntaxException = argumentResult.argumentSyntaxException;
suggestionHolder.argIndex = argIndex;
syntaxesSuggestions.put(argIndex, suggestionHolder);
}
break;
}
@ -118,11 +54,11 @@ public class CommandParser {
// Add the syntax to the list of valid syntaxes if correct
if (syntaxCorrect) {
if (commandArguments.length == argsValues.size() || useRemaining) {
if (commandArguments.length == argumentValueMap.size() || useRemaining) {
if (validSyntaxes != null) {
ValidSyntaxHolder validSyntaxHolder = new ValidSyntaxHolder();
validSyntaxHolder.syntax = syntax;
validSyntaxHolder.argumentsValue = argsValues;
validSyntaxHolder.argumentsValue = argumentValueMap;
validSyntaxes.add(validSyntaxHolder);
}
@ -179,4 +115,165 @@ public class CommandParser {
return finalSyntax;
}
public static ArgumentQueryResult findSuggestibleArgument(@NotNull Command command, String[] args, String rawCommandText) {
final Collection<CommandSyntax> syntaxes = command.getSyntaxes();
Int2ObjectRBTreeMap<ArgumentQueryResult> suggestions = new Int2ObjectRBTreeMap<>(Collections.reverseOrder());
for (CommandSyntax syntax : syntaxes) {
Argument<?>[] commandArguments = syntax.getArguments();
int inputIndex = 0;
ArgumentQueryResult maxArg = null;
int maxArgIndex = 0;
for (int argIndex = 0; argIndex < commandArguments.length; argIndex++) {
ArgumentResult argumentResult = validate(commandArguments, argIndex, args, inputIndex);
if (argumentResult == null) {
break;
}
// Update local var
inputIndex = argumentResult.inputIndex;
final Argument<?> argument = argumentResult.argument;
if (argument.hasSuggestion()) {
ArgumentQueryResult queryResult = new ArgumentQueryResult();
queryResult.argument = argumentResult.argument;
queryResult.input = argumentResult.rawArg;
queryResult.start = rawCommandText.length() - argumentResult.rawArg.length();
maxArg = queryResult;
maxArgIndex = argIndex;
}
if (!argumentResult.correct) {
break;
}
}
if (maxArg != null) {
suggestions.put(maxArgIndex, maxArg);
}
}
if (suggestions.isEmpty()) {
// No suggestion
return null;
}
final int max = suggestions.firstIntKey();
return suggestions.get(max);
}
@Nullable
private static ArgumentResult validate(@NotNull Argument<?>[] arguments, int argIndex,
@NotNull String[] inputArguments, int inputIndex) {
final Argument<?> argument = arguments[argIndex];
final boolean end = inputIndex == inputArguments.length;
if (end) // Stop if there is no input to analyze left
return null;
// the parsed argument value, null if incorrect
Object parsedValue = null;
// the argument exception, null if the input is correct
ArgumentSyntaxException argumentSyntaxException = null;
// true if the arg is valid, false otherwise
boolean correct = false;
// The raw string value of the argument
String rawArg = null;
if (argument.useRemaining()) {
final boolean hasArgs = inputArguments.length > inputIndex;
// Verify if there is any string part available
if (hasArgs) {
StringBuilder builder = new StringBuilder();
// Argument is supposed to take the rest of the command input
for (int i = inputIndex; i < inputArguments.length; i++) {
final String arg = inputArguments[i];
if (!builder.isEmpty())
builder.append(StringUtils.SPACE);
builder.append(arg);
}
rawArg = builder.toString();
try {
parsedValue = argument.parse(rawArg);
correct = true;
} catch (ArgumentSyntaxException exception) {
argumentSyntaxException = exception;
}
}
} else {
// Argument is either single-word or can accept optional delimited space(s)
StringBuilder builder = new StringBuilder();
for (int i = inputIndex; i < inputArguments.length; i++) {
builder.append(inputArguments[i]);
rawArg = builder.toString();
try {
parsedValue = argument.parse(rawArg);
// Prevent quitting the parsing too soon if the argument
// does not allow space
final boolean lastArgumentIteration = argIndex + 1 == arguments.length;
if (lastArgumentIteration && i + 1 < inputArguments.length) {
if (!argument.allowSpace())
break;
builder.append(StringUtils.SPACE);
continue;
}
correct = true;
inputIndex = i + 1;
break;
} catch (ArgumentSyntaxException exception) {
argumentSyntaxException = exception;
if (!argument.allowSpace()) {
// rawArg should be the remaining
for (int j = i + 1; j < inputArguments.length; j++) {
final String arg = inputArguments[j];
if (!builder.isEmpty())
builder.append(StringUtils.SPACE);
builder.append(arg);
}
rawArg = builder.toString();
break;
}
builder.append(StringUtils.SPACE);
}
}
}
ArgumentResult argumentResult = new ArgumentResult();
argumentResult.argument = argument;
argumentResult.correct = correct;
argumentResult.inputIndex = inputIndex;
argumentResult.argumentSyntaxException = argumentSyntaxException;
argumentResult.useRemaining = argument.useRemaining();
argumentResult.rawArg = rawArg;
argumentResult.parsedValue = parsedValue;
return argumentResult;
}
private static class ArgumentResult {
public Argument<?> argument;
public boolean correct;
public int inputIndex;
public ArgumentSyntaxException argumentSyntaxException;
public boolean useRemaining;
public String rawArg;
// If correct
public Object parsedValue;
}
}

View File

@ -1,17 +1,13 @@
package net.minestom.server.listener;
import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandManager;
import net.minestom.server.command.CommandProcessor;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandDispatcher;
import net.minestom.server.command.builder.CommandSyntax;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.command.builder.parser.ArgumentQueryResult;
import net.minestom.server.command.builder.parser.CommandParser;
import net.minestom.server.command.builder.parser.CommandSuggestionHolder;
import net.minestom.server.command.builder.parser.ValidSyntaxHolder;
import net.minestom.server.command.builder.suggestion.Suggestion;
import net.minestom.server.command.builder.suggestion.SuggestionCallback;
import net.minestom.server.entity.Player;
@ -19,10 +15,6 @@ import net.minestom.server.network.packet.client.play.ClientTabCompletePacket;
import net.minestom.server.network.packet.server.play.TabCompletePacket;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
public class TabCompleteListener {
@ -38,56 +30,44 @@ public class TabCompleteListener {
String commandName = split[0];
final CommandDispatcher commandDispatcher = MinecraftServer.getCommandManager().getDispatcher();
final Command command = commandDispatcher.findCommand(commandName);
final Collection<CommandSyntax> syntaxes = command.getSyntaxes();
List<ValidSyntaxHolder> validSyntaxes = new ArrayList<>(syntaxes.size());
Int2ObjectRBTreeMap<CommandSuggestionHolder> syntaxesSuggestions = new Int2ObjectRBTreeMap<>(Collections.reverseOrder());
String[] args = commandString.replaceFirst(Pattern.quote(commandName), "").trim().split(StringUtils.SPACE);
if (args.length == 1 && args[0].length() == 0) {
args = new String[0];
String[] args = new String[split.length - 1];
System.arraycopy(split, 1, args, 0, args.length);
final ArgumentQueryResult queryResult = CommandParser.findSuggestibleArgument(command, args, text);
if (queryResult == null) {
System.out.println("STOP");
return;
}
for (CommandSyntax syntax : syntaxes) {
CommandParser.parse(syntax, syntax.getArguments(), args, validSyntaxes, syntaxesSuggestions);
final Argument<?> argument = queryResult.argument;
final SuggestionCallback suggestionCallback = argument.suggestionCallback;
if (suggestionCallback != null) {
final int argumentLength = queryResult.input.length();
final int start = text.length() - argumentLength;
// TODO fix start
//Suggestion suggestion = new Suggestion(queryResult.start, queryResult.input.length());
Suggestion suggestion = new Suggestion(start, argumentLength);
suggestionCallback.apply(suggestion, commandString);
TabCompletePacket tabCompletePacket = new TabCompletePacket();
tabCompletePacket.transactionId = packet.transactionId;
tabCompletePacket.start = suggestion.getStart();
tabCompletePacket.length = suggestion.getLength();
tabCompletePacket.matches = suggestion.getEntries()
.stream()
.map(suggestionEntry -> {
TabCompletePacket.Match match = new TabCompletePacket.Match();
match.match = suggestionEntry.getEntry();
match.hasTooltip = suggestionEntry.getTooltip() != null;
match.tooltip = suggestionEntry.getTooltip();
return match;
}).toArray(TabCompletePacket.Match[]::new);
player.getPlayerConnection().sendPacket(tabCompletePacket);
}
if (!syntaxesSuggestions.isEmpty()) {
final int max = syntaxesSuggestions.firstIntKey();
final CommandSuggestionHolder suggestionHolder = syntaxesSuggestions.get(max);
final CommandSyntax syntax = suggestionHolder.syntax;
final ArgumentSyntaxException argumentSyntaxException = suggestionHolder.argumentSyntaxException;
final int argIndex = suggestionHolder.argIndex;
final Argument<?> argument = syntax.getArguments()[argIndex];
final SuggestionCallback suggestionCallback = argument.suggestionCallback;
if (suggestionCallback != null) {
final int argumentLength = argumentSyntaxException != null ? argumentSyntaxException.getInput().length() :
Integer.MAX_VALUE;
final int start = text.length() - argumentLength;
Suggestion suggestion = new Suggestion(start, argumentLength);
suggestionCallback.apply(suggestion, commandString);
TabCompletePacket tabCompletePacket = new TabCompletePacket();
tabCompletePacket.transactionId = packet.transactionId;
tabCompletePacket.start = suggestion.getStart();
tabCompletePacket.length = suggestion.getLength();
tabCompletePacket.matches = suggestion.getEntries()
.stream()
.map(suggestionEntry -> {
TabCompletePacket.Match match = new TabCompletePacket.Match();
match.match = suggestionEntry.getEntry();
match.hasTooltip = suggestionEntry.getTooltip() != null;
match.tooltip = suggestionEntry.getTooltip();
return match;
}).toArray(TabCompletePacket.Match[]::new);
player.getPlayerConnection().sendPacket(tabCompletePacket);
}
System.out.println("arg: " + argument.getClass());
}
System.out.println("test " + syntaxesSuggestions.size() + " " + validSyntaxes.size());
if (true)
return;
}

View File

@ -7,7 +7,7 @@ import net.minestom.server.command.builder.Arguments;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.suggestion.SuggestionEntry;
import static net.minestom.server.command.builder.arguments.ArgumentType.String;
import static net.minestom.server.command.builder.arguments.ArgumentType.*;
public class TestCommand extends Command {
@ -15,11 +15,19 @@ public class TestCommand extends Command {
super("testcmd");
setDefaultExecutor(this::usage);
addSyntax((sender, args) -> {
System.out.println("test: " + args.get("msg"));
}, String("msg").setSuggestionCallback((suggestion, input) -> {
var test1 = Integer("msg").setSuggestionCallback((suggestion, input) -> {
suggestion.addEntry(new SuggestionEntry(input, ColoredText.of(ChatColor.RED, "Hover")));
}));
});
var test2 = Word("msg2").setSuggestionCallback((suggestion, input) -> {
suggestion.addEntry(new SuggestionEntry(input, ColoredText.of(ChatColor.BRIGHT_GREEN, "GHRTEG")));
});
var test3 = String("msg3");
addSyntax((sender, args) -> {
System.out.println("COMMAND SYNTAX");
}, test3, test1);
}
private void usage(CommandSender sender, Arguments arguments) {