This commit is contained in:
TheMode 2022-07-13 22:45:38 +02:00 committed by GitHub
parent 1cdee4a8e1
commit 366f08f7f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1363 additions and 415 deletions

View File

@ -157,6 +157,7 @@ public final class CommandManager {
* @return the {@link DeclareCommandsPacket} for {@code player}
*/
public @NotNull DeclareCommandsPacket createDeclareCommandsPacket(@NotNull Player player) {
return GraphBuilder.forPlayer(this.dispatcher.getCommands(), player).createPacket();
final Graph merged = Graph.merge(dispatcher.getCommands());
return GraphConverter.createPacket(merged, player);
}
}

View File

@ -0,0 +1,61 @@
package net.minestom.server.command;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.Argument;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnknownNullability;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
sealed interface Graph permits GraphImpl {
static @NotNull Builder builder(@NotNull Argument<?> argument) {
return new GraphImpl.BuilderImpl(argument);
}
static @NotNull Graph fromCommand(@NotNull Command command) {
return GraphImpl.fromCommand(command);
}
static @NotNull Graph merge(@NotNull Collection<@NotNull Command> commands) {
return GraphImpl.merge(commands);
}
static @NotNull Graph merge(@NotNull List<@NotNull Graph> graphs) {
return GraphImpl.merge(graphs);
}
static @NotNull Graph merge(@NotNull Graph @NotNull ... graphs) {
return merge(List.of(graphs));
}
@NotNull Node root();
boolean compare(@NotNull Graph graph, @NotNull Comparator comparator);
sealed interface Node permits GraphImpl.NodeImpl {
@NotNull Argument<?> argument();
@UnknownNullability Executor executor();
@NotNull List<@NotNull Node> next();
}
sealed interface Executor extends Predicate<CommandSender> permits GraphImpl.ExecutorImpl {
// TODO execute the node
}
sealed interface Builder permits GraphImpl.BuilderImpl {
@NotNull Builder append(@NotNull Argument<?> argument, @NotNull Consumer<Builder> consumer);
@NotNull Builder append(@NotNull Argument<?> argument);
@NotNull Graph build();
}
enum Comparator {
TREE
}
}

View File

@ -1,204 +0,0 @@
package net.minestom.server.command;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectSet;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandSyntax;
import net.minestom.server.command.builder.arguments.*;
import net.minestom.server.command.builder.condition.CommandCondition;
import net.minestom.server.command.builder.exception.IllegalCommandStructureException;
import net.minestom.server.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Stream;
final class GraphBuilder {
private final AtomicInteger idSource = new AtomicInteger();
private final ObjectSet<Node> nodes = new ObjectOpenHashSet<>();
private final ObjectSet<Supplier<Boolean>> redirectWaitList = new ObjectOpenHashSet<>();
private final Node root = rootNode();
private GraphBuilder() {
//no instance
}
private Node rootNode() {
final Node rootNode = new Node(idSource.getAndIncrement());
nodes.add(rootNode);
return rootNode;
}
private Node createLiteralNode(String name, @Nullable Node parent, boolean executable, @Nullable String[] aliases, @Nullable Integer redirectTo) {
if (aliases != null) {
final Node node = createLiteralNode(name, parent, executable, null, null);
for (String alias : aliases) {
createLiteralNode(alias, parent, executable, null, node.id());
}
return node;
} else {
final Node literalNode = new Node(idSource.getAndIncrement(), name, redirectTo);
literalNode.setExecutable(executable);
nodes.add(literalNode);
if (parent != null) parent.addChild(literalNode);
return literalNode;
}
}
private Node[] createArgumentNode(Argument<?> argument, boolean executable) {
final Node[] nodes;
Integer overrideRedirectTarget = null;
if (argument instanceof ArgumentEnum<?> argumentEnum) {
nodes = argumentEnum.entries().stream().map(x -> createLiteralNode(x, null, executable, null, null)).toArray(Node[]::new);
} else if (argument instanceof ArgumentGroup argumentGroup) {
nodes = argumentGroup.group().stream().map(x -> createArgumentNode(x, executable)).flatMap(Stream::of).toArray(Node[]::new);
} else if (argument instanceof ArgumentLoop<?> argumentLoop) {
overrideRedirectTarget = idSource.get()-1;
nodes = argumentLoop.arguments().stream().map(x -> createArgumentNode(x, executable)).flatMap(Stream::of).toArray(Node[]::new);
} else {
if (argument instanceof ArgumentCommand) {
return new Node[]{createLiteralNode(argument.getId(), null, false, null, 0)};
}
final int id = idSource.getAndIncrement();
nodes = new Node[] {argument instanceof ArgumentLiteral ? new Node(id, argument.getId(), null) : new Node(id, argument)};
}
for (Node node : nodes) {
node.setExecutable(executable);
this.nodes.add(node);
Integer finalOverrideRedirectTarget = overrideRedirectTarget;
if (finalOverrideRedirectTarget != null) {
redirectWaitList.add(() -> {
int target = finalOverrideRedirectTarget;
if (target != -1) {
node.setRedirectTarget(target);
return true;
}
return false;
});
}
}
return nodes;
}
private int tryResolveId(String[] path) {
if (path.length == 0) {
return root.id();
} else {
Node target = root;
for (String next : path) {
Node finalTarget = target;
final Optional<Node> result = nodes.stream().filter(finalTarget::isParentOf)
.filter(x -> x.name().equals(next)).findFirst();
if (result.isEmpty()) {
return -1;
} else {
target = result.get();
}
}
return target.id();
}
}
private void finalizeStructure() {
redirectWaitList.removeIf(Supplier::get);
if (redirectWaitList.size() > 0)
throw new IllegalCommandStructureException("Could not set redirects for all arguments! Did you provide a " +
"correct id path which doesn't rely on redirects?");
}
/**
* Creates the nodes for the given command
*
* @param command the command to add
* @param parent where to append the command's root (literal) node
* @param player a player if we should filter commands
*/
private void createCommand(Command command, Node parent, @Nullable Player player) {
if (player != null) {
// Check if user can use the command
final CommandCondition condition = command.getCondition();
if (condition != null && !condition.canUse(player, null)) return;
}
// Create the command's root node
final Node cmdNode = createLiteralNode(command.getName(), parent,
command.getDefaultExecutor() != null, command.getAliases(), null);
// Add syntax to the command
for (CommandSyntax syntax : command.getSyntaxes()) {
if (player != null) {
// Check if user can use the syntax
final CommandCondition condition = syntax.getCommandCondition();
if (condition != null && !condition.canUse(player, null)) continue;
}
boolean executable = false;
Node[] lastArgNodes = new Node[] {cmdNode}; // First arg links to cmd root
@NotNull Argument<?>[] arguments = syntax.getArguments();
for (int i = 0; i < arguments.length; i++) {
Argument<?> argument = arguments[i];
// Determine if command is executable here
if (executable && argument.getDefaultValue() == null) {
// Optional arg was followed by a non-optional
throw new IllegalCommandStructureException("Optional argument was followed by a non-optional one.");
}
if (!executable && i < arguments.length-1 && arguments[i+1].getDefaultValue() != null || i+1 == arguments.length) {
executable = true;
}
// Append current node to previous
final Node[] argNodes = createArgumentNode(argument, executable);
for (Node lastArgNode : lastArgNodes) {
lastArgNode.addChild(argNodes);
}
lastArgNodes = argNodes;
}
}
// Add subcommands
for (Command subcommand : command.getSubcommands()) {
createCommand(subcommand, cmdNode, player);
}
}
public static NodeGraph forPlayer(@NotNull Set<Command> commands, Player player) {
final GraphBuilder builder = new GraphBuilder();
if (GraphBuilder.class.desiredAssertionStatus()) {
// Detect infinite recursion
for (Command command : commands) {
final HashSet<Command> processed = new HashSet<>();
final Stack<Command> stack = new Stack<>();
stack.push(command);
while (!stack.isEmpty()) {
final Command pop = stack.pop();
if (!processed.add(pop)) {
throw new IllegalCommandStructureException("Infinite recursion detected in command: "+command.getName());
} else {
stack.addAll(pop.getSubcommands());
}
}
builder.createCommand(command, builder.root, player);
}
} else {
for (Command command : commands) {
builder.createCommand(command, builder.root, player);
}
}
builder.finalizeStructure();
return new NodeGraph(builder.nodes, builder.root.id());
}
public static NodeGraph forServer(@NotNull Set<Command> commands) {
return forPlayer(commands, null);
}
}

