mirror of
https://github.com/Minestom/Minestom.git
synced 2025-03-02 11:21:15 +01:00
New command parser (#1235)
This commit is contained in:
parent
58a7a59036
commit
9e5de35fa7
@ -0,0 +1,105 @@
|
||||
package net.minestom.server.command;
|
||||
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import org.openjdk.jmh.annotations.*;
|
||||
import org.openjdk.jmh.infra.Blackhole;
|
||||
|
||||
import java.lang.String;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static net.minestom.server.command.builder.arguments.ArgumentType.Double;
|
||||
import static net.minestom.server.command.builder.arguments.ArgumentType.Float;
|
||||
import static net.minestom.server.command.builder.arguments.ArgumentType.Integer;
|
||||
import static net.minestom.server.command.builder.arguments.ArgumentType.Long;
|
||||
import static net.minestom.server.command.builder.arguments.ArgumentType.*;
|
||||
|
||||
@BenchmarkMode(Mode.AverageTime)
|
||||
@State(Scope.Benchmark)
|
||||
@OutputTimeUnit(TimeUnit.NANOSECONDS)
|
||||
@Fork(1)
|
||||
@Warmup(time = 2, iterations = 3)
|
||||
@Measurement(time = 6)
|
||||
public class CommandBenchmark {
|
||||
Function<String, Object> parser;
|
||||
|
||||
@Setup
|
||||
public void setup() {
|
||||
var graph = Graph.merge(Set.of(
|
||||
new Command("tp", "teleport") {{
|
||||
addSyntax((sender, context) -> {}, Potion("pos"));
|
||||
addSyntax((sender, context) -> {}, Entity("entity"), Potion("pos"));
|
||||
}},
|
||||
new Command("setblock", "set") {{
|
||||
addSyntax((sender, context) -> {}, RelativeBlockPosition("pos"), BlockState("block"));
|
||||
}},
|
||||
new Command("foo") {{
|
||||
setCondition((sender, commandString) -> true);
|
||||
addSubcommand(new Command("bar") {{
|
||||
addConditionalSyntax((sender, commandString) -> true, (sender, context) -> {});
|
||||
}});
|
||||
addSubcommand(new Command("baz"){{
|
||||
addSyntax((sender, context) -> {}, Word("A").from("a", "b", "c"), Word("B").from("a", "b", "c"));
|
||||
}});
|
||||
}},
|
||||
new Command("def") {{
|
||||
addSyntax((sender, context) -> {}, Literal("a"), Literal("b"), Literal("c"), Literal("d"),
|
||||
Literal("e"), Literal("f"));
|
||||
setDefaultExecutor((sender, context) -> {});
|
||||
}},
|
||||
new Command("parse") {{
|
||||
addSyntax((sender, context) -> {}, Literal("int"), Integer("val"));
|
||||
addSyntax((sender, context) -> {}, Literal("double"), Double("val"));
|
||||
addSyntax((sender, context) -> {}, Literal("float"), Float("val"));
|
||||
addSyntax((sender, context) -> {}, Literal("long"), Long("val"));
|
||||
}}
|
||||
));
|
||||
final CommandParser commandParser = CommandParser.parser();
|
||||
this.parser = input -> commandParser.parse(graph, input);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void unknownCommand5Char(Blackhole bh) {
|
||||
bh.consume(parser.apply("01234"));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void unknownCommand50Char(Blackhole bh) {
|
||||
bh.consume(parser.apply("01234567890123456789012345678901234567890123456789"));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void validCommandWithValidLiteral(Blackhole bh) {
|
||||
bh.consume(parser.apply("foo bar"));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void validCommandWithInvalid50CharLiteral(Blackhole bh) {
|
||||
bh.consume(parser.apply("foo 01234567890123456789012345678901234567890123456789"));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void numberParsing3Digit(Blackhole bh) {
|
||||
bh.consume(parser.apply("parse int 123"));
|
||||
bh.consume(parser.apply("parse float 123"));
|
||||
bh.consume(parser.apply("parse double 123"));
|
||||
bh.consume(parser.apply("parse long 123"));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void numberParsing10Digit(Blackhole bh) {
|
||||
bh.consume(parser.apply("parse int 1234567890"));
|
||||
bh.consume(parser.apply("parse float 1234567890"));
|
||||
bh.consume(parser.apply("parse double 1234567890"));
|
||||
bh.consume(parser.apply("parse long 1234567890"));
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public void numberParsing10DigitInvalid(Blackhole bh) {
|
||||
bh.consume(parser.apply("parse int a1234567890"));
|
||||
bh.consume(parser.apply("parse float a1234567890"));
|
||||
bh.consume(parser.apply("parse double a1234567890"));
|
||||
bh.consume(parser.apply("parse long a1234567890"));
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package net.minestom.server.command;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.CommandDispatcher;
|
||||
import net.minestom.server.command.builder.CommandResult;
|
||||
import net.minestom.server.command.builder.ParsedCommand;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.event.EventDispatcher;
|
||||
import net.minestom.server.event.player.PlayerCommandEvent;
|
||||
@ -12,6 +13,8 @@ import net.minestom.server.utils.validate.Check;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Manager used to register {@link Command commands}.
|
||||
* <p>
|
||||
@ -23,8 +26,10 @@ public final class CommandManager {
|
||||
|
||||
private final ServerSender serverSender = new ServerSender();
|
||||
private final ConsoleSender consoleSender = new ConsoleSender();
|
||||
|
||||
private final CommandDispatcher dispatcher = new CommandDispatcher();
|
||||
private final CommandParser parser = CommandParser.parser();
|
||||
private final CommandDispatcher dispatcher = new CommandDispatcher(this);
|
||||
private final Map<String, Command> commandMap = new HashMap<>();
|
||||
private final Set<Command> commands = new HashSet<>();
|
||||
|
||||
private CommandCallback unknownCommandCallback;
|
||||
|
||||
@ -46,7 +51,10 @@ public final class CommandManager {
|
||||
"A command with the name " + alias + " is already registered!");
|
||||
}
|
||||
}
|
||||
this.dispatcher.register(command);
|
||||
commands.add(command);
|
||||
for (String name : command.getNames()) {
|
||||
commandMap.put(name, command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,7 +64,10 @@ public final class CommandManager {
|
||||
* @param command the command to remove
|
||||
*/
|
||||
public void unregister(@NotNull Command command) {
|
||||
this.dispatcher.unregister(command);
|
||||
commands.remove(command);
|
||||
for (String name : command.getNames()) {
|
||||
commandMap.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -66,7 +77,7 @@ public final class CommandManager {
|
||||
* @return the command associated with the name, null if not any
|
||||
*/
|
||||
public @Nullable Command getCommand(@NotNull String commandName) {
|
||||
return dispatcher.findCommand(commandName);
|
||||
return commandMap.get(commandName.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,8 +87,7 @@ public final class CommandManager {
|
||||
* @return true if the command does exist
|
||||
*/
|
||||
public boolean commandExists(@NotNull String commandName) {
|
||||
commandName = commandName.toLowerCase();
|
||||
return dispatcher.findCommand(commandName) != null;
|
||||
return getCommand(commandName) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -88,6 +98,7 @@ public final class CommandManager {
|
||||
* @return the execution result
|
||||
*/
|
||||
public @NotNull CommandResult execute(@NotNull CommandSender sender, @NotNull String command) {
|
||||
command = command.trim();
|
||||
// Command event
|
||||
if (sender instanceof Player player) {
|
||||
PlayerCommandEvent playerCommandEvent = new PlayerCommandEvent(player, command);
|
||||
@ -97,7 +108,10 @@ public final class CommandManager {
|
||||
command = playerCommandEvent.getCommand();
|
||||
}
|
||||
// Process the command
|
||||
final CommandResult result = dispatcher.execute(sender, command);
|
||||
final CommandParser.Result parsedCommand = parseCommand(command);
|
||||
final ExecutableCommand executable = parsedCommand.executable();
|
||||
final ExecutableCommand.Result executeResult = executable.execute(sender);
|
||||
final CommandResult result = resultConverter(executable, executeResult, command);
|
||||
if (result.getType() == CommandResult.Type.UNKNOWN) {
|
||||
if (unknownCommandCallback != null) {
|
||||
this.unknownCommandCallback.apply(sender, command);
|
||||
@ -157,7 +171,36 @@ public final class CommandManager {
|
||||
* @return the {@link DeclareCommandsPacket} for {@code player}
|
||||
*/
|
||||
public @NotNull DeclareCommandsPacket createDeclareCommandsPacket(@NotNull Player player) {
|
||||
final Graph merged = Graph.merge(dispatcher.getCommands());
|
||||
return GraphConverter.createPacket(merged, player);
|
||||
return GraphConverter.createPacket(getGraph(), player);
|
||||
}
|
||||
|
||||
public @NotNull Set<@NotNull Command> getCommands() {
|
||||
return Collections.unmodifiableSet(commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the command based on the registered commands
|
||||
*
|
||||
* @param input commands string without prefix
|
||||
* @return the parsing result
|
||||
*/
|
||||
public CommandParser.Result parseCommand(String input) {
|
||||
return parser.parse(getGraph(), input);
|
||||
}
|
||||
|
||||
private Graph getGraph() {
|
||||
//todo cache
|
||||
return Graph.merge(commands);
|
||||
}
|
||||
|
||||
private static CommandResult resultConverter(ExecutableCommand executable,
|
||||
ExecutableCommand.Result newResult,
|
||||
String input) {
|
||||
return CommandResult.of(switch (newResult.type()) {
|
||||
case SUCCESS -> CommandResult.Type.SUCCESS;
|
||||
case CANCELLED, PRECONDITION_FAILED, EXECUTOR_EXCEPTION -> CommandResult.Type.CANCELLED;
|
||||
case INVALID_SYNTAX -> CommandResult.Type.INVALID_SYNTAX;
|
||||
case UNKNOWN -> CommandResult.Type.UNKNOWN;
|
||||
}, input, ParsedCommand.fromExecutable(executable), newResult.commandData());
|
||||
}
|
||||
}
|
||||
|
54
src/main/java/net/minestom/server/command/CommandParser.java
Normal file
54
src/main/java/net/minestom/server/command/CommandParser.java
Normal file
@ -0,0 +1,54 @@
|
||||
package net.minestom.server.command;
|
||||
|
||||
import net.minestom.server.command.builder.arguments.Argument;
|
||||
import net.minestom.server.command.builder.suggestion.Suggestion;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ApiStatus.Internal
|
||||
@ApiStatus.Experimental
|
||||
public interface CommandParser {
|
||||
static @NotNull CommandParser parser() {
|
||||
return CommandParserImpl.PARSER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the command by following the graph
|
||||
*
|
||||
* @param graph structure to use for parsing
|
||||
* @param input command string without prefix
|
||||
* @return the parsed command which can be executed and cached
|
||||
*/
|
||||
@Contract("_, _ -> new")
|
||||
@NotNull Result parse(@NotNull Graph graph, @NotNull String input);
|
||||
|
||||
sealed interface Result {
|
||||
@NotNull ExecutableCommand executable();
|
||||
|
||||
@ApiStatus.Internal
|
||||
@Nullable Suggestion suggestion(CommandSender sender);
|
||||
|
||||
@ApiStatus.Internal
|
||||
List<Argument<?>> args();
|
||||
|
||||
sealed interface UnknownCommand extends Result
|
||||
permits CommandParserImpl.UnknownCommandResult {
|
||||
}
|
||||
|
||||
sealed interface KnownCommand extends Result
|
||||
permits CommandParserImpl.InternalKnownCommand, Result.KnownCommand.Invalid, Result.KnownCommand.Valid {
|
||||
|
||||
sealed interface Valid extends KnownCommand
|
||||
permits CommandParserImpl.ValidCommand {
|
||||
}
|
||||
|
||||
sealed interface Invalid extends KnownCommand
|
||||
permits CommandParserImpl.InvalidCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
423
src/main/java/net/minestom/server/command/CommandParserImpl.java
Normal file
423
src/main/java/net/minestom/server/command/CommandParserImpl.java
Normal file
@ -0,0 +1,423 @@
|
||||
package net.minestom.server.command;
|
||||
|
||||
import net.minestom.server.command.Graph.Node;
|
||||
import net.minestom.server.command.builder.ArgumentCallback;
|
||||
import net.minestom.server.command.builder.CommandContext;
|
||||
import net.minestom.server.command.builder.CommandData;
|
||||
import net.minestom.server.command.builder.CommandExecutor;
|
||||
import net.minestom.server.command.builder.arguments.Argument;
|
||||
import net.minestom.server.command.builder.condition.CommandCondition;
|
||||
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
|
||||
import net.minestom.server.command.builder.suggestion.Suggestion;
|
||||
import net.minestom.server.command.builder.suggestion.SuggestionCallback;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
final class CommandParserImpl implements CommandParser {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CommandParserImpl.class);
|
||||
static final CommandParserImpl PARSER = new CommandParserImpl();
|
||||
|
||||
static final class Chain {
|
||||
CommandExecutor defaultExecutor = null;
|
||||
final ArrayDeque<NodeResult> nodeResults = new ArrayDeque<>();
|
||||
final List<CommandCondition> conditions = new ArrayList<>();
|
||||
final List<CommandExecutor> globalListeners = new ArrayList<>();
|
||||
|
||||
void append(NodeResult result) {
|
||||
this.nodeResults.add(result);
|
||||
final Graph.Execution execution = result.node.execution();
|
||||
if (execution != null) {
|
||||
// Create condition chain
|
||||
final CommandCondition condition = execution.condition();
|
||||
if (condition != null) conditions.add(condition);
|
||||
// Track default executor
|
||||
final CommandExecutor defExec = execution.defaultExecutor();
|
||||
if (defExec != null) defaultExecutor = defExec;
|
||||
// Merge global listeners
|
||||
final CommandExecutor globalListener = execution.globalListener();
|
||||
if (globalListener != null) globalListeners.add(globalListener);
|
||||
}
|
||||
}
|
||||
|
||||
CommandCondition mergedConditions() {
|
||||
return (sender, commandString) -> {
|
||||
for (CommandCondition condition : conditions) {
|
||||
if (!condition.canUse(sender, commandString)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
CommandExecutor mergedGlobalExecutors() {
|
||||
return (sender, context) -> globalListeners.forEach(x -> x.apply(sender, context));
|
||||
}
|
||||
|
||||
SuggestionCallback extractSuggestionCallback() {
|
||||
return nodeResults.peekLast().callback;
|
||||
}
|
||||
|
||||
Map<String, ArgumentResult<Object>> collectArguments() {
|
||||
return nodeResults.stream()
|
||||
.skip(1) // skip root
|
||||
.collect(Collectors.toUnmodifiableMap(NodeResult::name, NodeResult::argumentResult));
|
||||
}
|
||||
|
||||
List<Argument<?>> getArgs() {
|
||||
return nodeResults.stream().map(x -> x.node.argument()).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull CommandParser.Result parse(@NotNull Graph graph, @NotNull String input) {
|
||||
final CommandStringReader reader = new CommandStringReader(input);
|
||||
final Chain chain = new Chain();
|
||||
// Read from input
|
||||
NodeResult result;
|
||||
Node parent = graph.root();
|
||||
while ((result = parseChild(parent, reader)) != null) {
|
||||
chain.append(result);
|
||||
if (result.argumentResult instanceof ArgumentResult.SyntaxError<?> e) {
|
||||
// Syntax error stop at this arg
|
||||
final ArgumentCallback argumentCallback = parent.argument().getCallback();
|
||||
if (argumentCallback == null && chain.defaultExecutor != null) {
|
||||
return ValidCommand.defaultExecutor(input, chain);
|
||||
} else {
|
||||
return new InvalidCommand(input, chain.mergedConditions(),
|
||||
argumentCallback, e, chain.collectArguments(), chain.mergedGlobalExecutors(),
|
||||
chain.extractSuggestionCallback(), chain.getArgs());
|
||||
}
|
||||
}
|
||||
parent = result.node;
|
||||
}
|
||||
// Check children for arguments with default values
|
||||
do {
|
||||
Node tmp = parent;
|
||||
parent = null;
|
||||
for (Node child : tmp.next()) {
|
||||
final Argument<?> argument = child.argument();
|
||||
final Supplier<?> defaultSupplier = argument.getDefaultValue();
|
||||
if (defaultSupplier != null) {
|
||||
final Object value = defaultSupplier.get();
|
||||
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
|
||||
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 -> _")
|
||||
private static <R, T> @Nullable R nullSafeGetter(@Nullable T obj, Function<T, R> getter) {
|
||||
return obj == null ? null : getter.apply(obj);
|
||||
}
|
||||
|
||||
private static NodeResult parseChild(Node parent, CommandStringReader reader) {
|
||||
if (!reader.hasRemaining()) return null;
|
||||
for (Node child : parent.next()) {
|
||||
final Argument<?> argument = child.argument();
|
||||
final int start = reader.cursor();
|
||||
final ArgumentResult<?> parse = parse(argument, reader);
|
||||
if (parse instanceof ArgumentResult.Success<?> success) {
|
||||
return new NodeResult(child, (ArgumentResult<Object>) success,
|
||||
argument.getSuggestionCallback());
|
||||
} else if (parse instanceof ArgumentResult.SyntaxError<?> syntaxError) {
|
||||
return new NodeResult(child, (ArgumentResult<Object>) syntaxError,
|
||||
argument.getSuggestionCallback());
|
||||
} else {
|
||||
// Reset cursor & try next
|
||||
reader.cursor(start);
|
||||
}
|
||||
}
|
||||
for (Node node : parent.next()) {
|
||||
final SuggestionCallback suggestionCallback = node.argument().getSuggestionCallback();
|
||||
if (suggestionCallback != null) {
|
||||
return new NodeResult(parent,
|
||||
new ArgumentResult.SyntaxError<>("None of the arguments were compatible, but a suggestion callback was found.", "", -1),
|
||||
suggestionCallback);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
record UnknownCommandResult() implements Result.UnknownCommand {
|
||||
private static final Result INSTANCE = new UnknownCommandResult();
|
||||
|
||||
@Override
|
||||
public @NotNull ExecutableCommand executable() {
|
||||
return UnknownExecutableCmd.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Suggestion suggestion(CommandSender sender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Argument<?>> args() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface InternalKnownCommand extends Result.KnownCommand {
|
||||
String input();
|
||||
|
||||
@Nullable CommandCondition condition();
|
||||
|
||||
@NotNull Map<String, ArgumentResult<Object>> arguments();
|
||||
|
||||
CommandExecutor globalListener();
|
||||
|
||||
@Nullable SuggestionCallback suggestionCallback();
|
||||
|
||||
@Override
|
||||
default @Nullable Suggestion suggestion(CommandSender sender) {
|
||||
final SuggestionCallback callback = suggestionCallback();
|
||||
if (callback == null) return null;
|
||||
final int lastSpace = input().lastIndexOf(" ");
|
||||
final Suggestion suggestion = new Suggestion(input(), lastSpace + 2, input().length() - lastSpace - 1);
|
||||
final CommandContext context = createCommandContext(input(), arguments());
|
||||
callback.apply(sender, context, suggestion);
|
||||
return suggestion;
|
||||
}
|
||||
}
|
||||
|
||||
record InvalidCommand(String input, CommandCondition condition, ArgumentCallback callback,
|
||||
ArgumentResult.SyntaxError<?> error,
|
||||
@NotNull Map<String, ArgumentResult<Object>> arguments, CommandExecutor globalListener,
|
||||
@Nullable SuggestionCallback suggestionCallback, List<Argument<?>> args)
|
||||
implements InternalKnownCommand, Result.KnownCommand.Invalid {
|
||||
|
||||
static InvalidCommand invalid(String input, Chain chain) {
|
||||
return new InvalidCommand(input, chain.mergedConditions(),
|
||||
null/*todo command syntax callback*/,
|
||||
new ArgumentResult.SyntaxError<>("Command has trailing data.", null, -1),
|
||||
chain.collectArguments(), chain.mergedGlobalExecutors(), chain.extractSuggestionCallback(), chain.getArgs());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ExecutableCommand executable() {
|
||||
return new InvalidExecutableCmd(condition, globalListener, callback, error, input, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
record ValidCommand(String input, CommandCondition condition, CommandExecutor executor,
|
||||
@NotNull Map<String, ArgumentResult<Object>> arguments,
|
||||
CommandExecutor globalListener, @Nullable SuggestionCallback suggestionCallback, List<Argument<?>> args)
|
||||
implements InternalKnownCommand, Result.KnownCommand.Valid {
|
||||
|
||||
static ValidCommand defaultExecutor(String input, Chain chain) {
|
||||
return new ValidCommand(input, chain.mergedConditions(), chain.defaultExecutor, chain.collectArguments(),
|
||||
chain.mergedGlobalExecutors(), chain.extractSuggestionCallback(), chain.getArgs());
|
||||
}
|
||||
|
||||
static ValidCommand executor(String input, Chain chain, CommandExecutor executor) {
|
||||
return new ValidCommand(input, chain.mergedConditions(), executor, chain.collectArguments(), chain.mergedGlobalExecutors(),
|
||||
chain.extractSuggestionCallback(), chain.getArgs());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ExecutableCommand executable() {
|
||||
return new ValidExecutableCmd(condition, globalListener, executor, input, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
record UnknownExecutableCmd() implements ExecutableCommand {
|
||||
static final ExecutableCommand INSTANCE = new UnknownExecutableCmd();
|
||||
|
||||
@Override
|
||||
public @NotNull Result execute(@NotNull CommandSender sender) {
|
||||
return ExecutionResultImpl.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
record ValidExecutableCmd(CommandCondition condition, CommandExecutor globalListener, CommandExecutor executor,
|
||||
String input,
|
||||
Map<String, ArgumentResult<Object>> arguments) implements ExecutableCommand {
|
||||
@Override
|
||||
public @NotNull Result execute(@NotNull CommandSender sender) {
|
||||
final CommandContext context = createCommandContext(input, arguments);
|
||||
|
||||
globalListener().apply(sender, context);
|
||||
|
||||
if (condition != null && !condition.canUse(sender, input())) {
|
||||
return ExecutionResultImpl.PRECONDITION_FAILED;
|
||||
}
|
||||
try {
|
||||
executor().apply(sender, context);
|
||||
return new ExecutionResultImpl(ExecutableCommand.Result.Type.SUCCESS, context.getReturnData());
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("An exception was encountered while executing command: " + input(), e);
|
||||
return ExecutionResultImpl.EXECUTOR_EXCEPTION;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record InvalidExecutableCmd(CommandCondition condition, CommandExecutor globalListener, ArgumentCallback callback,
|
||||
ArgumentResult.SyntaxError<?> error, String input,
|
||||
Map<String, ArgumentResult<Object>> arguments) implements ExecutableCommand {
|
||||
@Override
|
||||
public @NotNull Result execute(@NotNull CommandSender sender) {
|
||||
globalListener().apply(sender, createCommandContext(input, arguments));
|
||||
|
||||
if (condition != null && !condition.canUse(sender, input())) {
|
||||
return ExecutionResultImpl.PRECONDITION_FAILED;
|
||||
}
|
||||
if (callback != null)
|
||||
callback.apply(sender, new ArgumentSyntaxException(error.message(), error.input(), error.code()));
|
||||
return ExecutionResultImpl.INVALID_SYNTAX;
|
||||
}
|
||||
}
|
||||
|
||||
private static CommandContext createCommandContext(String input, Map<String, ArgumentResult<Object>> arguments) {
|
||||
final CommandContext context = new CommandContext(input);
|
||||
for (var entry : arguments.entrySet()) {
|
||||
final String identifier = entry.getKey();
|
||||
final ArgumentResult<Object> value = entry.getValue();
|
||||
|
||||
final Object argOutput = value instanceof ArgumentResult.Success<Object> success ? success.value() : null;
|
||||
final String argInput = value instanceof ArgumentResult.Success<Object> success ? success.input() : "";
|
||||
|
||||
context.setArg(identifier, argOutput, argInput);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
record ExecutionResultImpl(Type type, CommandData commandData) implements ExecutableCommand.Result {
|
||||
static final ExecutableCommand.Result CANCELLED = new ExecutionResultImpl(Type.CANCELLED, null);
|
||||
static final ExecutableCommand.Result UNKNOWN = new ExecutionResultImpl(Type.UNKNOWN, null);
|
||||
static final ExecutableCommand.Result EXECUTOR_EXCEPTION = new ExecutionResultImpl(Type.EXECUTOR_EXCEPTION, null);
|
||||
static final ExecutableCommand.Result PRECONDITION_FAILED = new ExecutionResultImpl(Type.PRECONDITION_FAILED, null);
|
||||
static final ExecutableCommand.Result INVALID_SYNTAX = new ExecutionResultImpl(Type.INVALID_SYNTAX, null);
|
||||
}
|
||||
|
||||
private record NodeResult(Node node, ArgumentResult<Object> argumentResult, SuggestionCallback callback) {
|
||||
public String name() {
|
||||
return node.argument().getId();
|
||||
}
|
||||
}
|
||||
|
||||
static final class CommandStringReader {
|
||||
private final String input;
|
||||
private int cursor = 0;
|
||||
|
||||
CommandStringReader(String input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
boolean hasRemaining() {
|
||||
return cursor < input.length();
|
||||
}
|
||||
|
||||
String readWord() {
|
||||
final String input = this.input;
|
||||
final int cursor = this.cursor;
|
||||
|
||||
final int i = input.indexOf(' ', cursor);
|
||||
if (i == -1) {
|
||||
this.cursor = input.length() + 1;
|
||||
return input.substring(cursor);
|
||||
}
|
||||
final String read = input.substring(cursor, i);
|
||||
this.cursor += read.length() + 1;
|
||||
return read;
|
||||
}
|
||||
|
||||
String readRemaining() {
|
||||
final String input = this.input;
|
||||
final String result = input.substring(cursor);
|
||||
this.cursor = input.length();
|
||||
return result;
|
||||
}
|
||||
|
||||
int cursor() {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
void cursor(int cursor) {
|
||||
assert cursor >= 0 && cursor <= input.length();
|
||||
this.cursor = cursor;
|
||||
}
|
||||
}
|
||||
|
||||
// ARGUMENT
|
||||
|
||||
private static <T> ArgumentResult<T> parse(Argument<T> argument, CommandStringReader reader) {
|
||||
// Handle specific type without loop
|
||||
try {
|
||||
// Single word argument
|
||||
if (!argument.allowSpace()) {
|
||||
final String word = reader.readWord();
|
||||
return new ArgumentResult.Success<>(argument.parse(word), word);
|
||||
}
|
||||
// Complete input argument
|
||||
if (argument.useRemaining()) {
|
||||
final String remaining = reader.readRemaining();
|
||||
return new ArgumentResult.Success<>(argument.parse(remaining), remaining);
|
||||
}
|
||||
} catch (ArgumentSyntaxException ignored) {
|
||||
return new ArgumentResult.IncompatibleType<>();
|
||||
}
|
||||
// Bruteforce
|
||||
assert argument.allowSpace() && !argument.useRemaining();
|
||||
StringBuilder current = new StringBuilder(reader.readWord());
|
||||
while (true) {
|
||||
try {
|
||||
final String input = current.toString();
|
||||
return new ArgumentResult.Success<>(argument.parse(input), input);
|
||||
} catch (ArgumentSyntaxException ignored) {
|
||||
if (!reader.hasRemaining()) break;
|
||||
current.append(" ");
|
||||
current.append(reader.readWord());
|
||||
}
|
||||
}
|
||||
return new ArgumentResult.IncompatibleType<>();
|
||||
}
|
||||
|
||||
private sealed interface ArgumentResult<R> {
|
||||
record Success<T>(T value, String input)
|
||||
implements ArgumentResult<T> {
|
||||
}
|
||||
|
||||
record IncompatibleType<T>()
|
||||
implements ArgumentResult<T> {
|
||||
}
|
||||
|
||||
record SyntaxError<T>(String message, String input, int code)
|
||||
implements ArgumentResult<T> {
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package net.minestom.server.command;
|
||||
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.CommandData;
|
||||
import net.minestom.server.command.builder.CommandSyntax;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ApiStatus.Internal
|
||||
@ApiStatus.Experimental
|
||||
public interface ExecutableCommand {
|
||||
@NotNull Result execute(@NotNull CommandSender sender);
|
||||
|
||||
interface Result {
|
||||
@NotNull Type type();
|
||||
|
||||
@NotNull CommandData commandData();
|
||||
|
||||
enum Type {
|
||||
/**
|
||||
* Command executed successfully.
|
||||
*/
|
||||
SUCCESS,
|
||||
/**
|
||||
* Command cancelled by an event listener.
|
||||
*/
|
||||
CANCELLED,
|
||||
/**
|
||||
* Either {@link Command#getCondition()} or {@link CommandSyntax#getCommandCondition()} failed
|
||||
*/
|
||||
PRECONDITION_FAILED,
|
||||
/**
|
||||
* Command couldn't be executed because of a syntax error
|
||||
*/
|
||||
INVALID_SYNTAX,
|
||||
/**
|
||||
* The command executor threw an exception
|
||||
*/
|
||||
EXECUTOR_EXCEPTION,
|
||||
/**
|
||||
* Unknown command
|
||||
*/
|
||||
UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
package net.minestom.server.command;
|
||||
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.CommandExecutor;
|
||||
import net.minestom.server.command.builder.arguments.Argument;
|
||||
import net.minestom.server.command.builder.condition.CommandCondition;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnknownNullability;
|
||||
|
||||
import java.util.Collection;
|
||||
@ -11,8 +14,12 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
sealed interface Graph permits GraphImpl {
|
||||
static @NotNull Builder builder(@NotNull Argument<?> argument, @Nullable Execution execution) {
|
||||
return new GraphImpl.BuilderImpl(argument, execution);
|
||||
}
|
||||
|
||||
static @NotNull Builder builder(@NotNull Argument<?> argument) {
|
||||
return new GraphImpl.BuilderImpl(argument);
|
||||
return new GraphImpl.BuilderImpl(argument, null);
|
||||
}
|
||||
|
||||
static @NotNull Graph fromCommand(@NotNull Command command) {
|
||||
@ -44,13 +51,35 @@ sealed interface Graph permits GraphImpl {
|
||||
}
|
||||
|
||||
sealed interface Execution extends Predicate<CommandSender> permits GraphImpl.ExecutionImpl {
|
||||
// TODO execute the node
|
||||
@UnknownNullability CommandExecutor defaultExecutor();
|
||||
|
||||
@UnknownNullability CommandExecutor globalListener();
|
||||
|
||||
/**
|
||||
* Non-null if the command at this point considered executable, must be present
|
||||
* on the last node of the syntax.
|
||||
*/
|
||||
@Nullable CommandExecutor executor();
|
||||
|
||||
/**
|
||||
* Non-null if the command or syntax has a condition, must be present
|
||||
* only on nodes that specify it
|
||||
*/
|
||||
@Nullable CommandCondition condition();
|
||||
}
|
||||
|
||||
sealed interface Builder permits GraphImpl.BuilderImpl {
|
||||
@NotNull Builder append(@NotNull Argument<?> argument, @NotNull Consumer<Builder> consumer);
|
||||
@NotNull Builder append(@NotNull Argument<?> argument, @Nullable Execution execution, @NotNull Consumer<Builder> consumer);
|
||||
|
||||
@NotNull Builder append(@NotNull Argument<?> argument);
|
||||
@NotNull Builder append(@NotNull Argument<?> argument, @Nullable Execution execution);
|
||||
|
||||
default @NotNull Builder append(@NotNull Argument<?> argument, @NotNull Consumer<Builder> consumer) {
|
||||
return append(argument, null, consumer);
|
||||
}
|
||||
|
||||
default @NotNull Builder append(@NotNull Argument<?> argument) {
|
||||
return append(argument, (Execution) null);
|
||||
}
|
||||
|
||||
@NotNull Graph build();
|
||||
}
|
||||
|
@ -6,11 +6,9 @@ import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
final class GraphConverter {
|
||||
private GraphConverter() {
|
||||
@ -20,18 +18,19 @@ final class GraphConverter {
|
||||
@Contract("_, _ -> new")
|
||||
public static DeclareCommandsPacket createPacket(Graph graph, @Nullable Player player) {
|
||||
List<DeclareCommandsPacket.Node> nodes = new ArrayList<>();
|
||||
List<Consumer<Integer>> rootRedirect = new ArrayList<>();
|
||||
List<BiConsumer<Graph, Integer>> redirects = new ArrayList<>();
|
||||
Map<Argument<?>, Integer> argToPacketId = new HashMap<>();
|
||||
final AtomicInteger idSource = new AtomicInteger(0);
|
||||
final int rootId = append(graph.root(), nodes, rootRedirect, idSource, null, null, player)[0];
|
||||
for (var i : rootRedirect) {
|
||||
i.accept(rootId);
|
||||
final int rootId = append(graph.root(), nodes, redirects, idSource, null, player, argToPacketId)[0];
|
||||
for (var r : redirects) {
|
||||
r.accept(graph, rootId);
|
||||
}
|
||||
return new DeclareCommandsPacket(nodes, rootId);
|
||||
}
|
||||
|
||||
private static int[] append(Graph.Node graphNode, List<DeclareCommandsPacket.Node> to,
|
||||
List<Consumer<Integer>> rootRedirect, AtomicInteger id, @Nullable AtomicInteger redirect,
|
||||
List<Runnable> redirectSetters, @Nullable Player player) {
|
||||
List<BiConsumer<Graph, Integer>> redirects, AtomicInteger id, @Nullable AtomicInteger redirect,
|
||||
@Nullable Player player, Map<Argument<?>, Integer> argToPacketId) {
|
||||
final Graph.Execution execution = graphNode.execution();
|
||||
if (player != null && execution != null) {
|
||||
if (!execution.test(player)) return new int[0];
|
||||
@ -43,7 +42,10 @@ final class GraphConverter {
|
||||
final DeclareCommandsPacket.Node node = new DeclareCommandsPacket.Node();
|
||||
int[] packetNodeChildren = new int[children.size()];
|
||||
for (int i = 0, appendIndex = 0; i < children.size(); i++) {
|
||||
final int[] append = append(children.get(i), to, rootRedirect, id, redirect, redirectSetters, player);
|
||||
final int[] append = append(children.get(i), to, redirects, id, redirect, player, argToPacketId);
|
||||
if (append.length > 0) {
|
||||
argToPacketId.put(children.get(i).argument(), append[0]);
|
||||
}
|
||||
if (append.length == 1) {
|
||||
packetNodeChildren[appendIndex++] = append[0];
|
||||
} else {
|
||||
@ -61,21 +63,36 @@ final class GraphConverter {
|
||||
node.name = argument.getId();
|
||||
if (redirect != null) {
|
||||
node.flags |= 0x8;
|
||||
redirectSetters.add(() -> node.redirectedNode = redirect.get());
|
||||
redirects.add((graph, root) -> node.redirectedNode = redirect.get());
|
||||
}
|
||||
}
|
||||
to.add(node);
|
||||
return new int[]{id.getAndIncrement()};
|
||||
} else {
|
||||
if (argument instanceof ArgumentCommand) {
|
||||
if (argument instanceof ArgumentCommand argCmd) {
|
||||
node.flags = literal(false, true);
|
||||
node.name = argument.getId();
|
||||
rootRedirect.add(i -> node.redirectedNode = i);
|
||||
final String shortcut = argCmd.getShortcut();
|
||||
if (shortcut.isEmpty()) {
|
||||
redirects.add((graph, root) -> node.redirectedNode = root);
|
||||
} else {
|
||||
redirects.add((graph, root) -> {
|
||||
final List<Argument<?>> args = CommandParser.parser().parse(graph, shortcut).args();
|
||||
final Argument<?> last = args.get(args.size() - 1);
|
||||
if (last.allowSpace()) {
|
||||
node.redirectedNode = argToPacketId.get(args.get(args.size()-2));
|
||||
} else {
|
||||
node.redirectedNode = argToPacketId.get(last);
|
||||
}
|
||||
});
|
||||
}
|
||||
to.add(node);
|
||||
|
||||
return new int[]{id.getAndIncrement()};
|
||||
} else if (argument instanceof ArgumentEnum<?> || (argument instanceof ArgumentWord word && word.hasRestrictions())) {
|
||||
List<String> entries = argument instanceof ArgumentEnum<?> ? ((ArgumentEnum<?>) argument).entries() : Arrays.stream(((ArgumentWord) argument).getRestrictions()).toList();
|
||||
List<String> entries = argument instanceof ArgumentEnum<?> ?
|
||||
((ArgumentEnum<?>) argument).entries() :
|
||||
Arrays.stream(((ArgumentWord) argument).getRestrictions()).toList();
|
||||
final int[] res = new int[entries.size()];
|
||||
for (int i = 0; i < res.length; i++) {
|
||||
String entry = entries.get(i);
|
||||
@ -85,7 +102,7 @@ final class GraphConverter {
|
||||
subNode.name = entry;
|
||||
if (redirect != null) {
|
||||
subNode.flags |= 0x8;
|
||||
redirectSetters.add(() -> subNode.redirectedNode = redirect.get());
|
||||
redirects.add((graph, root) -> subNode.redirectedNode = redirect.get());
|
||||
}
|
||||
to.add(subNode);
|
||||
res[i] = id.getAndIncrement();
|
||||
@ -99,7 +116,8 @@ final class GraphConverter {
|
||||
Argument<?> entry = entries.get(i);
|
||||
if (i == entries.size() - 1) {
|
||||
// Last will be the parent of next args
|
||||
final int[] l = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, rootRedirect, id, redirect, redirectSetters, player);
|
||||
final int[] l = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, redirects,
|
||||
id, redirect, player, argToPacketId);
|
||||
for (int n : l) {
|
||||
to.get(n).children = node.children;
|
||||
}
|
||||
@ -109,10 +127,12 @@ final class GraphConverter {
|
||||
return res == null ? l : res;
|
||||
} else if (i == 0) {
|
||||
// First will be the children & parent of following
|
||||
res = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, rootRedirect, id, null, redirectSetters, player);
|
||||
res = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, redirects, id,
|
||||
null, player, argToPacketId);
|
||||
last = res;
|
||||
} else {
|
||||
final int[] l = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, rootRedirect, id, null, redirectSetters, player);
|
||||
final int[] l = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, redirects,
|
||||
id, null, player, argToPacketId);
|
||||
for (int n : last) {
|
||||
to.get(n).children = l;
|
||||
}
|
||||
@ -122,12 +142,12 @@ final class GraphConverter {
|
||||
throw new RuntimeException("Arg group must have child args.");
|
||||
} else if (argument instanceof ArgumentLoop special) {
|
||||
AtomicInteger r = new AtomicInteger();
|
||||
List<Runnable> setters = new ArrayList<>();
|
||||
int[] res = new int[special.arguments().size()];
|
||||
List<?> arguments = special.arguments();
|
||||
for (int i = 0, appendIndex = 0; i < arguments.size(); i++) {
|
||||
Object arg = arguments.get(i);
|
||||
final int[] append = append(new GraphImpl.NodeImpl((Argument<?>) arg, null, List.of()), to, rootRedirect, id, r, setters, player);
|
||||
final int[] append = append(new GraphImpl.NodeImpl((Argument<?>) arg, null, List.of()), to,
|
||||
redirects, id, r, player, argToPacketId);
|
||||
if (append.length == 1) {
|
||||
res[appendIndex++] = append[0];
|
||||
} else {
|
||||
@ -137,16 +157,19 @@ final class GraphConverter {
|
||||
}
|
||||
}
|
||||
r.set(id.get());
|
||||
setters.forEach(Runnable::run);
|
||||
return res;
|
||||
} else {
|
||||
node.flags = arg(false, argument.hasSuggestion());
|
||||
final boolean hasSuggestion = argument.hasSuggestion();
|
||||
node.flags = arg(false, hasSuggestion);
|
||||
node.name = argument.getId();
|
||||
node.parser = argument.parser();
|
||||
node.properties = argument.nodeProperties();
|
||||
if (redirect != null) {
|
||||
node.flags |= 0x8;
|
||||
redirectSetters.add(() -> node.redirectedNode = redirect.get());
|
||||
redirects.add((graph, root) -> node.redirectedNode = redirect.get());
|
||||
}
|
||||
if (hasSuggestion) {
|
||||
node.suggestionsType = argument.suggestionType().getIdentifier();
|
||||
}
|
||||
to.add(node);
|
||||
return new int[]{id.getAndIncrement()};
|
||||
|
@ -1,10 +1,12 @@
|
||||
package net.minestom.server.command;
|
||||
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.CommandExecutor;
|
||||
import net.minestom.server.command.builder.CommandSyntax;
|
||||
import net.minestom.server.command.builder.arguments.Argument;
|
||||
import net.minestom.server.command.builder.condition.CommandCondition;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
@ -33,22 +35,23 @@ record GraphImpl(NodeImpl root) implements Graph {
|
||||
return compare(root, graph.root(), comparator);
|
||||
}
|
||||
|
||||
record BuilderImpl(Argument<?> argument, List<BuilderImpl> children) implements Graph.Builder {
|
||||
public BuilderImpl(Argument<?> argument) {
|
||||
this(argument, new ArrayList<>());
|
||||
record BuilderImpl(Argument<?> argument, List<BuilderImpl> children, Execution execution) implements Graph.Builder {
|
||||
public BuilderImpl(Argument<?> argument, Execution execution) {
|
||||
this(argument, new ArrayList<>(), execution);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Graph.@NotNull Builder append(@NotNull Argument<?> argument, @NotNull Consumer<Graph.Builder> consumer) {
|
||||
BuilderImpl builder = new BuilderImpl(argument);
|
||||
public Graph.@NotNull Builder append(@NotNull Argument<?> argument, @Nullable Execution execution,
|
||||
@NotNull Consumer<Graph.Builder> consumer) {
|
||||
BuilderImpl builder = new BuilderImpl(argument, execution);
|
||||
consumer.accept(builder);
|
||||
this.children.add(builder);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Graph.@NotNull Builder append(@NotNull Argument<?> argument) {
|
||||
this.children.add(new BuilderImpl(argument, List.of()));
|
||||
public Graph.@NotNull Builder append(@NotNull Argument<?> argument, @Nullable Execution execution) {
|
||||
this.children.add(new BuilderImpl(argument, List.of(), execution));
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -63,7 +66,7 @@ record GraphImpl(NodeImpl root) implements Graph {
|
||||
final List<BuilderImpl> children = builder.children;
|
||||
Node[] nodes = new NodeImpl[children.size()];
|
||||
for (int i = 0; i < children.size(); i++) nodes[i] = fromBuilder(children.get(i));
|
||||
return new NodeImpl(builder.argument, null, List.of(nodes));
|
||||
return new NodeImpl(builder.argument, (ExecutionImpl) builder.execution, List.of(nodes));
|
||||
}
|
||||
|
||||
static NodeImpl command(Command command) {
|
||||
@ -75,22 +78,38 @@ record GraphImpl(NodeImpl root) implements Graph {
|
||||
}
|
||||
}
|
||||
|
||||
record ExecutionImpl(Predicate<CommandSender> predicate) implements Execution {
|
||||
record ExecutionImpl(Predicate<CommandSender> predicate,
|
||||
CommandExecutor defaultExecutor, CommandExecutor globalListener,
|
||||
CommandExecutor executor, CommandCondition condition) implements Execution {
|
||||
@Override
|
||||
public boolean test(CommandSender commandSender) {
|
||||
return predicate.test(commandSender);
|
||||
}
|
||||
|
||||
static ExecutionImpl fromCommand(Command command) {
|
||||
final CommandCondition condition = command.getCondition();
|
||||
if (condition == null) return null;
|
||||
return new ExecutionImpl(commandSender -> condition.canUse(commandSender, null));
|
||||
final CommandExecutor defaultExecutor = command.getDefaultExecutor();
|
||||
final CommandCondition defaultCondition = command.getCondition();
|
||||
|
||||
CommandExecutor executor = defaultExecutor;
|
||||
CommandCondition condition = defaultCondition;
|
||||
for (var syntax : command.getSyntaxes()) {
|
||||
if (syntax.getArguments().length == 0) {
|
||||
executor = syntax.getExecutor();
|
||||
condition = syntax.getCommandCondition();
|
||||
break;
|
||||
}
|
||||
}
|
||||
final CommandExecutor globalListener = (sender, context) -> command.globalListener(sender, context, context.getInput());
|
||||
|
||||
return new ExecutionImpl(commandSender -> defaultCondition == null || defaultCondition.canUse(commandSender, null),
|
||||
defaultExecutor, globalListener, executor, condition);
|
||||
}
|
||||
|
||||
static ExecutionImpl fromSyntax(CommandSyntax syntax) {
|
||||
final CommandExecutor executor = syntax.getExecutor();
|
||||
final CommandCondition condition = syntax.getCommandCondition();
|
||||
if (condition == null) return null;
|
||||
return new ExecutionImpl(commandSender -> condition.canUse(commandSender, null));
|
||||
return new ExecutionImpl(commandSender -> condition == null || condition.canUse(commandSender, null),
|
||||
null, null, executor, condition);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,33 +2,33 @@ package net.minestom.server.command.builder;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap;
|
||||
import net.minestom.server.command.CommandManager;
|
||||
import net.minestom.server.command.CommandParser;
|
||||
import net.minestom.server.command.CommandSender;
|
||||
import net.minestom.server.command.builder.arguments.Argument;
|
||||
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
|
||||
import net.minestom.server.command.builder.parser.CommandParser;
|
||||
import net.minestom.server.command.builder.parser.CommandQueryResult;
|
||||
import net.minestom.server.command.builder.parser.CommandSuggestionHolder;
|
||||
import net.minestom.server.command.builder.parser.ValidSyntaxHolder;
|
||||
import net.minestom.server.utils.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Class responsible for parsing {@link Command}.
|
||||
*/
|
||||
public class CommandDispatcher {
|
||||
|
||||
private final Map<String, Command> commandMap = new HashMap<>();
|
||||
private final Set<Command> commands = new HashSet<>();
|
||||
private final CommandManager manager;
|
||||
|
||||
private final Cache<String, CommandResult> cache = Caffeine.newBuilder()
|
||||
.expireAfterWrite(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
public CommandDispatcher(CommandManager manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
public CommandDispatcher() {
|
||||
this(new CommandManager());
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a command,
|
||||
* be aware that registering a command name or alias will override the previous entry.
|
||||
@ -36,37 +36,15 @@ public class CommandDispatcher {
|
||||
* @param command the command to register
|
||||
*/
|
||||
public void register(@NotNull Command command) {
|
||||
this.commandMap.put(command.getName().toLowerCase(), command);
|
||||
|
||||
// Register aliases
|
||||
final String[] aliases = command.getAliases();
|
||||
if (aliases != null) {
|
||||
for (String alias : aliases) {
|
||||
this.commandMap.put(alias.toLowerCase(), command);
|
||||
}
|
||||
}
|
||||
|
||||
this.commands.add(command);
|
||||
manager.register(command);
|
||||
}
|
||||
|
||||
public void unregister(@NotNull Command command) {
|
||||
this.commandMap.remove(command.getName().toLowerCase());
|
||||
|
||||
final String[] aliases = command.getAliases();
|
||||
if (aliases != null) {
|
||||
for (String alias : aliases) {
|
||||
this.commandMap.remove(alias.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
this.commands.remove(command);
|
||||
|
||||
// Clear cache
|
||||
this.cache.invalidateAll();
|
||||
manager.unregister(command);
|
||||
}
|
||||
|
||||
public @NotNull Set<Command> getCommands() {
|
||||
return Collections.unmodifiableSet(commands);
|
||||
return manager.getCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,8 +54,7 @@ public class CommandDispatcher {
|
||||
* @return the {@link Command} associated with the name, null if not any
|
||||
*/
|
||||
public @Nullable Command findCommand(@NotNull String commandName) {
|
||||
commandName = commandName.toLowerCase();
|
||||
return commandMap.getOrDefault(commandName, null);
|
||||
return manager.getCommand(commandName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -88,12 +65,7 @@ public class CommandDispatcher {
|
||||
* @return the command result
|
||||
*/
|
||||
public @NotNull CommandResult execute(@NotNull CommandSender source, @NotNull String commandString) {
|
||||
CommandResult commandResult = parse(commandString);
|
||||
ParsedCommand parsedCommand = commandResult.parsedCommand;
|
||||
if (parsedCommand != null) {
|
||||
commandResult.commandData = parsedCommand.execute(source);
|
||||
}
|
||||
return commandResult;
|
||||
return manager.execute(source, commandString);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,137 +75,21 @@ public class CommandDispatcher {
|
||||
* @return the parsing result
|
||||
*/
|
||||
public @NotNull CommandResult parse(@NotNull String commandString) {
|
||||
commandString = commandString.trim();
|
||||
// Verify if the result is cached
|
||||
{
|
||||
final CommandResult cachedResult = cache.getIfPresent(commandString);
|
||||
if (cachedResult != null) {
|
||||
return cachedResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Split space
|
||||
final String[] parts = commandString.split(StringUtils.SPACE);
|
||||
final String commandName = parts[0];
|
||||
|
||||
final CommandQueryResult commandQueryResult = CommandParser.findCommand(this, commandString);
|
||||
// Check if the command exists
|
||||
if (commandQueryResult == null) {
|
||||
return CommandResult.of(CommandResult.Type.UNKNOWN, commandName);
|
||||
}
|
||||
CommandResult result = new CommandResult();
|
||||
result.input = commandString;
|
||||
// Find the used syntax and fill CommandResult#type and CommandResult#parsedCommand
|
||||
findParsedCommand( commandQueryResult, commandName, commandString, result);
|
||||
|
||||
// Cache result
|
||||
this.cache.put(commandString, result);
|
||||
|
||||
return result;
|
||||
final net.minestom.server.command.CommandParser.Result test = manager.parseCommand(commandString);
|
||||
return resultConverter(test, commandString);
|
||||
}
|
||||
|
||||
private @NotNull ParsedCommand findParsedCommand(@NotNull CommandQueryResult commandQueryResult,
|
||||
@NotNull String commandName,
|
||||
@NotNull String commandString,
|
||||
@NotNull CommandResult result) {
|
||||
final Command command = commandQueryResult.command();
|
||||
String[] args = commandQueryResult.args();
|
||||
final boolean hasArgument = args.length > 0;
|
||||
|
||||
final String input = commandName + StringUtils.SPACE + String.join(StringUtils.SPACE, args);
|
||||
|
||||
ParsedCommand parsedCommand = new ParsedCommand();
|
||||
parsedCommand.parents = commandQueryResult.parents();
|
||||
parsedCommand.command = command;
|
||||
parsedCommand.commandString = commandString;
|
||||
|
||||
// The default executor should be used if no argument is provided
|
||||
if (!hasArgument) {
|
||||
Optional<CommandSyntax> optionalSyntax = command.getSyntaxes()
|
||||
.stream()
|
||||
.filter(syntax -> syntax.getArguments().length == 0)
|
||||
.findFirst();
|
||||
|
||||
if (optionalSyntax.isPresent()) {
|
||||
// Empty syntax found
|
||||
final CommandSyntax syntax = optionalSyntax.get();
|
||||
parsedCommand.syntax = syntax;
|
||||
parsedCommand.executor = syntax.getExecutor();
|
||||
parsedCommand.context = new CommandContext(input);
|
||||
|
||||
result.type = CommandResult.Type.SUCCESS;
|
||||
result.parsedCommand = parsedCommand;
|
||||
return parsedCommand;
|
||||
} else {
|
||||
// No empty syntax, use default executor if any
|
||||
final CommandExecutor defaultExecutor = command.getDefaultExecutor();
|
||||
if (defaultExecutor != null) {
|
||||
parsedCommand.executor = defaultExecutor;
|
||||
parsedCommand.context = new CommandContext(input);
|
||||
|
||||
result.type = CommandResult.Type.SUCCESS;
|
||||
result.parsedCommand = parsedCommand;
|
||||
return parsedCommand;
|
||||
}
|
||||
}
|
||||
private static CommandResult resultConverter(net.minestom.server.command.CommandParser.Result parseResult, String input) {
|
||||
CommandResult.Type type;
|
||||
if (parseResult instanceof CommandParser.Result.UnknownCommand) {
|
||||
type = CommandResult.Type.UNKNOWN;
|
||||
} else if (parseResult instanceof CommandParser.Result.KnownCommand.Valid) {
|
||||
type = CommandResult.Type.SUCCESS;
|
||||
} else if (parseResult instanceof CommandParser.Result.KnownCommand.Invalid) {
|
||||
type = CommandResult.Type.INVALID_SYNTAX;
|
||||
} else {
|
||||
throw new IllegalStateException("Unknown CommandParser.Result type");
|
||||
}
|
||||
|
||||
// SYNTAXES PARSING
|
||||
|
||||
// All the registered syntaxes of the command
|
||||
final Collection<CommandSyntax> syntaxes = command.getSyntaxes();
|
||||
// Contains all the fully validated syntaxes (we later find the one with the most amount of arguments)
|
||||
List<ValidSyntaxHolder> validSyntaxes = new ArrayList<>(syntaxes.size());
|
||||
// Contains all the syntaxes that are not fully correct, used to later, retrieve the "most correct syntax"
|
||||
// Number of correct argument - The data about the failing argument
|
||||
Int2ObjectRBTreeMap<CommandSuggestionHolder> syntaxesSuggestions = new Int2ObjectRBTreeMap<>(Collections.reverseOrder());
|
||||
|
||||
for (CommandSyntax syntax : syntaxes) {
|
||||
CommandParser.parse(syntax, syntax.getArguments(), args, commandString, validSyntaxes, syntaxesSuggestions);
|
||||
}
|
||||
|
||||
// Check if there is at least one correct syntax
|
||||
if (!validSyntaxes.isEmpty()) {
|
||||
CommandContext context = new CommandContext(input);
|
||||
// Search the syntax with all perfect args
|
||||
final ValidSyntaxHolder finalValidSyntax = CommandParser.findMostCorrectSyntax(validSyntaxes, context);
|
||||
if (finalValidSyntax != null) {
|
||||
// A fully correct syntax has been found, use it
|
||||
final CommandSyntax syntax = finalValidSyntax.syntax();
|
||||
|
||||
parsedCommand.syntax = syntax;
|
||||
parsedCommand.executor = syntax.getExecutor();
|
||||
parsedCommand.context = context;
|
||||
|
||||
result.type = CommandResult.Type.SUCCESS;
|
||||
result.parsedCommand = parsedCommand;
|
||||
return parsedCommand;
|
||||
}
|
||||
}
|
||||
|
||||
// No all-correct syntax, find the closest one to use the argument callback
|
||||
if (!syntaxesSuggestions.isEmpty()) {
|
||||
final int max = syntaxesSuggestions.firstIntKey(); // number of correct arguments in the most correct syntax
|
||||
final CommandSuggestionHolder suggestionHolder = syntaxesSuggestions.get(max);
|
||||
final CommandSyntax syntax = suggestionHolder.syntax();
|
||||
final ArgumentSyntaxException argumentSyntaxException = suggestionHolder.argumentSyntaxException();
|
||||
final int argIndex = suggestionHolder.argIndex();
|
||||
|
||||
// Found the closest syntax with at least 1 correct argument
|
||||
final Argument<?> argument = syntax.getArguments()[argIndex];
|
||||
if (argument.hasErrorCallback() && argumentSyntaxException != null) {
|
||||
parsedCommand.callback = argument.getCallback();
|
||||
parsedCommand.argumentSyntaxException = argumentSyntaxException;
|
||||
|
||||
result.type = CommandResult.Type.INVALID_SYNTAX;
|
||||
result.parsedCommand = parsedCommand;
|
||||
return parsedCommand;
|
||||
}
|
||||
}
|
||||
|
||||
// No syntax found
|
||||
result.type = CommandResult.Type.INVALID_SYNTAX;
|
||||
result.parsedCommand = ParsedCommand.withDefaultExecutor(command, input);
|
||||
return result.parsedCommand;
|
||||
return CommandResult.of(type, input, ParsedCommand.fromExecutable(parseResult.executable()), null);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.minestom.server.command.builder;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@ -52,4 +53,14 @@ public class CommandResult {
|
||||
result.input = input;
|
||||
return result;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public static @NotNull CommandResult of(Type type, String input, ParsedCommand parsedCommand, CommandData data) {
|
||||
CommandResult result = new CommandResult();
|
||||
result.type = type;
|
||||
result.input = input;
|
||||
result.parsedCommand = parsedCommand;
|
||||
result.commandData = data;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,19 @@
|
||||
package net.minestom.server.command.builder;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.command.CommandSender;
|
||||
import net.minestom.server.command.builder.condition.CommandCondition;
|
||||
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
|
||||
import net.minestom.server.command.ExecutableCommand;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a {@link Command} ready to be executed (already parsed).
|
||||
*/
|
||||
public class ParsedCommand {
|
||||
private final ExecutableCommand executableCommand;
|
||||
|
||||
// Command
|
||||
protected List<Command> parents;
|
||||
protected Command command;
|
||||
protected String commandString;
|
||||
|
||||
// Command Executor
|
||||
protected CommandSyntax syntax;
|
||||
|
||||
protected CommandExecutor executor;
|
||||
protected CommandContext context;
|
||||
|
||||
// Argument Callback
|
||||
protected ArgumentCallback callback;
|
||||
protected ArgumentSyntaxException argumentSyntaxException;
|
||||
private ParsedCommand(ExecutableCommand executableCommand) {
|
||||
this.executableCommand = executableCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the command for the given source.
|
||||
@ -40,70 +25,11 @@ public class ParsedCommand {
|
||||
* @return the command data, null if none
|
||||
*/
|
||||
public @Nullable CommandData execute(@NotNull CommandSender source) {
|
||||
// Global listener
|
||||
command.globalListener(source, Objects.requireNonNullElseGet(context, () -> new CommandContext(commandString)), commandString);
|
||||
// Command condition check
|
||||
{
|
||||
// Parents
|
||||
if (parents != null) {
|
||||
for (Command parent : parents) {
|
||||
final CommandCondition condition = parent.getCondition();
|
||||
if (condition != null) {
|
||||
final boolean result = condition.canUse(source, commandString);
|
||||
if (!result) return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Self
|
||||
final CommandCondition condition = command.getCondition();
|
||||
if (condition != null) {
|
||||
final boolean result = condition.canUse(source, commandString);
|
||||
if (!result) return null;
|
||||
}
|
||||
}
|
||||
// Condition is respected
|
||||
if (executor != null) {
|
||||
// An executor has been found
|
||||
|
||||
if (syntax != null) {
|
||||
// The executor is from a syntax
|
||||
final CommandCondition commandCondition = syntax.getCommandCondition();
|
||||
if (commandCondition == null || commandCondition.canUse(source, commandString)) {
|
||||
context.retrieveDefaultValues(syntax.getDefaultValuesMap());
|
||||
try {
|
||||
executor.apply(source, context);
|
||||
} catch (Throwable throwable) {
|
||||
MinecraftServer.getExceptionManager().handleException(throwable);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The executor is probably the default one
|
||||
try {
|
||||
executor.apply(source, context);
|
||||
} catch (Throwable throwable) {
|
||||
MinecraftServer.getExceptionManager().handleException(throwable);
|
||||
}
|
||||
}
|
||||
} else if (callback != null && argumentSyntaxException != null) {
|
||||
// No syntax has been validated but the faulty argument with a callback has been found
|
||||
// Execute the faulty argument callback
|
||||
callback.apply(source, argumentSyntaxException);
|
||||
}
|
||||
|
||||
if (context == null) {
|
||||
// Argument callbacks cannot return data
|
||||
return null;
|
||||
}
|
||||
|
||||
return context.getReturnData();
|
||||
final ExecutableCommand.Result result = executableCommand.execute(source);
|
||||
return result.commandData();
|
||||
}
|
||||
|
||||
public static @NotNull ParsedCommand withDefaultExecutor(@NotNull Command command, @NotNull String input) {
|
||||
ParsedCommand parsedCommand = new ParsedCommand();
|
||||
parsedCommand.command = command;
|
||||
parsedCommand.commandString = input;
|
||||
parsedCommand.executor = command.getDefaultExecutor();
|
||||
parsedCommand.context = new CommandContext(input);
|
||||
return parsedCommand;
|
||||
public static @NotNull ParsedCommand fromExecutable(ExecutableCommand executableCommand) {
|
||||
return new ParsedCommand(executableCommand);
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,12 @@ public class ArgumentSyntaxException extends RuntimeException {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable fillInStackTrace() {
|
||||
// Stacktrace is useless to the parser
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the problematic command input.
|
||||
*
|
||||
|
@ -2,7 +2,10 @@ package net.minestom.server.command.builder.parser;
|
||||
|
||||
import net.minestom.server.command.builder.arguments.*;
|
||||
import net.minestom.server.command.builder.arguments.minecraft.*;
|
||||
import net.minestom.server.command.builder.arguments.minecraft.registry.*;
|
||||
import net.minestom.server.command.builder.arguments.minecraft.registry.ArgumentEnchantment;
|
||||
import net.minestom.server.command.builder.arguments.minecraft.registry.ArgumentEntityType;
|
||||
import net.minestom.server.command.builder.arguments.minecraft.registry.ArgumentParticle;
|
||||
import net.minestom.server.command.builder.arguments.minecraft.registry.ArgumentPotionEffect;
|
||||
import net.minestom.server.command.builder.arguments.number.ArgumentDouble;
|
||||
import net.minestom.server.command.builder.arguments.number.ArgumentFloat;
|
||||
import net.minestom.server.command.builder.arguments.number.ArgumentInteger;
|
||||
|
@ -1,24 +1,13 @@
|
||||
package net.minestom.server.listener;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.command.CommandManager;
|
||||
import net.minestom.server.command.CommandSender;
|
||||
import net.minestom.server.command.builder.CommandSyntax;
|
||||
import net.minestom.server.command.builder.arguments.Argument;
|
||||
import net.minestom.server.command.builder.parser.ArgumentQueryResult;
|
||||
import net.minestom.server.command.builder.parser.CommandParser;
|
||||
import net.minestom.server.command.builder.parser.CommandQueryResult;
|
||||
import net.minestom.server.command.builder.suggestion.Suggestion;
|
||||
import net.minestom.server.command.builder.suggestion.SuggestionCallback;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.network.packet.client.play.ClientTabCompletePacket;
|
||||
import net.minestom.server.network.packet.server.play.TabCompletePacket;
|
||||
import net.minestom.server.utils.StringUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TabCompleteListener {
|
||||
|
||||
public static void listener(ClientTabCompletePacket packet, Player player) {
|
||||
@ -37,43 +26,15 @@ public class TabCompleteListener {
|
||||
}
|
||||
|
||||
public static @Nullable Suggestion getSuggestion(CommandSender commandSender, String text) {
|
||||
String commandString = text.replaceFirst(CommandManager.COMMAND_PREFIX, "");
|
||||
String[] split = commandString.split(StringUtils.SPACE);
|
||||
String commandName = split[0];
|
||||
String args = commandString.replaceFirst(Pattern.quote(commandName), "");
|
||||
|
||||
final CommandQueryResult commandQueryResult = CommandParser.findCommand(MinecraftServer.getCommandManager().getDispatcher(), commandString);
|
||||
if (commandQueryResult == null) {
|
||||
// Command not found
|
||||
return null;
|
||||
if (text.startsWith("/")) {
|
||||
text = text.substring(1);
|
||||
}
|
||||
|
||||
final ArgumentQueryResult queryResult = CommandParser.findEligibleArgument(commandQueryResult.command(),
|
||||
commandQueryResult.args(), commandString, text.endsWith(StringUtils.SPACE), false,
|
||||
CommandSyntax::hasSuggestion, Argument::hasSuggestion);
|
||||
if (queryResult == null) {
|
||||
// Suggestible argument not found
|
||||
return null;
|
||||
if (text.endsWith(" ")) {
|
||||
// Append a placeholder char if the command ends with a space allowing the parser to find suggestion
|
||||
// for the next arg without typing the first char of it, this is probably the most hacky solution, but hey
|
||||
// it works as intended :)
|
||||
text = text + '\00';
|
||||
}
|
||||
|
||||
final Argument<?> argument = queryResult.argument();
|
||||
|
||||
final SuggestionCallback suggestionCallback = argument.getSuggestionCallback();
|
||||
if (suggestionCallback != null) {
|
||||
final String input = queryResult.input();
|
||||
final int inputLength = input.length();
|
||||
|
||||
final int commandLength = Arrays.stream(split).map(String::length).reduce(0, Integer::sum) +
|
||||
StringUtils.countMatches(args, StringUtils.SPACE_CHAR);
|
||||
final int trailingSpaces = !input.isEmpty() ? text.length() - text.trim().length() : 0;
|
||||
|
||||
final int start = commandLength - inputLength + 1 - trailingSpaces;
|
||||
|
||||
Suggestion suggestion = new Suggestion(input, start, inputLength);
|
||||
suggestionCallback.apply(commandSender, queryResult.context(), suggestion);
|
||||
|
||||
return suggestion;
|
||||
}
|
||||
return null;
|
||||
return MinecraftServer.getCommandManager().parseCommand(text).suggestion(commandSender);
|
||||
}
|
||||
}
|
||||
|
@ -191,6 +191,42 @@ public class CommandPacketTest {
|
||||
""", graph);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cmdArgShortcut() {
|
||||
var foo = Graph.builder(ArgumentType.Literal("foo"))
|
||||
.append(ArgumentType.String("msg"))
|
||||
.build();
|
||||
var bar = Graph.builder(ArgumentType.Literal("bar"))
|
||||
.append(ArgumentType.Command("cmd").setShortcut("foo"))
|
||||
.build();
|
||||
assertPacketGraph("""
|
||||
foo bar cmd=%
|
||||
0->foo bar
|
||||
bar->cmd
|
||||
cmd+>foo
|
||||
foo->msg
|
||||
msg=! brigadier:string 1
|
||||
""", foo, bar);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cmdArgShortcutWithPartialArg() {
|
||||
var foo = Graph.builder(ArgumentType.Literal("foo"))
|
||||
.append(ArgumentType.String("msg"))
|
||||
.build();
|
||||
var bar = Graph.builder(ArgumentType.Literal("bar"))
|
||||
.append(ArgumentType.Command("cmd").setShortcut("foo \"prefix "))
|
||||
.build();
|
||||
assertPacketGraph("""
|
||||
foo bar cmd=%
|
||||
0->foo bar
|
||||
bar->cmd
|
||||
cmd+>foo
|
||||
foo->msg
|
||||
msg=! brigadier:string 1
|
||||
""", foo, bar);
|
||||
}
|
||||
|
||||
static void assertPacketGraph(String expected, Graph... graphs) {
|
||||
var packet = GraphConverter.createPacket(Graph.merge(graphs), null);
|
||||
CommandTestUtils.assertPacket(packet, expected);
|
||||
|
147
src/test/java/net/minestom/server/command/CommandParseTest.java
Normal file
147
src/test/java/net/minestom/server/command/CommandParseTest.java
Normal file
@ -0,0 +1,147 @@
|
||||
package net.minestom.server.command;
|
||||
|
||||
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static net.minestom.server.command.builder.arguments.ArgumentType.Literal;
|
||||
import static net.minestom.server.command.builder.arguments.ArgumentType.Word;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class CommandParseTest {
|
||||
|
||||
@Test
|
||||
public void singleParameterlessCommand() {
|
||||
final AtomicBoolean b = new AtomicBoolean();
|
||||
var foo = Graph.merge(Graph.builder(Literal("foo"), createExecutor(b)).build());
|
||||
assertValid(foo, "foo", b);
|
||||
assertUnknown(foo, "bar");
|
||||
assertSyntaxError(foo, "foo bar baz");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void twoParameterlessCommand() {
|
||||
final AtomicBoolean b = new AtomicBoolean();
|
||||
final AtomicBoolean b1 = new AtomicBoolean();
|
||||
var graph = Graph.merge(
|
||||
Graph.builder(Literal("foo"), createExecutor(b)).build(),
|
||||
Graph.builder(Literal("bar"), createExecutor(b1)).build()
|
||||
);
|
||||
assertValid(graph, "foo", b);
|
||||
assertValid(graph, "bar", b1);
|
||||
assertUnknown(graph, "baz");
|
||||
assertSyntaxError(graph, "foo bar baz");
|
||||
assertSyntaxError(graph, "bar 25");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleCommandWithMultipleSyntax() {
|
||||
final AtomicBoolean add = new AtomicBoolean();
|
||||
final AtomicBoolean action = new AtomicBoolean();
|
||||
var foo = Graph.merge(Graph.builder(Literal("foo"))
|
||||
.append(Literal("add"),
|
||||
x -> x.append(Word("name"), createExecutor(add)))
|
||||
.append(Word("action").from("inc", "dec"),
|
||||
x -> x.append(ArgumentType.Integer("num"), createExecutor(action)))
|
||||
.build());
|
||||
assertValid(foo, "foo add test", add);
|
||||
assertValid(foo, "foo add inc", add);
|
||||
assertValid(foo, "foo add 157", add);
|
||||
assertValid(foo, "foo inc 157", action);
|
||||
assertValid(foo, "foo dec 157", action);
|
||||
assertSyntaxError(foo, "foo 15");
|
||||
assertSyntaxError(foo, "foo asd");
|
||||
assertSyntaxError(foo, "foo inc");
|
||||
assertSyntaxError(foo, "foo inc asd");
|
||||
assertSyntaxError(foo, "foo inc 15 dec");
|
||||
assertSyntaxError(foo, "foo inc 15 20");
|
||||
assertUnknown(foo, "bar");
|
||||
assertUnknown(foo, "add");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleCommandOptionalArgs() {
|
||||
final AtomicBoolean b = new AtomicBoolean();
|
||||
final AtomicReference<String> expectedFirstArg = new AtomicReference<>("T");
|
||||
var foo = Graph.merge(Graph.builder(Literal("foo"))
|
||||
.append(Word("a").setDefaultValue("A"),
|
||||
x -> x.append(Word("b").setDefaultValue("B"),
|
||||
x1 -> x1.append(Word("c").setDefaultValue("C"),
|
||||
x2 -> x2.append(Word("d").setDefaultValue("D"),
|
||||
new GraphImpl.ExecutionImpl(null, null, null,
|
||||
(sender, context) -> {
|
||||
b.set(true);
|
||||
assertEquals(expectedFirstArg.get(), context.get("a"));
|
||||
assertEquals("B", context.get("b"));
|
||||
assertEquals("C", context.get("c"));
|
||||
assertEquals("D", context.get("d"));
|
||||
}, null)))))
|
||||
.build());
|
||||
assertValid(foo, "foo T", b);
|
||||
expectedFirstArg.set("A");
|
||||
assertValid(foo, "foo", b);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleCommandSingleEnumArg() {
|
||||
enum A {a, b}
|
||||
final AtomicBoolean b = new AtomicBoolean();
|
||||
var foo = Graph.merge(Graph.builder(Literal("foo"))
|
||||
.append(ArgumentType.Enum("test", A.class), createExecutor(b))
|
||||
.build());
|
||||
assertValid(foo, "foo a", b);
|
||||
assertValid(foo, "foo b", b);
|
||||
assertSyntaxError(foo, "foo c");
|
||||
assertSyntaxError(foo, "foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasWithoutArgs() {
|
||||
final AtomicBoolean b = new AtomicBoolean();
|
||||
var foo = Graph.merge(Graph.builder(Word("").from("foo", "bar"), createExecutor(b))
|
||||
.build());
|
||||
assertValid(foo, "foo", b);
|
||||
assertValid(foo, "bar", b);
|
||||
assertUnknown(foo, "test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasWithArgs() {
|
||||
final AtomicBoolean b = new AtomicBoolean();
|
||||
var foo = Graph.merge(Graph.builder(Word("").from("foo", "bar"))
|
||||
.append(ArgumentType.Integer("test"), createExecutor(b))
|
||||
.build());
|
||||
assertValid(foo, "foo 1", b);
|
||||
assertValid(foo, "bar 1", b);
|
||||
assertSyntaxError(foo, "foo");
|
||||
assertSyntaxError(foo, "bar");
|
||||
}
|
||||
|
||||
private static void assertSyntaxError(Graph graph, String input) {
|
||||
assertInstanceOf(CommandParser.Result.KnownCommand.Invalid.class, parseCommand(graph, input));
|
||||
}
|
||||
|
||||
private static void assertUnknown(Graph graph, String input) {
|
||||
assertInstanceOf(CommandParser.Result.UnknownCommand.class, parseCommand(graph, input));
|
||||
}
|
||||
|
||||
private static void assertValid(Graph graph, String input, AtomicBoolean executorTest) {
|
||||
final CommandParser.Result result = parseCommand(graph, input);
|
||||
assertInstanceOf(CommandParser.Result.KnownCommand.Valid.class, result);
|
||||
result.executable().execute(null);
|
||||
assertTrue(executorTest.get(), "Parser returned valid syntax, but with the wrong executor.");
|
||||
executorTest.set(false);
|
||||
}
|
||||
|
||||
private static CommandParser.Result parseCommand(Graph graph, String input) {
|
||||
return CommandParser.parser().parse(graph, input);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Graph.Execution createExecutor(AtomicBoolean atomicBoolean) {
|
||||
return new GraphImpl.ExecutionImpl(null, null, null, (sender, context) -> atomicBoolean.set(true), null);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package net.minestom.server.command;
|
||||
|
||||
import net.minestom.server.api.Env;
|
||||
import net.minestom.server.api.EnvTest;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.suggestion.SuggestionEntry;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.network.packet.client.play.ClientTabCompletePacket;
|
||||
import net.minestom.server.network.packet.server.play.TabCompletePacket;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static net.minestom.server.command.builder.arguments.ArgumentType.Literal;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
@EnvTest
|
||||
public class CommandSuggestionIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void suggestion(Env env) {
|
||||
var instance = env.createFlatInstance();
|
||||
var connection = env.createConnection();
|
||||
var player = connection.connect(instance, new Pos(0, 42, 0)).join();
|
||||
|
||||
var command = new Command("test");
|
||||
command.addSyntax((sender, context) -> {
|
||||
|
||||
}, Literal("arg").setSuggestionCallback((sender, context, suggestion) -> {
|
||||
assertEquals(player, sender);
|
||||
assertNull(context.get("arg"));
|
||||
assertEquals("test", context.getCommandName());
|
||||
assertEquals("test te", context.getInput());
|
||||
suggestion.addEntry(new SuggestionEntry("test1"));
|
||||
}));
|
||||
|
||||
env.process().command().register(command);
|
||||
|
||||
var listener = connection.trackIncoming(TabCompletePacket.class);
|
||||
player.addPacketToQueue(new ClientTabCompletePacket(3, "test te"));
|
||||
player.interpretPacketQueue();
|
||||
|
||||
listener.assertSingle(tabCompletePacket -> {
|
||||
assertEquals(3, tabCompletePacket.transactionId());
|
||||
assertEquals(6, tabCompletePacket.start());
|
||||
assertEquals(2, tabCompletePacket.length());
|
||||
assertEquals(List.of(new TabCompletePacket.Match("test1", null)), tabCompletePacket.matches());
|
||||
});
|
||||
}
|
||||
}
|
@ -10,7 +10,8 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class CommandTestUtils {
|
||||
|
||||
@ -19,9 +20,20 @@ public class CommandTestUtils {
|
||||
final List<NodeStructure.TestNode> actualList = NodeStructure.fromString(NodeStructure.packetToString(packet));
|
||||
try {
|
||||
assertEquals(expectedList.size(), actualList.size(), "Different node counts");
|
||||
assertTrue(actualList.containsAll(expectedList), "Packet doesn't contain all expected nodes.");
|
||||
for (NodeStructure.TestNode expected : expectedList) {
|
||||
boolean found = false;
|
||||
for (NodeStructure.TestNode actual : actualList) {
|
||||
if (expected.equals(actual)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
fail("Packet doesn't contain " + expected.toString());
|
||||
}
|
||||
}
|
||||
} catch (AssertionFailedError error) {
|
||||
fail("Graphs didn't match. Actual graph from packet: " + CommandTestUtils.exportGarphvizDot(packet, false));
|
||||
fail("Graphs didn't match. Actual graph from packet: " + CommandTestUtils.exportGarphvizDot(packet, false), error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,19 +2,13 @@ package net.minestom.server.command;
|
||||
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.CommandContext;
|
||||
import net.minestom.server.command.builder.condition.CommandCondition;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static net.minestom.server.command.builder.arguments.ArgumentType.Literal;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class GraphConversionExecutorTest {
|
||||
@Test
|
||||
public void empty() {
|
||||
final Command foo = new Command("foo");
|
||||
var graph = Graph.fromCommand(foo);
|
||||
assertNull(graph.root().execution());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultCondition() {
|
||||
final Command foo = new Command("foo");
|
||||
@ -43,7 +37,10 @@ public class GraphConversionExecutorTest {
|
||||
|
||||
var graph = Graph.fromCommand(foo);
|
||||
assertEquals(1, graph.root().next().size());
|
||||
assertNull(graph.root().next().get(0).execution());
|
||||
var execution = graph.root().next().get(0).execution();
|
||||
assertNotNull(execution);
|
||||
assertNull(execution.condition());
|
||||
assertNotNull(execution.executor());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -72,6 +69,18 @@ public class GraphConversionExecutorTest {
|
||||
assertFalse(execution.test(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commandConditionFalse() {
|
||||
final Command foo = new Command("foo");
|
||||
foo.setCondition((sender, commandString) -> false);
|
||||
final Graph graph = Graph.fromCommand(foo);
|
||||
final Graph.Execution execution = graph.root().execution();
|
||||
assertNotNull(execution);
|
||||
final CommandCondition condition = execution.condition();
|
||||
assertNotNull(condition);
|
||||
assertFalse(condition.canUse(null, null));
|
||||
}
|
||||
|
||||
private static void dummyExecutor(CommandSender sender, CommandContext context) {
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user