mirror of
https://github.com/Minestom/Minestom.git
synced 2025-01-04 23:47:59 +01:00
hollow-cube/command-parser-fixes (#54)
* Rewrite CommandParserImpl recursively (fix #1327) * Fix for tests: CommandManagerTest, CommandParseTest * Make attributes final inside Chain * fix #1295 with argument type priority * Don't include command name in list of arguments * Add test for #1327 * Add test for #1295 * Fix suggestions with bad syntax * Fix #1916 * Add test for #1916 * add failing test * mess with arg order * Fix `GraphImpl` causing syntax order issues --------- Co-authored-by: Spanner <spanner77@protonmail.com>
This commit is contained in:
parent
0e8151150a
commit
e9d0098418
@ -58,6 +58,7 @@ public class Main {
|
|||||||
commandManager.register(new RedirectTestCommand());
|
commandManager.register(new RedirectTestCommand());
|
||||||
commandManager.register(new DisplayCommand());
|
commandManager.register(new DisplayCommand());
|
||||||
commandManager.register(new NotificationCommand());
|
commandManager.register(new NotificationCommand());
|
||||||
|
commandManager.register(new TestCommand2());
|
||||||
|
|
||||||
commandManager.setUnknownCommandCallback((sender, command) -> sender.sendMessage(Component.text("Unknown command", NamedTextColor.RED)));
|
commandManager.setUnknownCommandCallback((sender, command) -> sender.sendMessage(Component.text("Unknown command", NamedTextColor.RED)));
|
||||||
|
|
||||||
|
@ -136,6 +136,14 @@ public class PlayerInit {
|
|||||||
})
|
})
|
||||||
.addListener(PlayerBlockPlaceEvent.class, event -> {
|
.addListener(PlayerBlockPlaceEvent.class, event -> {
|
||||||
// event.setDoBlockUpdates(false);
|
// event.setDoBlockUpdates(false);
|
||||||
|
})
|
||||||
|
.addListener(PlayerBlockInteractEvent.class, event -> {
|
||||||
|
var block = event.getBlock();
|
||||||
|
var rawOpenProp = block.getProperty("open");
|
||||||
|
if (rawOpenProp == null) return;
|
||||||
|
|
||||||
|
block = block.withProperty("open", String.valueOf(!Boolean.parseBoolean(rawOpenProp)));
|
||||||
|
event.getInstance().setBlock(event.getBlockPosition(), block);
|
||||||
});
|
});
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package net.minestom.demo.commands;
|
||||||
|
|
||||||
|
import net.minestom.server.command.builder.Command;
|
||||||
|
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||||
|
|
||||||
|
public class TestCommand2 extends Command {
|
||||||
|
public TestCommand2() {
|
||||||
|
super("test2");
|
||||||
|
|
||||||
|
var argA = ArgumentType.String("a");
|
||||||
|
var argB = ArgumentType.String("b");
|
||||||
|
|
||||||
|
addSyntax((sender, context) -> {
|
||||||
|
sender.sendMessage("a only");
|
||||||
|
}, argA);
|
||||||
|
addSyntax((sender, context) -> {
|
||||||
|
sender.sendMessage("a and b");
|
||||||
|
}, argB, argA);
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
final class CommandParserImpl implements CommandParser {
|
final class CommandParserImpl implements CommandParser {
|
||||||
@ -27,6 +26,7 @@ final class CommandParserImpl implements CommandParser {
|
|||||||
|
|
||||||
static final class Chain {
|
static final class Chain {
|
||||||
CommandExecutor defaultExecutor = null;
|
CommandExecutor defaultExecutor = null;
|
||||||
|
SuggestionCallback suggestionCallback = null;
|
||||||
final ArrayDeque<NodeResult> nodeResults = new ArrayDeque<>();
|
final ArrayDeque<NodeResult> nodeResults = new ArrayDeque<>();
|
||||||
final List<CommandCondition> conditions = new ArrayList<>();
|
final List<CommandCondition> conditions = new ArrayList<>();
|
||||||
final List<CommandExecutor> globalListeners = new ArrayList<>();
|
final List<CommandExecutor> globalListeners = new ArrayList<>();
|
||||||
@ -60,81 +60,60 @@ final class CommandParserImpl implements CommandParser {
|
|||||||
return (sender, context) -> globalListeners.forEach(x -> x.apply(sender, context));
|
return (sender, context) -> globalListeners.forEach(x -> x.apply(sender, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
SuggestionCallback extractSuggestionCallback() {
|
|
||||||
return nodeResults.peekLast().callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, ArgumentResult<Object>> collectArguments() {
|
Map<String, ArgumentResult<Object>> collectArguments() {
|
||||||
return nodeResults.stream()
|
return nodeResults.stream()
|
||||||
.skip(1) // skip root
|
.skip(2) // skip root node and command
|
||||||
.collect(Collectors.toUnmodifiableMap(NodeResult::name, NodeResult::argumentResult));
|
.collect(Collectors.toUnmodifiableMap(NodeResult::name, NodeResult::argumentResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Argument<?>> getArgs() {
|
List<Argument<?>> getArgs() {
|
||||||
return nodeResults.stream().map(x -> x.node.argument()).collect(Collectors.toList());
|
return nodeResults.stream().map(x -> x.node.argument()).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Chain() {}
|
||||||
|
|
||||||
|
Chain(CommandExecutor defaultExecutor,
|
||||||
|
SuggestionCallback suggestionCallback,
|
||||||
|
ArrayDeque<NodeResult> nodeResults,
|
||||||
|
List<CommandCondition> conditions,
|
||||||
|
List<CommandExecutor> globalListeners) {
|
||||||
|
this.defaultExecutor = defaultExecutor;
|
||||||
|
this.suggestionCallback = suggestionCallback;
|
||||||
|
this.nodeResults.addAll(nodeResults);
|
||||||
|
this.conditions.addAll(conditions);
|
||||||
|
this.globalListeners.addAll(globalListeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
Chain fork() {
|
||||||
|
return new Chain(defaultExecutor, suggestionCallback, nodeResults, conditions, globalListeners);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull CommandParser.Result parse(@NotNull CommandSender sender, @NotNull Graph graph, @NotNull String input) {
|
public @NotNull CommandParser.Result parse(@NotNull CommandSender sender, @NotNull Graph graph, @NotNull String input) {
|
||||||
final CommandStringReader reader = new CommandStringReader(input);
|
final CommandStringReader reader = new CommandStringReader(input);
|
||||||
final Chain chain = new Chain();
|
Chain chain = new Chain();
|
||||||
// Read from input
|
|
||||||
NodeResult result;
|
|
||||||
Node parent = graph.root();
|
Node parent = graph.root();
|
||||||
while ((result = parseChild(sender, parent, reader)) != null) {
|
|
||||||
chain.append(result);
|
NodeResult result = parseNode(sender, parent, chain, reader);
|
||||||
if (result.argumentResult instanceof ArgumentResult.SyntaxError<?> e) {
|
chain = result.chain;
|
||||||
// Syntax error stop at this arg
|
if (result.argumentResult instanceof ArgumentResult.Success<?>) {
|
||||||
final ArgumentCallback argumentCallback = parent.argument().getCallback();
|
NodeResult lastNodeResult = chain.nodeResults.peekLast();
|
||||||
if (argumentCallback == null && chain.defaultExecutor != null) {
|
Node lastNode = lastNodeResult.node;
|
||||||
return ValidCommand.defaultExecutor(input, chain);
|
|
||||||
} else {
|
CommandExecutor executor = nullSafeGetter(lastNode.execution(), Graph.Execution::executor);
|
||||||
return new InvalidCommand(input, chain.mergedConditions(),
|
if (executor != null) return ValidCommand.executor(input, chain, executor);
|
||||||
argumentCallback, e, chain.collectArguments(), chain.mergedGlobalExecutors(),
|
|
||||||
chain.extractSuggestionCallback(), chain.getArgs());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parent = result.node;
|
|
||||||
}
|
}
|
||||||
// Check children for arguments with default values
|
// If here, then the command failed or didn't have an executor
|
||||||
do {
|
|
||||||
Node tmp = parent;
|
// Look for a default executor, or give up if we got nowhere
|
||||||
parent = null;
|
NodeResult lastNode = chain.nodeResults.peekLast();
|
||||||
for (Node child : tmp.next()) {
|
if (lastNode.node.equals(parent)) return UnknownCommandResult.INSTANCE;
|
||||||
final Argument<?> argument = child.argument();
|
if (chain.defaultExecutor != null) {
|
||||||
final Function<CommandSender, ?> defaultSupplier = argument.getDefaultValue();
|
return ValidCommand.defaultExecutor(input, chain);
|
||||||
if (defaultSupplier != null) {
|
|
||||||
final Object value = defaultSupplier.apply(sender);
|
|
||||||
final ArgumentResult<Object> argumentResult = new ArgumentResult.Success<>(value, "");
|
|
||||||
chain.append(new NodeResult(child, argumentResult, argument.getSuggestionCallback()));
|
|
||||||
parent = child;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (parent != null);
|
|
||||||
// Check if any syntax has been found
|
|
||||||
final NodeResult lastNode = chain.nodeResults.peekLast();
|
|
||||||
if (lastNode == null) return UnknownCommandResult.INSTANCE;
|
|
||||||
// Verify syntax(s)
|
|
||||||
final CommandExecutor executor = nullSafeGetter(lastNode.node().execution(), Graph.Execution::executor);
|
|
||||||
if (executor == null) {
|
|
||||||
// Syntax error
|
|
||||||
if (chain.defaultExecutor != null) {
|
|
||||||
return ValidCommand.defaultExecutor(input, chain);
|
|
||||||
} else {
|
|
||||||
return InvalidCommand.invalid(input, chain);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (reader.hasRemaining()) {
|
|
||||||
// Command had trailing data
|
return InvalidCommand.invalid(input, chain);
|
||||||
if (chain.defaultExecutor != null) {
|
|
||||||
return ValidCommand.defaultExecutor(input, chain);
|
|
||||||
} else {
|
|
||||||
return InvalidCommand.invalid(input, chain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ValidCommand.executor(input, chain, executor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Contract("null, _ -> null; !null, null -> fail; !null, !null -> _")
|
@Contract("null, _ -> null; !null, null -> fail; !null, !null -> _")
|
||||||
@ -142,32 +121,90 @@ final class CommandParserImpl implements CommandParser {
|
|||||||
return obj == null ? null : getter.apply(obj);
|
return obj == null ? null : getter.apply(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NodeResult parseChild(@NotNull CommandSender sender, Node parent, CommandStringReader reader) {
|
private static NodeResult parseNode(@NotNull CommandSender sender, Node node, Chain chain, CommandStringReader reader) {
|
||||||
if (!reader.hasRemaining()) return null;
|
chain = chain.fork();
|
||||||
for (Node child : parent.next()) {
|
Argument<?> argument = node.argument();
|
||||||
final Argument<?> argument = child.argument();
|
int start = reader.cursor();
|
||||||
final int start = reader.cursor();
|
|
||||||
final ArgumentResult<?> parse = parse(sender, argument, reader);
|
if (reader.hasRemaining()) {
|
||||||
if (parse instanceof ArgumentResult.Success<?> success) {
|
ArgumentResult<?> result = parseArgument(sender, argument, reader);
|
||||||
return new NodeResult(child, (ArgumentResult<Object>) success,
|
SuggestionCallback suggestionCallback = argument.getSuggestionCallback();
|
||||||
argument.getSuggestionCallback());
|
NodeResult nodeResult = new NodeResult(node, chain, (ArgumentResult<Object>) result, suggestionCallback);
|
||||||
} else if (parse instanceof ArgumentResult.SyntaxError<?> syntaxError) {
|
chain.append(nodeResult);
|
||||||
return new NodeResult(child, (ArgumentResult<Object>) syntaxError,
|
if (suggestionCallback != null) chain.suggestionCallback = suggestionCallback;
|
||||||
argument.getSuggestionCallback());
|
if (chain.nodeResults.size() == 1) { // If this is the root node (usually "Literal<>")
|
||||||
|
reader.cursor(start);
|
||||||
} else {
|
} else {
|
||||||
// Reset cursor & try next
|
if (!(result instanceof ArgumentResult.Success<?>)) {
|
||||||
|
reader.cursor(start);
|
||||||
|
return nodeResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Nothing left, yet we're still being asked to parse? There must be defaults then
|
||||||
|
Function<CommandSender, ?> defaultSupplier = node.argument().getDefaultValue();
|
||||||
|
if (defaultSupplier != null) {
|
||||||
|
Object value = defaultSupplier.apply(sender);
|
||||||
|
ArgumentResult<Object> argumentResult = new ArgumentResult.Success<>(value, "");
|
||||||
|
chain.append(new NodeResult(node, chain, argumentResult, argument.getSuggestionCallback()));
|
||||||
|
// Add the default to the chain, and then carry on dealing with this node
|
||||||
|
} else {
|
||||||
|
// Still being asked to parse yet there's nothing left, syntax error.
|
||||||
|
return new NodeResult(
|
||||||
|
node,
|
||||||
|
chain,
|
||||||
|
new ArgumentResult.SyntaxError<>("Not enough arguments","",-1),
|
||||||
|
argument.getSuggestionCallback()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Successfully matched this node's argument
|
||||||
|
start = reader.cursor();
|
||||||
|
if (!reader.hasRemaining()) start--; // This is needed otherwise the reader throws an AssertionError
|
||||||
|
|
||||||
|
NodeResult error = null;
|
||||||
|
for (Node child : node.next()) {
|
||||||
|
NodeResult childResult = parseNode(sender, child, chain, reader);
|
||||||
|
if (childResult.argumentResult instanceof ArgumentResult.Success<Object>) {
|
||||||
|
// Assume that there is only one successful node for a given chain of arguments
|
||||||
|
return childResult;
|
||||||
|
} else {
|
||||||
|
if (error == null) {
|
||||||
|
// If this is the base argument (e.g. "teleport" in /teleport) then
|
||||||
|
// do not report an argument to be incompatible, since the more
|
||||||
|
// correct thing would be to say that the command is unknown.
|
||||||
|
if (!(childResult.chain.nodeResults.size() == 2 && childResult.argumentResult instanceof ArgumentResult.IncompatibleType<?>)) {
|
||||||
|
error = childResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
reader.cursor(start);
|
reader.cursor(start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (Node node : parent.next()) {
|
// None were successful. Either incompatible types, or syntax error. It doesn't matter to us, though
|
||||||
final SuggestionCallback suggestionCallback = node.argument().getSuggestionCallback();
|
|
||||||
if (suggestionCallback != null) {
|
// Try to execute this node
|
||||||
return new NodeResult(parent,
|
CommandExecutor executor = nullSafeGetter(node.execution(), Graph.Execution::executor);
|
||||||
new ArgumentResult.SyntaxError<>("None of the arguments were compatible, but a suggestion callback was found.", "", -1),
|
if (executor == null) {
|
||||||
suggestionCallback);
|
// Stuck here with no executor
|
||||||
|
if (error != null) {
|
||||||
|
return error;
|
||||||
|
} else {
|
||||||
|
return chain.nodeResults.peekLast();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
if (reader.hasRemaining()) {
|
||||||
|
// Trailing data is a syntax error
|
||||||
|
return new NodeResult(
|
||||||
|
node,
|
||||||
|
chain,
|
||||||
|
new ArgumentResult.SyntaxError<>("Command has trailing data", "", -1),
|
||||||
|
argument.getSuggestionCallback()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command was successful!
|
||||||
|
return chain.nodeResults.peekLast();
|
||||||
}
|
}
|
||||||
|
|
||||||
record UnknownCommandResult() implements Result.UnknownCommand {
|
record UnknownCommandResult() implements Result.UnknownCommand {
|
||||||
@ -222,7 +259,7 @@ final class CommandParserImpl implements CommandParser {
|
|||||||
return new InvalidCommand(input, chain.mergedConditions(),
|
return new InvalidCommand(input, chain.mergedConditions(),
|
||||||
null/*todo command syntax callback*/,
|
null/*todo command syntax callback*/,
|
||||||
new ArgumentResult.SyntaxError<>("Command has trailing data.", null, -1),
|
new ArgumentResult.SyntaxError<>("Command has trailing data.", null, -1),
|
||||||
chain.collectArguments(), chain.mergedGlobalExecutors(), chain.extractSuggestionCallback(), chain.getArgs());
|
chain.collectArguments(), chain.mergedGlobalExecutors(), chain.suggestionCallback, chain.getArgs());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -238,12 +275,12 @@ final class CommandParserImpl implements CommandParser {
|
|||||||
|
|
||||||
static ValidCommand defaultExecutor(String input, Chain chain) {
|
static ValidCommand defaultExecutor(String input, Chain chain) {
|
||||||
return new ValidCommand(input, chain.mergedConditions(), chain.defaultExecutor, chain.collectArguments(),
|
return new ValidCommand(input, chain.mergedConditions(), chain.defaultExecutor, chain.collectArguments(),
|
||||||
chain.mergedGlobalExecutors(), chain.extractSuggestionCallback(), chain.getArgs());
|
chain.mergedGlobalExecutors(), chain.suggestionCallback, chain.getArgs());
|
||||||
}
|
}
|
||||||
|
|
||||||
static ValidCommand executor(String input, Chain chain, CommandExecutor executor) {
|
static ValidCommand executor(String input, Chain chain, CommandExecutor executor) {
|
||||||
return new ValidCommand(input, chain.mergedConditions(), executor, chain.collectArguments(), chain.mergedGlobalExecutors(),
|
return new ValidCommand(input, chain.mergedConditions(), executor, chain.collectArguments(), chain.mergedGlobalExecutors(),
|
||||||
chain.extractSuggestionCallback(), chain.getArgs());
|
chain.suggestionCallback, chain.getArgs());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -321,7 +358,7 @@ final class CommandParserImpl implements CommandParser {
|
|||||||
static final ExecutableCommand.Result INVALID_SYNTAX = new ExecutionResultImpl(Type.INVALID_SYNTAX, null);
|
static final ExecutableCommand.Result INVALID_SYNTAX = new ExecutionResultImpl(Type.INVALID_SYNTAX, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record NodeResult(Node node, ArgumentResult<Object> argumentResult, SuggestionCallback callback) {
|
private record NodeResult(Node node, Chain chain, ArgumentResult<Object> argumentResult, SuggestionCallback callback) {
|
||||||
public String name() {
|
public String name() {
|
||||||
return node.argument().getId();
|
return node.argument().getId();
|
||||||
}
|
}
|
||||||
@ -372,7 +409,7 @@ final class CommandParserImpl implements CommandParser {
|
|||||||
|
|
||||||
// ARGUMENT
|
// ARGUMENT
|
||||||
|
|
||||||
private static <T> ArgumentResult<T> parse(@NotNull CommandSender sender, Argument<T> argument, CommandStringReader reader) {
|
private static <T> ArgumentResult<T> parseArgument(@NotNull CommandSender sender, Argument<T> argument, CommandStringReader reader) {
|
||||||
// Handle specific type without loop
|
// Handle specific type without loop
|
||||||
try {
|
try {
|
||||||
// Single word argument
|
// Single word argument
|
||||||
|
@ -4,6 +4,8 @@ import net.minestom.server.command.builder.Command;
|
|||||||
import net.minestom.server.command.builder.CommandExecutor;
|
import net.minestom.server.command.builder.CommandExecutor;
|
||||||
import net.minestom.server.command.builder.CommandSyntax;
|
import net.minestom.server.command.builder.CommandSyntax;
|
||||||
import net.minestom.server.command.builder.arguments.Argument;
|
import net.minestom.server.command.builder.arguments.Argument;
|
||||||
|
import net.minestom.server.command.builder.arguments.ArgumentCommand;
|
||||||
|
import net.minestom.server.command.builder.arguments.ArgumentLiteral;
|
||||||
import net.minestom.server.command.builder.condition.CommandCondition;
|
import net.minestom.server.command.builder.condition.CommandCondition;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
@ -62,6 +64,12 @@ record GraphImpl(NodeImpl root) implements Graph {
|
|||||||
}
|
}
|
||||||
|
|
||||||
record NodeImpl(Argument<?> argument, ExecutionImpl execution, List<Graph.Node> next) implements Graph.Node {
|
record NodeImpl(Argument<?> argument, ExecutionImpl execution, List<Graph.Node> next) implements Graph.Node {
|
||||||
|
NodeImpl(Argument<?> argument, ExecutionImpl execution, List<Graph.Node> next) {
|
||||||
|
this.argument = argument;
|
||||||
|
this.execution = execution;
|
||||||
|
this.next = next.stream().sorted(nodePriority).toList();
|
||||||
|
}
|
||||||
|
|
||||||
static NodeImpl fromBuilder(BuilderImpl builder) {
|
static NodeImpl fromBuilder(BuilderImpl builder) {
|
||||||
final List<BuilderImpl> children = builder.children;
|
final List<BuilderImpl> children = builder.children;
|
||||||
Node[] nodes = new NodeImpl[children.size()];
|
Node[] nodes = new NodeImpl[children.size()];
|
||||||
@ -76,6 +84,17 @@ record GraphImpl(NodeImpl root) implements Graph {
|
|||||||
static NodeImpl rootCommands(Collection<Command> commands) {
|
static NodeImpl rootCommands(Collection<Command> commands) {
|
||||||
return ConversionNode.rootConv(commands).toNode();
|
return ConversionNode.rootConv(commands).toNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final java.util.Comparator<Node> nodePriority = (node1, node2) -> {
|
||||||
|
int node1Value = argumentValue(node1.argument());
|
||||||
|
int node2Value = argumentValue(node2.argument());
|
||||||
|
return Integer.compare(node1Value, node2Value);
|
||||||
|
};
|
||||||
|
private static int argumentValue(Argument<?> argument) {
|
||||||
|
if (argument.getClass() == ArgumentCommand.class) return -3000;
|
||||||
|
if (argument.getClass() == ArgumentLiteral.class) return -2000;
|
||||||
|
return -1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
record ExecutionImpl(Predicate<CommandSender> predicate,
|
record ExecutionImpl(Predicate<CommandSender> predicate,
|
||||||
@ -113,8 +132,17 @@ record GraphImpl(NodeImpl root) implements Graph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ConversionNode(Argument<?> argument, ExecutionImpl execution,
|
private static final class ConversionNode {
|
||||||
Map<Argument<?>, ConversionNode> nextMap) {
|
final Argument<?> argument;
|
||||||
|
ExecutionImpl execution;
|
||||||
|
final Map<Argument<?>, ConversionNode> nextMap;
|
||||||
|
|
||||||
|
public ConversionNode(Argument<?> argument, ExecutionImpl execution, Map<Argument<?>, ConversionNode> nextMap) {
|
||||||
|
this.argument = argument;
|
||||||
|
this.execution = execution;
|
||||||
|
this.nextMap = nextMap;
|
||||||
|
}
|
||||||
|
|
||||||
ConversionNode(Argument<?> argument, ExecutionImpl execution) {
|
ConversionNode(Argument<?> argument, ExecutionImpl execution) {
|
||||||
this(argument, execution, new LinkedHashMap<>());
|
this(argument, execution, new LinkedHashMap<>());
|
||||||
}
|
}
|
||||||
@ -133,10 +161,9 @@ record GraphImpl(NodeImpl root) implements Graph {
|
|||||||
ConversionNode syntaxNode = root;
|
ConversionNode syntaxNode = root;
|
||||||
for (Argument<?> arg : syntax.getArguments()) {
|
for (Argument<?> arg : syntax.getArguments()) {
|
||||||
boolean last = arg == syntax.getArguments()[syntax.getArguments().length - 1];
|
boolean last = arg == syntax.getArguments()[syntax.getArguments().length - 1];
|
||||||
syntaxNode = syntaxNode.nextMap.computeIfAbsent(arg, argument -> {
|
var ex = last ? ExecutionImpl.fromSyntax(syntax) : null;
|
||||||
var ex = last ? ExecutionImpl.fromSyntax(syntax) : null;
|
syntaxNode = syntaxNode.nextMap.computeIfAbsent(arg, argument -> new ConversionNode(argument, ex));
|
||||||
return new ConversionNode(argument, ex);
|
if (syntaxNode.execution == null) syntaxNode.execution = ex;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Subcommands
|
// Subcommands
|
||||||
@ -154,6 +181,7 @@ record GraphImpl(NodeImpl root) implements Graph {
|
|||||||
}
|
}
|
||||||
return new ConversionNode(Literal(""), null, next);
|
return new ConversionNode(Literal(""), null, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Argument<String> commandToArgument(Command command) {
|
static Argument<String> commandToArgument(Command command) {
|
||||||
|
@ -21,7 +21,6 @@ public class EntityActionListener {
|
|||||||
|
|
||||||
private static void setSneaking(Player player, boolean sneaking) {
|
private static void setSneaking(Player player, boolean sneaking) {
|
||||||
boolean oldState = player.isSneaking();
|
boolean oldState = player.isSneaking();
|
||||||
|
|
||||||
player.setSneaking(sneaking);
|
player.setSneaking(sneaking);
|
||||||
|
|
||||||
if (oldState != sneaking) {
|
if (oldState != sneaking) {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package net.minestom.server.command;
|
package net.minestom.server.command;
|
||||||
|
|
||||||
import net.minestom.server.command.builder.Command;
|
import net.minestom.server.command.builder.Command;
|
||||||
|
import net.minestom.server.command.builder.CommandResult;
|
||||||
|
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||||
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
|
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
@ -45,6 +48,62 @@ public class CommandManagerTest {
|
|||||||
assertTrue(check.get());
|
assertTrue(check.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSharedArgumentSyntaxABFirst() {
|
||||||
|
var manager = new CommandManager();
|
||||||
|
|
||||||
|
var checkA = new AtomicBoolean(false);
|
||||||
|
var checkAB = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
var cmd = new Command("cmd");
|
||||||
|
var argA = ArgumentType.String("a");
|
||||||
|
var argB = ArgumentType.String("b");
|
||||||
|
cmd.addSyntax((sender, context) -> checkAB.set(true), argA, argB);
|
||||||
|
cmd.addSyntax((sender, context) -> checkA.set(true), argA);
|
||||||
|
manager.register(cmd);
|
||||||
|
|
||||||
|
var result = manager.executeServerCommand("cmd a");
|
||||||
|
assertEquals(CommandResult.Type.SUCCESS, result.getType());
|
||||||
|
assertTrue(checkA.get());
|
||||||
|
assertFalse(checkAB.get());
|
||||||
|
|
||||||
|
checkA.set(false); // these should be different tests
|
||||||
|
checkAB.set(false);
|
||||||
|
|
||||||
|
result = manager.executeServerCommand("cmd a b");
|
||||||
|
assertEquals(CommandResult.Type.SUCCESS, result.getType());
|
||||||
|
assertFalse(checkA.get());
|
||||||
|
assertTrue(checkAB.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSharedArgumentSyntaxAFirst() {
|
||||||
|
var manager = new CommandManager();
|
||||||
|
|
||||||
|
var checkA = new AtomicBoolean(false);
|
||||||
|
var checkAB = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
var cmd = new Command("cmd");
|
||||||
|
var argA = ArgumentType.String("a");
|
||||||
|
var argB = ArgumentType.String("b");
|
||||||
|
cmd.addSyntax((sender, context) -> checkA.set(true), argA);
|
||||||
|
cmd.addSyntax((sender, context) -> checkAB.set(true), argA, argB);
|
||||||
|
manager.register(cmd);
|
||||||
|
|
||||||
|
var result = manager.executeServerCommand("cmd a");
|
||||||
|
assertEquals(CommandResult.Type.SUCCESS, result.getType());
|
||||||
|
assertTrue(checkA.get());
|
||||||
|
assertFalse(checkAB.get());
|
||||||
|
|
||||||
|
checkA.set(false); // these should be different tests
|
||||||
|
checkAB.set(false);
|
||||||
|
|
||||||
|
result = manager.executeServerCommand("cmd a b");
|
||||||
|
assertEquals(CommandResult.Type.SUCCESS, result.getType());
|
||||||
|
assertFalse(checkA.get());
|
||||||
|
assertTrue(checkAB.get());
|
||||||
|
}
|
||||||
|
|
||||||
private static void assertNodeEquals(DeclareCommandsPacket.Node node, byte flags, int[] children, int redirectedNode,
|
private static void assertNodeEquals(DeclareCommandsPacket.Node node, byte flags, int[] children, int redirectedNode,
|
||||||
String name, String parser, byte[] properties, String suggestionsType) {
|
String name, String parser, byte[] properties, String suggestionsType) {
|
||||||
assertEquals(flags, node.flags);
|
assertEquals(flags, node.flags);
|
||||||
|
@ -12,6 +12,8 @@ import org.junit.jupiter.api.Test;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static net.minestom.server.command.builder.arguments.ArgumentType.Literal;
|
import static net.minestom.server.command.builder.arguments.ArgumentType.Literal;
|
||||||
|
import static net.minestom.server.command.builder.arguments.ArgumentType.Word;
|
||||||
|
import static net.minestom.server.command.builder.arguments.ArgumentType.Integer;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
@ -48,4 +50,29 @@ public class CommandSuggestionIntegrationTest {
|
|||||||
assertEquals(List.of(new TabCompletePacket.Match("test1", null)), tabCompletePacket.matches());
|
assertEquals(List.of(new TabCompletePacket.Match("test1", null)), tabCompletePacket.matches());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void suggestionWithDefaults(Env env) {
|
||||||
|
var instance = env.createFlatInstance();
|
||||||
|
var connection = env.createConnection();
|
||||||
|
var player = connection.connect(instance, new Pos(0, 42, 0)).join();
|
||||||
|
|
||||||
|
var suggestArg = Word("suggestArg").setSuggestionCallback(
|
||||||
|
(sender, context, suggestion) -> suggestion.addEntry(new SuggestionEntry("suggestion"))
|
||||||
|
);
|
||||||
|
var defaultArg = Integer("defaultArg").setDefaultValue(123);
|
||||||
|
|
||||||
|
var command = new Command("foo");
|
||||||
|
|
||||||
|
command.addSyntax((sender,context)->{}, suggestArg, defaultArg);
|
||||||
|
env.process().command().register(command);
|
||||||
|
|
||||||
|
var listener = connection.trackIncoming(TabCompletePacket.class);
|
||||||
|
player.addPacketToQueue(new ClientTabCompletePacket(1, "foo 1"));
|
||||||
|
player.interpretPacketQueue();
|
||||||
|
|
||||||
|
listener.assertSingle(tabCompletePacket -> {
|
||||||
|
assertEquals(List.of(new TabCompletePacket.Match("suggestion", null)), tabCompletePacket.matches());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import java.lang.String;
|
import java.lang.String;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static net.minestom.server.command.builder.arguments.ArgumentType.Float;
|
import static net.minestom.server.command.builder.arguments.ArgumentType.Float;
|
||||||
@ -27,6 +28,25 @@ public class CommandSyntaxMultiTest {
|
|||||||
assertSyntax(args, "float 5.5", ExpectedExecution.SECOND_SYNTAX, Map.of("float", "float", "number", 5.5f));
|
assertSyntax(args, "float 5.5", ExpectedExecution.SECOND_SYNTAX, Map.of("float", "float", "number", 5.5f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void argPriority() {
|
||||||
|
List<List<Argument<?>>> args = List.of(
|
||||||
|
List.of(Word("word")),
|
||||||
|
List.of(Literal("literal"))
|
||||||
|
);
|
||||||
|
assertSyntax(args, "literal", ExpectedExecution.SECOND_SYNTAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void similarArgs() {
|
||||||
|
List<List<Argument<?>>> args = List.of(
|
||||||
|
List.of(Word("a")),
|
||||||
|
List.of(Word("b"), Word("a"))
|
||||||
|
);
|
||||||
|
assertSyntax(args, "baz", ExpectedExecution.FIRST_SYNTAX);
|
||||||
|
assertSyntax(args, "bar baz", ExpectedExecution.SECOND_SYNTAX);
|
||||||
|
}
|
||||||
|
|
||||||
private static void assertSyntax(List<List<Argument<?>>> args, String input, ExpectedExecution expectedExecution, Map<String, Object> expectedValues) {
|
private static void assertSyntax(List<List<Argument<?>>> args, String input, ExpectedExecution expectedExecution, Map<String, Object> expectedValues) {
|
||||||
final String commandName = "name";
|
final String commandName = "name";
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user