View File

@ -0,0 +1,164 @@
package net.minestom.server.command;
import net.minestom.server.command.builder.arguments.*;
import net.minestom.server.entity.Player;
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.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
final class GraphConverter {
private GraphConverter() {
//no instance
}
@Contract("_, _ -> new")
public static DeclareCommandsPacket createPacket(Graph graph, @Nullable Player player) {
List<DeclareCommandsPacket.Node> nodes = new ArrayList<>();
List<Consumer<Integer>> rootRedirect = new ArrayList<>();
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);
}
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) {
final Graph.Executor executor = graphNode.executor();
if (player != null && executor != null) {
if (!executor.test(player)) return new int[0];
}
final Argument<?> argument = graphNode.argument();
final List<Graph.Node> children = graphNode.next();
final DeclareCommandsPacket.Node node = new DeclareCommandsPacket.Node();
int[] packetNodeChildren = new int[children.size()];
for (int i = 0; i < packetNodeChildren.length; i++) {
final int[] append = append(children.get(i), to, rootRedirect, id, redirect, redirectSetters, player);
if (append.length == 1) {
packetNodeChildren[i] = append[0];
} else {
packetNodeChildren = Arrays.copyOf(packetNodeChildren, packetNodeChildren.length + append.length - 1);
System.arraycopy(append, 0, packetNodeChildren, i, append.length);
i += append.length;
}
}
node.children = packetNodeChildren;
if (argument instanceof ArgumentLiteral literal) {
if (literal.getId().isEmpty()) {
node.flags = 0; //root
} else {
node.flags = literal(false, false);
node.name = argument.getId();
if (redirect != null) {
node.flags |= 0x8;
redirectSetters.add(() -> node.redirectedNode = redirect.get());
}
}
to.add(node);
return new int[]{id.getAndIncrement()};
} else {
if (argument instanceof ArgumentCommand) {
node.flags = literal(false, true);
node.name = argument.getId();
rootRedirect.add(i -> node.redirectedNode = i);
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();
final int[] res = new int[entries.size()];
for (int i = 0; i < res.length; i++) {
String entry = entries.get(i);
final DeclareCommandsPacket.Node subNode = new DeclareCommandsPacket.Node();
subNode.children = node.children;
subNode.flags = literal(false, false);
subNode.name = entry;
if (redirect != null) {
subNode.flags |= 0x8;
redirectSetters.add(() -> subNode.redirectedNode = redirect.get());
}
to.add(subNode);
res[i] = id.getAndIncrement();
}
return res;
} else if (argument instanceof ArgumentGroup special) {
List<Argument<?>> entries = special.group();
int[] res = null;
int[] last = new int[0];
for (int i = 0; i < entries.size(); i++) {
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);
for (int n : l) {
to.get(n).children = node.children;
}
for (int n : last) {
to.get(n).children = l;
}
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);
last = res;
} else {
final int[] l = append(new GraphImpl.NodeImpl(entry, null, List.of()), to, rootRedirect, id, null, redirectSetters, player);
for (int n : last) {
to.get(n).children = l;
}
last = l;
}
}
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; 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);
if (append.length == 1) {
res[i] = append[0];
} else {
res = Arrays.copyOf(res, res.length + append.length - 1);
System.arraycopy(append, 0, res, i, append.length);
i += append.length;
}
}
r.set(id.get());
setters.forEach(Runnable::run);
return res;
} else {
node.flags = arg(false, argument.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());
}
to.add(node);
return new int[]{id.getAndIncrement()};
}
}
}
private static byte literal(boolean executable, boolean hasRedirect) {
return DeclareCommandsPacket.getFlag(DeclareCommandsPacket.NodeType.LITERAL, executable, hasRedirect, false);
}
private static byte arg(boolean executable, boolean hasSuggestion) {
return DeclareCommandsPacket.getFlag(DeclareCommandsPacket.NodeType.ARGUMENT, executable, false, hasSuggestion);
}
}

View File

@ -0,0 +1,153 @@
package net.minestom.server.command;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandSyntax;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.condition.CommandCondition;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static net.minestom.server.command.builder.arguments.ArgumentType.Literal;
record GraphImpl(NodeImpl root) implements Graph {
static GraphImpl fromCommand(Command command) {
return new GraphImpl(NodeImpl.command(command));
}
static Graph merge(Collection<Command> commands) {
return new GraphImpl(NodeImpl.rootCommands(commands));
}
static GraphImpl merge(List<Graph> graphs) {
final List<Node> children = graphs.stream().map(Graph::root).toList();
final NodeImpl root = new NodeImpl(Literal(""), null, children);
return new GraphImpl(root);
}
@Override
public boolean compare(@NotNull Graph graph, @NotNull Comparator comparator) {
return compare(root, graph.root(), comparator);
}
record BuilderImpl(Argument<?> argument, List<BuilderImpl> children) implements Graph.Builder {
public BuilderImpl(Argument<?> argument) {
this(argument, new ArrayList<>());
}
@Override
public Graph.@NotNull Builder append(@NotNull Argument<?> argument, @NotNull Consumer<Graph.Builder> consumer) {
BuilderImpl builder = new BuilderImpl(argument);
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()));
return this;
}
@Override
public @NotNull GraphImpl build() {
return new GraphImpl(NodeImpl.fromBuilder(this));
}
}
record NodeImpl(Argument<?> argument, ExecutorImpl executor, List<Graph.Node> next) implements Graph.Node {
static NodeImpl fromBuilder(BuilderImpl builder) {
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));
}
static NodeImpl command(Command command) {
return ConversionNode.fromCommand(command).toNode();
}
static NodeImpl rootCommands(Collection<Command> commands) {
return ConversionNode.rootConv(commands).toNode();
}
}
record ExecutorImpl(Predicate<CommandSender> predicate) implements Graph.Executor {
@Override
public boolean test(CommandSender commandSender) {
return predicate.test(commandSender);
}
static ExecutorImpl fromCommand(Command command) {
final CommandCondition condition = command.getCondition();
if (condition == null) return null;
return new ExecutorImpl(commandSender -> condition.canUse(commandSender, null));
}
static ExecutorImpl fromSyntax(CommandSyntax syntax) {
final CommandCondition condition = syntax.getCommandCondition();
if (condition == null) return null;
return new ExecutorImpl(commandSender -> condition.canUse(commandSender, null));
}
}
private record ConversionNode(Argument<?> argument, ExecutorImpl executor,
Map<Argument<?>, ConversionNode> nextMap) {
ConversionNode(Argument<?> argument, ExecutorImpl executor) {
this(argument, executor, new LinkedHashMap<>());
}
private NodeImpl toNode() {
Node[] nodes = new NodeImpl[nextMap.size()];
int i = 0;
for (var entry : nextMap.values()) nodes[i++] = entry.toNode();
return new NodeImpl(argument, executor, List.of(nodes));
}
static ConversionNode fromCommand(Command command) {
ConversionNode root = new ConversionNode(Literal(command.getName()), ExecutorImpl.fromCommand(command));
// Syntaxes
for (CommandSyntax syntax : command.getSyntaxes()) {
ConversionNode syntaxNode = root;
for (Argument<?> arg : syntax.getArguments()) {
boolean last = arg == syntax.getArguments()[syntax.getArguments().length - 1];
syntaxNode = syntaxNode.nextMap.computeIfAbsent(arg, argument -> {
var ex = last ? ExecutorImpl.fromSyntax(syntax) : null;
return new ConversionNode(argument, ex);
});
}
}
// Subcommands
for (Command subcommand : command.getSubcommands()) {
root.nextMap.put(Literal(subcommand.getName()), fromCommand(subcommand));
}
return root;
}
static ConversionNode rootConv(Collection<Command> commands) {
Map<Argument<?>, ConversionNode> next = new LinkedHashMap<>(commands.size());
for (Command command : commands) {
final ConversionNode conv = fromCommand(command);
next.put(conv.argument, conv);
}
return new ConversionNode(Literal(""), null, next);
}
}
static boolean compare(@NotNull Node first, Node second, @NotNull Comparator comparator) {
return switch (comparator) {
case TREE -> {
if (!first.argument().equals(second.argument())) yield false;
if (first.next().size() != second.next().size()) yield false;
for (int i = 0; i < first.next().size(); i++) {
if (!compare(first.next().get(i), second.next().get(i), comparator)) {
yield false;
}
}
yield true;
}
};
}
}

View File

@ -1,105 +0,0 @@
package net.minestom.server.command;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.ints.IntSets;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
final class Node {
private final int id;
private final IntSet children = new IntOpenHashSet();
private final IntSet childrenView = IntSets.unmodifiable(children);
private final DeclareCommandsPacket.NodeType type;
private String name;
private Integer redirectTarget;
private Argument<?> argument;
private boolean executable;
Node(int id, DeclareCommandsPacket.NodeType type) {
this.id = id;
this.type = type;
}
Node(int id) {
this(id, DeclareCommandsPacket.NodeType.ROOT);
}
Node(int id, String name, Integer redirectTarget) {
this(id, DeclareCommandsPacket.NodeType.LITERAL);
setName(name);
setRedirectTarget(redirectTarget);
}
Node(int id, Argument<?> argument) {
this(id, DeclareCommandsPacket.NodeType.ARGUMENT);
setName(argument.getId());
this.argument = argument;
}
public void setExecutable(boolean executable) {
this.executable = executable;
}
public String name() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setRedirectTarget(Integer redirectTarget) {
this.redirectTarget = redirectTarget;
}
public void addChild(Node ...nodes) {
for (Node node : nodes) {
children.add(node.id);
}
}
public boolean isParentOf(Node node) {
return children.contains(node.id());
}
public int id() {
return id;
}
public DeclareCommandsPacket.NodeType type() {
return type;
}
public IntSet children() {
return childrenView;
}
public Integer redirectTarget() {
return redirectTarget;
}
public boolean isRoot() {
return type == DeclareCommandsPacket.NodeType.ROOT;
}
public DeclareCommandsPacket.Node getPacketNode() {
final DeclareCommandsPacket.Node node = new DeclareCommandsPacket.Node();
node.children = children.toIntArray();
node.flags = DeclareCommandsPacket.getFlag(type, executable, redirectTarget != null,
type == DeclareCommandsPacket.NodeType.ARGUMENT && argument.hasSuggestion());
node.name = name;
if (redirectTarget != null) {
node.redirectedNode = redirectTarget;
}
if (type == DeclareCommandsPacket.NodeType.ARGUMENT) {
node.properties = argument.nodeProperties();
node.parser = argument.parser();
if (argument.hasSuggestion()) {
//noinspection ConstantConditions
node.suggestionsType = argument.suggestionType().getIdentifier();
}
}
return node;
}
}

View File

@ -1,41 +0,0 @@
package net.minestom.server.command;
import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
import it.unimi.dsi.fastutil.objects.ObjectList;
import it.unimi.dsi.fastutil.objects.ObjectSet;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
import java.util.Comparator;
import java.util.List;
class NodeGraph {
private final ObjectList<Node> nodes;
private final Node root;
NodeGraph(ObjectSet<Node> nodes, int rootId) {
this.nodes = new ObjectImmutableList<>(nodes.stream().sorted(Comparator.comparing(Node::id)).toList());
this.root = this.nodes.get(rootId);
assert root.isRoot() : "rootId doesn't point to the root node";
assert this.nodes.stream().filter(Node::isRoot).count() == 1 : "Invalid root node count!";
}
public Node resolveId(int id) {
return nodes.get(id);
}
public List<Node> getChildren(Node node) {
return node.children().intStream().mapToObj(this::resolveId).toList();
}
public @Nullable Node getRedirectTarget(Node node) {
final Integer target = node.redirectTarget();
return target == null ? null : resolveId(target);
}
@Contract("-> new")
public DeclareCommandsPacket createPacket() {
return new DeclareCommandsPacket(nodes.stream().map(Node::getPacketNode).toList(), root.id());
}
}

View File

@ -0,0 +1,205 @@
package net.minestom.server.command;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.entity.Player;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
import org.junit.jupiter.api.Test;
import java.util.Set;
import java.util.UUID;
@SuppressWarnings("ConstantConditions")
public class CommandPacketFilteringTest {
private static final Player PLAYER = new Player(UUID.randomUUID(), "", null);
@Test
public void singleCommandFilteredFalse() {
final Command foo = new Command("foo");
foo.setCondition(((sender, commandString) -> false));
assertFiltering(foo, "");
}
@Test
public void singleCommandFilteredTrue() {
final Command foo = new Command("foo");
foo.setCondition(((sender, commandString) -> true));
assertFiltering(foo, """
foo=%
0->foo
""");
}
@Test
public void singleCommandUnfiltered() {
final Command foo = new Command("foo");
assertFiltering(foo, """
foo=%
0->foo
""");
}
@Test
public void singleCommandFilteredTrueWithFilteredSubcommandTrueWithFilteredSyntaxFalse() {
final Command foo = new Command("foo");
foo.setCondition((sender, commandString) -> true);
final Command bar = new Command("bar");
bar.setCondition((sender, commandString) -> true);
foo.addSubcommand(bar);
bar.addConditionalSyntax((sender, commandString) -> false, null, ArgumentType.Literal("baz"));
assertFiltering(foo, """
foo bar=%
0->foo
foo->bar
""");
}
@Test
public void singleCommandFilteredTrueWithFilteredSubcommandFalse() {
final Command foo = new Command("foo");
foo.setCondition((sender, commandString) -> true);
final Command bar = new Command("bar");
bar.setCondition((sender, commandString) -> false);
foo.addSubcommand(bar);
assertFiltering(foo, """
foo=%
0->foo
""");
}
@Test
public void singleCommandFilteredTrueWithFilteredSubcommandTrue() {
final Command foo = new Command("foo");
foo.setCondition((sender, commandString) -> true);
final Command bar = new Command("bar");
bar.setCondition((sender, commandString) -> true);
foo.addSubcommand(bar);
assertFiltering(foo, """
foo bar=%
0->foo
foo->bar
""");
}
@Test
public void singleCommandFilteredTrueWithFilteredSubcommandTrueWithFilteredSyntaxBoth() {
final Command foo = new Command("foo");
foo.setCondition((sender, commandString) -> true);
final Command bar = new Command("bar");
bar.setCondition((sender, commandString) -> true);
foo.addSubcommand(bar);
bar.addConditionalSyntax((sender, commandString) -> true, null, ArgumentType.Literal("true"));
bar.addConditionalSyntax((sender, commandString) -> false, null, ArgumentType.Literal("false"));
assertFiltering(foo, """
foo bar true=%
0->foo
foo->bar
bar->true
""");
}
@Test
public void singleCommandConditionalArgGroupTrue() {
final Command foo = new Command("foo");
foo.addConditionalSyntax((sender, commandString) -> true, null, ArgumentType.Group("test", ArgumentType.Literal("bar")));
assertFiltering(foo, """
foo bar=%
0->foo
foo->bar
""");
}
@Test
public void singleCommandConditionalArgGroupFalse() {
final Command foo = new Command("foo");
foo.addConditionalSyntax((sender, commandString) -> false, null, ArgumentType.Group("test", ArgumentType.Literal("foo")));
assertFiltering(foo, """
foo=%
0->foo
""");
}
@Test
public void singleCommandUnconditionalArgGroup() {
final Command foo = new Command("foo");
foo.addSyntax(null, ArgumentType.Group("test", ArgumentType.Literal("bar")));
assertFiltering(foo, """
foo bar=%
0->foo
foo->bar
""");
}
@Test
public void singleCommandConditionalArgGroupTrue2() {
final Command foo = new Command("foo");
foo.addConditionalSyntax((sender, commandString) -> true, null, ArgumentType.Group("test", ArgumentType.Literal("bar"), ArgumentType.Literal("baz")));
assertFiltering(foo, """
foo bar baz=%
0->foo
foo->bar
bar->baz
""");
}
@Test
public void singleCommandConditionalArgGroupFalse2() {
final Command foo = new Command("foo");
foo.addConditionalSyntax((sender, commandString) -> false, null, ArgumentType.Group("test", ArgumentType.Literal("foo"), ArgumentType.Literal("baz")));
assertFiltering(foo, """
foo=%
0->foo
""");
}
@Test
public void singleCommandUnconditionalArgGroup2() {
final Command foo = new Command("foo");
foo.addSyntax(null, ArgumentType.Group("test", ArgumentType.Literal("bar"), ArgumentType.Literal("baz")));
assertFiltering(foo, """
foo bar baz=%
0->foo
foo->bar
bar->baz
""");
}
@Test
public void singleCommandUnconditionalArgLoop() {
final Command foo = new Command("foo");
foo.addSyntax(null, ArgumentType.Loop("test", ArgumentType.Literal("bar"), ArgumentType.Literal("baz")));
assertFiltering(foo, """
foo bar baz=%
0->foo
foo->bar baz
bar baz+>foo
""");
}
@Test
public void singleCommandConditionalArgLoopTrue() {
final Command foo = new Command("foo");
foo.addConditionalSyntax((sender, commandString) -> true, null, ArgumentType.Loop("test", ArgumentType.Literal("bar"), ArgumentType.Literal("baz")));
assertFiltering(foo, """
foo bar baz=%
0->foo
foo->bar baz
bar baz+>foo
""");
}
@Test
public void singleCommandConditionalArgLoopFalse() {
final Command foo = new Command("foo");
foo.addConditionalSyntax((sender, commandString) -> false, null, ArgumentType.Loop("test", ArgumentType.Literal("bar"), ArgumentType.Literal("baz")));
assertFiltering(foo, """
foo=%
0->foo
""");
}
private void assertFiltering(Command command, String expectedStructure) {
final DeclareCommandsPacket packet = GraphConverter.createPacket(Graph.merge(Set.of(command)), PLAYER);
CommandTestUtils.assertPacket(packet, expectedStructure);
}
}

View File

@ -0,0 +1,175 @@
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.arguments.ArgumentType;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class CommandPacketTest {
@Test
public void singleCommandWithOneSyntax() {
final Command foo = new Command("foo");
foo.addSyntax(CommandPacketTest::dummyExecutor, ArgumentType.Integer("bar"));
final DeclareCommandsPacket packet = GraphConverter.createPacket(Graph.merge(Graph.fromCommand(foo)), null);
assertEquals(3, packet.nodes().size());
final DeclareCommandsPacket.Node root = packet.nodes().get(packet.rootIndex());
assertNotNull(root);
assertNodeType(DeclareCommandsPacket.NodeType.ROOT, root.flags);
assertEquals(1, root.children.length);
final DeclareCommandsPacket.Node cmd = packet.nodes().get(root.children[0]);
assertNotNull(cmd);
assertNodeType(DeclareCommandsPacket.NodeType.LITERAL, cmd.flags);
assertEquals(1, cmd.children.length);
assertEquals("foo", cmd.name);
final DeclareCommandsPacket.Node arg = packet.nodes().get(cmd.children[0]);
assertNotNull(arg);
assertNodeType(DeclareCommandsPacket.NodeType.ARGUMENT, arg.flags);
assertEquals(0, arg.children.length);
assertEquals("bar", arg.name);
}
@Test
public void executeLike() {
enum Dimension {OVERWORLD, THE_NETHER, THE_END}
final Command execute = new Command("execute");
execute.addSyntax(CommandPacketTest::dummyExecutor, ArgumentType.Loop("params",
ArgumentType.Group("facing", ArgumentType.Literal("facing"), ArgumentType.RelativeVec3("pos")),
ArgumentType.Group("at", ArgumentType.Literal("at"), ArgumentType.Entity("targets")),
ArgumentType.Group("as", ArgumentType.Literal("as"), ArgumentType.Entity("targets")),
ArgumentType.Group("in", ArgumentType.Literal("in"), ArgumentType.Enum("dimension", Dimension.class)),
ArgumentType.Group("run", ArgumentType.Command("run"))
));
var graph = Graph.fromCommand(execute);
assertPacketGraph("""
execute facing at as in run=%
overworld the_nether the_end=§
0->execute
atEnt asEnt=targets minecraft:entity 0
execute->facing at as in run
at->atEnt
as->asEnt
in->overworld the_nether the_end
pos=! minecraft:vec3
facing->pos
pos atEnt asEnt overworld the_nether the_end+>execute
run+>0
""", graph);
}
@Test
public void singleCommandTwoEnum() {
var graph = Graph.builder(ArgumentType.Literal("foo"))
.append(ArgumentType.Enum("bar", A.class), b -> b.append(ArgumentType.Enum("baz", B.class)))
.build();
assertPacketGraph("""
foo=%
a b c d e f=§
0->foo
foo->a b c
a b c->d e f
""", graph);
}
@Test
public void singleCommandRestrictedWord() {
var graph = Graph.builder(ArgumentType.Literal("foo"))
.append(ArgumentType.Word("bar").from("A", "B", "C"))
.build();
assertPacketGraph("""
foo=%
a b c=§
0->foo
foo->a b c
""", graph);
}
@Test
public void singleCommandWord() {
var graph = Graph.builder(ArgumentType.Literal("foo"))
.append(ArgumentType.Word("bar"))
.build();
assertPacketGraph("""
foo=%
bar=! brigadier:string 0
0->foo
foo->bar
""", graph);
}
@Test
public void singleCommandCommandAfterEnum() {
var graph = Graph.builder(ArgumentType.Literal("foo"))
.append(ArgumentType.Enum("bar", A.class), b -> b.append(ArgumentType.Command("baz")))
.build();
assertPacketGraph("""
foo baz=%
a b c=§
0->foo
foo->a b c
a b c->baz
baz+>0
""", graph);
}
@Test
public void twoCommandIntEnumInt() {
var graph = Graph.builder(ArgumentType.Literal("foo"))
.append(ArgumentType.Integer("int1"), b -> b.append(ArgumentType.Enum("test", A.class), c -> c.append(ArgumentType.Integer("int2"))))
.build();
var graph2 = Graph.builder(ArgumentType.Literal("bar"))
.append(ArgumentType.Integer("int3"), b -> b.append(ArgumentType.Enum("test", B.class), c -> c.append(ArgumentType.Integer("int4"))))
.build();
assertPacketGraph("""
foo bar=%
0->foo bar
a b c d e f=§
int1 int2 int3 int4=! brigadier:integer 0
foo->int1
bar->int3
int1->a b c
int3->d e f
a b c->int2
d e f->int4
""", graph, graph2);
}
@Test
public void singleCommandTwoGroupOfIntInt() {
var graph = Graph.builder(ArgumentType.Literal("foo"))
.append(ArgumentType.Group("1", ArgumentType.Integer("int1"), ArgumentType.Integer("int2")),
b -> b.append(ArgumentType.Group("2", ArgumentType.Integer("int3"), ArgumentType.Integer("int4"))))
.build();
assertPacketGraph("""
foo=%
int1 int2 int3 int4=! brigadier:integer 0
0->foo
foo->int1
int1->int2
int2->int3
int3->int4
""", graph);
}
static void assertPacketGraph(String expected, Graph... graphs) {
var packet = GraphConverter.createPacket(Graph.merge(graphs), null);
CommandTestUtils.assertPacket(packet, expected);
}
enum A {A, B, C}
enum B {D, E, F}
enum C {G, H, I, J, K}
private static void assertNodeType(DeclareCommandsPacket.NodeType expected, byte flags) {
assertEquals(expected, DeclareCommandsPacket.NodeType.values()[flags & 0x03]);
}
private static void dummyExecutor(CommandSender sender, CommandContext context) {
}
}

View File

@ -0,0 +1,277 @@
package net.minestom.server.command;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
import org.jetbrains.annotations.NotNull;
import org.opentest4j.AssertionFailedError;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
public class CommandTestUtils {
public static void assertPacket(DeclareCommandsPacket packet, String expectedStructure) {
final List<NodeStructure.TestNode> expectedList = NodeStructure.fromString("0\n0=$root$\n" + expectedStructure);
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.");
} catch (AssertionFailedError error) {
fail("Graphs didn't match. Actual graph from packet: " + CommandTestUtils.exportGarphvizDot(packet, false));
}
}
static class NodeStructure {
private static final Map<Character, Function<String, Collection<String>>> functions = Map.of(
'!', s -> {
final String[] strings = splitDeclaration(s);
final ArrayList<String> result = new ArrayList<>();
for (String s1 : strings[0].split(" ")) {
result.add(s1+"="+(strings[1].replaceAll("!", s1)));
}
return result;
},
'%', s -> {
final String[] strings = splitDeclaration(s);
final ArrayList<String> result = new ArrayList<>();
for (String s1 : strings[0].split(" ")) {
result.add(s1+"="+(strings[1].replaceAll("%", "'"+s1+"'")));
}
return result;
},
'§', s -> {
final String[] strings = splitDeclaration(s);
final ArrayList<String> result = new ArrayList<>();
for (String s1 : strings[0].split(" ")) {
result.add(s1+"="+(strings[1].replaceAll("§", "'"+(s1.toUpperCase(Locale.ROOT))+"'")));
}
return result;
}
);
private static final Set<Character> placeholders = functions.keySet();
static String packetToString(DeclareCommandsPacket packet) {
final char lineSeparator = '\n';
final StringBuilder builder = new StringBuilder();
builder.append(packet.rootIndex());
builder.append(lineSeparator);
@NotNull List<DeclareCommandsPacket.Node> nodes = packet.nodes();
for (int i = 0; i < nodes.size(); i++) {
DeclareCommandsPacket.Node node = nodes.get(i);
builder.append(i);
builder.append('=');
// Meta
if ((node.flags & 0x3) == 0) {
builder.append("$root$");
} else {
if ((node.flags & 0x3) == 1) {
builder.append("'");
builder.append(node.name);
builder.append("'");
} else {
builder.append(node.name);
builder.append(' ');
builder.append(node.parser);
if (node.properties != null) {
builder.append(' ');
builder.append(new BigInteger(node.properties).toString(16));
}
}
}
if ((node.flags & 0x4) == 0x4) {
builder.append(" executable");
}
if ((node.flags & 0x10) == 0x10) {
builder.append(' ');
builder.append(node.suggestionsType);
}
builder.append(lineSeparator);
if (node.children.length > 0) {
builder.append(i);
builder.append("->");
builder.append(Arrays.stream(node.children).mapToObj(String::valueOf).collect(Collectors.joining(" ")));
builder.append(lineSeparator);
}
if ((node.flags & 0x8) == 0x8) {
builder.append(i);
builder.append("+>");
builder.append(node.redirectedNode);
builder.append(lineSeparator);
}
}
return builder.toString();
}
private static String[] splitDeclaration(String input) {
return input.split("=", 2);
}
private static List<String> preProcessString(String string) {
final List<String> strings = Arrays.stream(string.split("\n")).toList();
final ArrayList<String> result = new ArrayList<>();
for (String s : strings) {
if (s.indexOf('=') > -1) {
boolean match = false;
for (Character placeholder : placeholders) {
if (s.indexOf(placeholder) > -1) {
result.addAll(functions.get(placeholder).apply(s));
match = true;
break;
}
}
if (!match) {
final int spaceIndex = s.indexOf(" ");
if (spaceIndex > -1 && spaceIndex < s.indexOf('=')) {
final String[] split = s.split("=", 2);
for (String s1 : split[0].split(" ")) {
result.add(s1+"="+split[1]);
}
} else {
result.add(s);
}
}
} else {
final int spaceIndex = s.indexOf(" ");
if (spaceIndex > -1 && spaceIndex < s.indexOf('-')) {
final String[] split = s.split("-", 2);
for (String s1 : split[0].split(" ")) {
result.add(s1+"-"+split[1]);
}
} else if (spaceIndex > -1 && spaceIndex < s.indexOf('+')) {
final String[] split = s.split("\\+", 2);
for (String s1 : split[0].split(" ")) {
result.add(s1+"+"+split[1]);
}
} else {
result.add(s);
}
}
}
return result;
}
static List<TestNode> fromString(String input) {
Map<String, String[]> references = new HashMap<>();
Map<String, TestNode> nodes = new HashMap<>();
final List<String> strings = preProcessString(input);
String rootId = strings.get(0);
for (String s : strings.stream().skip(0).toList()) {
if (s.length() < 3) continue; //invalid line
final int declareSeparator = s.indexOf('=');
if (declareSeparator > -1) {
final String id = s.substring(0, declareSeparator);
final String meta = s.substring(declareSeparator + 1);
nodes.put(id, new TestNode(new ArrayList<>(), meta, new AtomicReference<>()));
} else {
final int childSeparator = s.indexOf('-');
if (childSeparator > -1) {
references.put(s.substring(0, childSeparator), s.substring(childSeparator + 2).split(" "));
} else {
final int redirectSeparator = s.indexOf('+');
references.put(s.substring(0, redirectSeparator), new String[]{null, s.substring(redirectSeparator + 2)});
}
}
}
final ArrayList<TestNode> result = new ArrayList<>();
List<Runnable> redirectSetters = new ArrayList<>();
resolveNode(rootId, references, nodes, result, new HashMap<>(), redirectSetters, "");
redirectSetters.forEach(Runnable::run);
return result;
}
private static String resolveNode(String id, Map<String, String[]> references,
Map<String, TestNode> nodes, ArrayList<TestNode> result,
Map<String, String> nameToMetaPath,
List<Runnable> redirectSetters, String metaPath) {
final TestNode node = nodes.get(id);
final String[] refs = references.get(id);
final String path = metaPath + "#" + node.meta;
if (refs == null) {
result.add(node);
nameToMetaPath.put(id, path);
return path;
} else if (refs[0] == null) {
redirectSetters.add(() -> node.redirect.set(nameToMetaPath.get(refs[1])));
} else {
for (String ref : refs) {
node.children.add(resolveNode(ref, references, nodes, result, nameToMetaPath, redirectSetters, path));
}
}
result.add(node);
nameToMetaPath.put(id, path);
return path;
}
record TestNode(List<String> children, String meta, AtomicReference<String> redirect) {
@Override
public boolean equals(Object obj) {
if (obj instanceof TestNode that) {
return this.meta.equals(that.meta) && Objects.equals(this.redirect.get(), that.redirect.get()) &&
this.children.containsAll(that.children) && this.children.size() == that.children.size();
} else {
return false;
}
}
}
}
static String exportGarphvizDot(DeclareCommandsPacket packet, boolean prettyPrint) {
final StringBuilder builder = new StringBuilder();
final char statementSeparator = ';';
builder.append("digraph G {");
builder.append("rankdir=LR");
builder.append(statementSeparator);
builder.append(packet.rootIndex());
builder.append(" [label=\"root\",shape=rectangle]");
builder.append(statementSeparator);
@NotNull List<DeclareCommandsPacket.Node> nodes = packet.nodes();
for (int i = 0; i < nodes.size(); i++) {
DeclareCommandsPacket.Node node = nodes.get(i);
if ((node.flags & 0x3) != 0) {
builder.append(i);
builder.append(" [label=");
builder.append('"');
if ((node.flags & 0x3) == 1) {
builder.append("'");
builder.append(node.name);
builder.append("'");
} else {
builder.append(node.name);
}
builder.append('"');
if ((node.flags & 0x4) == 0x4) {
builder.append(",bgcolor=gray,style=filled");
}
builder.append("]");
builder.append(statementSeparator);
}
if (node.children.length == 0 && (node.flags & 0x8) == 0) continue;
builder.append(i);
builder.append(" -> { ");
if ((node.flags & 0x8) == 0) {
builder.append(Arrays.stream(node.children).mapToObj(Integer::toString).collect(Collectors.joining(" ")));
builder.append(" }");
builder.append(statementSeparator);
} else {
builder.append(node.redirectedNode);
builder.append(" } [style = dotted]");
builder.append(statementSeparator);
}
}
builder.append("}");
if (prettyPrint)
return builder.toString()
.replaceFirst("\\{r", "{\n r")
.replaceAll(";", "\n ")
.replaceFirst(" {2}}$", "}\n");
else
return builder.toString();
}
}

View File

@ -0,0 +1,77 @@
package net.minestom.server.command;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
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().executor());
}
@Test
public void defaultCondition() {
final Command foo = new Command("foo");
// Constant true
{
foo.setCondition((sender, commandString) -> true);
var graph = Graph.fromCommand(foo);
var executor = graph.root().executor();
assertNotNull(executor);
assertTrue(executor.test(null));
}
// Constant false
{
foo.setCondition((sender, commandString) -> false);
var graph = Graph.fromCommand(foo);
var executor = graph.root().executor();
assertNotNull(executor);
assertFalse(executor.test(null));
}
}
@Test
public void emptySyntaxCondition() {
final Command foo = new Command("foo");
foo.addSyntax(GraphConversionExecutorTest::dummyExecutor, Literal("first"));
var graph = Graph.fromCommand(foo);
assertEquals(1, graph.root().next().size());
assertNull(graph.root().next().get(0).executor());
}
@Test
public void syntaxConditionTrue() {
final Command foo = new Command("foo");
foo.addConditionalSyntax((sender, context) -> true,
GraphConversionExecutorTest::dummyExecutor, Literal("first"));
var graph = Graph.fromCommand(foo);
assertEquals(1, graph.root().next().size());
var executor = graph.root().next().get(0).executor();
assertNotNull(executor);
assertTrue(executor.test(null));
}
@Test
public void syntaxConditionFalse() {
final Command foo = new Command("foo");
foo.addConditionalSyntax((sender, context) -> false,
GraphConversionExecutorTest::dummyExecutor, Literal("first"));
var graph = Graph.fromCommand(foo);
assertEquals(1, graph.root().next().size());
var executor = graph.root().next().get(0).executor();
assertNotNull(executor);
assertFalse(executor.test(null));
}
private static void dummyExecutor(CommandSender sender, CommandContext context) {
}
}

View File

@ -0,0 +1,115 @@
package net.minestom.server.command;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import org.junit.jupiter.api.Test;
import static net.minestom.server.command.builder.arguments.ArgumentType.Enum;
import static net.minestom.server.command.builder.arguments.ArgumentType.Integer;
import static net.minestom.server.command.builder.arguments.ArgumentType.*;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class GraphConversionTest {
@Test
public void empty() {
final Command foo = new Command("foo");
var graph = Graph.builder(Literal("foo")).build();
assertEqualsGraph(graph, foo);
}
@Test
public void singleLiteral() {
final Command foo = new Command("foo");
var first = Literal("first");
foo.addSyntax(GraphConversionTest::dummyExecutor, first);
var graph = Graph.builder(Literal("foo"))
.append(first).build();
assertEqualsGraph(graph, foo);
}
@Test
public void literalsPath() {
final Command foo = new Command("foo");
var first = Literal("first");
var second = Literal("second");
foo.addSyntax(GraphConversionTest::dummyExecutor, first);
foo.addSyntax(GraphConversionTest::dummyExecutor, second);
var graph = Graph.builder(Literal("foo"))
.append(first).append(second)
.build();
assertEqualsGraph(graph, foo);
}
@Test
public void doubleSyntax() {
enum A {A, B, C, D, E}
final Command foo = new Command("foo");
var bar = Literal("bar");
var baz = Literal("baz");
var a = Enum("a", A.class);
foo.addSyntax(GraphConversionTest::dummyExecutor, bar);
foo.addSyntax(GraphConversionTest::dummyExecutor, baz, a);
var graph = Graph.builder(Literal("foo"))
.append(bar)
.append(baz, builder ->
builder.append(a))
.build();
assertEqualsGraph(graph, foo);
}
@Test
public void doubleSyntaxMerge() {
final Command foo = new Command("foo");
var bar = Literal("bar");
var number = Integer("number");
foo.addSyntax(GraphConversionTest::dummyExecutor, bar);
foo.addSyntax(GraphConversionTest::dummyExecutor, bar, number);
// The two syntax shall start from the same node
var graph = Graph.builder(Literal("foo"))
.append(bar, builder -> builder.append(number))
.build();
assertEqualsGraph(graph, foo);
}
@Test
public void subcommand() {
final Command main = new Command("main");
final Command sub = new Command("sub");
var bar = Literal("bar");
var number = Integer("number");
sub.addSyntax(GraphConversionTest::dummyExecutor, bar);
sub.addSyntax(GraphConversionTest::dummyExecutor, bar, number);
main.addSubcommand(sub);
// The two syntax shall start from the same node
var graph = Graph.builder(Literal("main"))
.append(Literal("sub"), builder ->
builder.append(bar, builder1 -> builder1.append(number)))
.build();
assertEqualsGraph(graph, main);
}
private static void assertEqualsGraph(Graph expected, Command command) {
final Graph actual = Graph.fromCommand(command);
assertTrue(expected.compare(actual, Graph.Comparator.TREE), () -> {
System.out.println("Expected: " + expected);
System.out.println("Actual: " + actual);
return "";
});
}
private static void dummyExecutor(CommandSender sender, CommandContext context) {
}
}

View File

@ -0,0 +1,53 @@
package net.minestom.server.command;
import net.minestom.server.command.builder.Command;
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.assertTrue;
public class GraphMergeTest {
@Test
public void commands() {
var foo = new Command("foo");
var bar = new Command("bar");
var result = Graph.builder(Literal(""))
.append(Literal("foo"))
.append(Literal("bar"))
.build();
assertEqualsGraph(result, Graph.merge(List.of(foo, bar)));
}
@Test
public void empty() {
var graph1 = Graph.builder(Literal("foo")).build();
var graph2 = Graph.builder(Literal("bar")).build();
var result = Graph.builder(Literal(""))
.append(Literal("foo"))
.append(Literal("bar"))
.build();
assertEqualsGraph(result, Graph.merge(graph1, graph2));
}
@Test
public void literals() {
var graph1 = Graph.builder(Literal("foo")).append(Literal("1")).build();
var graph2 = Graph.builder(Literal("bar")).append(Literal("2")).build();
var result = Graph.builder(Literal(""))
.append(Literal("foo"), builder -> builder.append(Literal("1")))
.append(Literal("bar"), builder -> builder.append(Literal("2")))
.build();
assertEqualsGraph(result, Graph.merge(graph1, graph2));
}
private static void assertEqualsGraph(Graph expected, Graph actual) {
assertTrue(expected.compare(actual, Graph.Comparator.TREE), () -> {
System.out.println("Expected: " + expected);
System.out.println("Actual: " + actual);
return "";
});
}
}

View File

@ -0,0 +1,79 @@
package net.minestom.server.command;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
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.*;
public class GraphTest {
@Test
public void empty() {
var result = Graph.builder(Literal(""))
.build();
var node = result.root();
assertEquals(Literal(""), node.argument());
assertTrue(node.next().isEmpty());
}
@Test
public void next() {
var result = Graph.builder(Literal(""))
.append(Literal("foo"))
.build();
var node = result.root();
assertEquals(Literal(""), node.argument());
assertEquals(1, node.next().size());
assertEquals(Literal("foo"), node.next().get(0).argument());
}
@Test
public void immutableNextBuilder() {
var result = Graph.builder(Literal(""))
.append(Literal("foo"))
.append(Literal("bar"))
.build();
var node = result.root();
assertThrows(Exception.class, () -> result.root().next().add(node));
assertThrows(Exception.class, () -> result.root().next().get(0).next().add(node));
}
@Test
public void immutableNextCommand() {
final Command foo = new Command("foo");
var first = Literal("first");
foo.addSyntax(GraphTest::dummyExecutor, first);
var result = Graph.fromCommand(foo);
var node = result.root();
assertThrows(Exception.class, () -> result.root().next().add(node));
assertThrows(Exception.class, () -> result.root().next().get(0).next().add(node));
}
@Test
public void immutableNextCommands() {
final Command foo, bar;
{
var first = Literal("first");
foo = new Command("foo");
foo.addSyntax(GraphTest::dummyExecutor, first);
bar = new Command("foo");
bar.addSyntax(GraphTest::dummyExecutor, first);
}
var result = Graph.merge(List.of(foo, bar));
var node = result.root();
assertThrows(Exception.class, () -> result.root().next().add(node));
assertThrows(Exception.class, () -> result.root().next().get(0).next().add(node));
}
private static void dummyExecutor(CommandSender sender, CommandContext context) {
}
}

View File

@ -1,47 +0,0 @@
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.arguments.ArgumentType;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket.NodeType;
import org.junit.jupiter.api.Test;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class NodeGraphTest {
@Test
public void singleCommandWithOneSyntax() {
final Command foo = new Command("foo");
foo.addSyntax(NodeGraphTest::dummyExecutor, ArgumentType.Integer("bar"));
final DeclareCommandsPacket packet = GraphBuilder.forServer(Set.of(foo)).createPacket();
assertEquals(3, packet.nodes().size());
final DeclareCommandsPacket.Node root = packet.nodes().get(packet.rootIndex());
assertNotNull(root);
assertNodeType(NodeType.ROOT, root.flags);
assertEquals(1, root.children.length);
final DeclareCommandsPacket.Node cmd = packet.nodes().get(root.children[0]);
assertNotNull(cmd);
assertNodeType(NodeType.LITERAL, cmd.flags);
assertEquals(1, cmd.children.length);
assertEquals("foo", cmd.name);
final DeclareCommandsPacket.Node arg = packet.nodes().get(cmd.children[0]);
assertNotNull(arg);
assertNodeType(NodeType.ARGUMENT, arg.flags);
assertEquals(0, arg.children.length);
assertEquals("bar", arg.name);
}
private static void assertNodeType(NodeType expected, byte flags) {
assertEquals(expected, NodeType.values()[flags & 0x03]);
}
private static void dummyExecutor(CommandSender sender, CommandContext context) {
}
}

View File

@ -1,14 +1,12 @@
package net.minestom.server.command;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.exception.IllegalCommandStructureException;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class SubcommandTest {
@ -67,17 +65,4 @@ public class SubcommandTest {
assertFalse(parentExecuted.get());
assertFalse(childExecuted.get());
}
@Test
public void testRecursionDetection() {
final Command foo = new Command("foo");
final Command bar = new Command("bar");
bar.addSubcommand(foo);
assertDoesNotThrow(() -> GraphBuilder.forServer(Set.of(foo, bar)));
foo.addSubcommand(bar);
assertTimeout(Duration.ofSeconds(5), () -> assertThrows(IllegalCommandStructureException.class,
() -> GraphBuilder.forServer(Set.of(foo, bar)), "Builder didn't detect infinite recursion."),
"Is your stack fine?!");
}
}