From 366f08f7f930514aadae0c1a92a78322f7f01530 Mon Sep 17 00:00:00 2001 From: TheMode Date: Wed, 13 Jul 2022 22:45:38 +0200 Subject: [PATCH] Graph api (#1233) --- .../server/command/CommandManager.java | 3 +- .../net/minestom/server/command/Graph.java | 61 ++++ .../minestom/server/command/GraphBuilder.java | 204 ------------- .../server/command/GraphConverter.java | 164 +++++++++++ .../minestom/server/command/GraphImpl.java | 153 ++++++++++ .../net/minestom/server/command/Node.java | 105 ------- .../minestom/server/command/NodeGraph.java | 41 --- .../command/CommandPacketFilteringTest.java | 205 +++++++++++++ .../server/command/CommandPacketTest.java | 175 +++++++++++ .../server/command/CommandTestUtils.java | 277 ++++++++++++++++++ .../command/GraphConversionExecutorTest.java | 77 +++++ .../server/command/GraphConversionTest.java | 115 ++++++++ .../server/command/GraphMergeTest.java | 53 ++++ .../minestom/server/command/GraphTest.java | 79 +++++ .../server/command/NodeGraphTest.java | 47 --- .../server/command/SubcommandTest.java | 19 +- 16 files changed, 1363 insertions(+), 415 deletions(-) create mode 100644 src/main/java/net/minestom/server/command/Graph.java delete mode 100644 src/main/java/net/minestom/server/command/GraphBuilder.java create mode 100644 src/main/java/net/minestom/server/command/GraphConverter.java create mode 100644 src/main/java/net/minestom/server/command/GraphImpl.java delete mode 100644 src/main/java/net/minestom/server/command/Node.java delete mode 100644 src/main/java/net/minestom/server/command/NodeGraph.java create mode 100644 src/test/java/net/minestom/server/command/CommandPacketFilteringTest.java create mode 100644 src/test/java/net/minestom/server/command/CommandPacketTest.java create mode 100644 src/test/java/net/minestom/server/command/CommandTestUtils.java create mode 100644 src/test/java/net/minestom/server/command/GraphConversionExecutorTest.java create mode 100644 src/test/java/net/minestom/server/command/GraphConversionTest.java create mode 100644 src/test/java/net/minestom/server/command/GraphMergeTest.java create mode 100644 src/test/java/net/minestom/server/command/GraphTest.java delete mode 100644 src/test/java/net/minestom/server/command/NodeGraphTest.java diff --git a/src/main/java/net/minestom/server/command/CommandManager.java b/src/main/java/net/minestom/server/command/CommandManager.java index e6bd16e88..fa153c0d5 100644 --- a/src/main/java/net/minestom/server/command/CommandManager.java +++ b/src/main/java/net/minestom/server/command/CommandManager.java @@ -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); } } diff --git a/src/main/java/net/minestom/server/command/Graph.java b/src/main/java/net/minestom/server/command/Graph.java new file mode 100644 index 000000000..1e0a3a11a --- /dev/null +++ b/src/main/java/net/minestom/server/command/Graph.java @@ -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 permits GraphImpl.ExecutorImpl { + // TODO execute the node + } + + sealed interface Builder permits GraphImpl.BuilderImpl { + @NotNull Builder append(@NotNull Argument argument, @NotNull Consumer consumer); + + @NotNull Builder append(@NotNull Argument argument); + + @NotNull Graph build(); + } + + enum Comparator { + TREE + } +} diff --git a/src/main/java/net/minestom/server/command/GraphBuilder.java b/src/main/java/net/minestom/server/command/GraphBuilder.java deleted file mode 100644 index 2a2f6bbc9..000000000 --- a/src/main/java/net/minestom/server/command/GraphBuilder.java +++ /dev/null @@ -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 nodes = new ObjectOpenHashSet<>(); - private final ObjectSet> 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 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 commands, Player player) { - final GraphBuilder builder = new GraphBuilder(); - - if (GraphBuilder.class.desiredAssertionStatus()) { - // Detect infinite recursion - for (Command command : commands) { - final HashSet processed = new HashSet<>(); - final Stack 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 commands) { - return forPlayer(commands, null); - } -} diff --git a/src/main/java/net/minestom/server/command/GraphConverter.java b/src/main/java/net/minestom/server/command/GraphConverter.java new file mode 100644 index 000000000..f01dcee34 --- /dev/null +++ b/src/main/java/net/minestom/server/command/GraphConverter.java @@ -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 nodes = new ArrayList<>(); + List> 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 to, + List> rootRedirect, AtomicInteger id, @Nullable AtomicInteger redirect, + List 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 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 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> 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 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); + } +} diff --git a/src/main/java/net/minestom/server/command/GraphImpl.java b/src/main/java/net/minestom/server/command/GraphImpl.java new file mode 100644 index 000000000..b22692ee0 --- /dev/null +++ b/src/main/java/net/minestom/server/command/GraphImpl.java @@ -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 commands) { + return new GraphImpl(NodeImpl.rootCommands(commands)); + } + + static GraphImpl merge(List graphs) { + final List 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 children) implements Graph.Builder { + public BuilderImpl(Argument argument) { + this(argument, new ArrayList<>()); + } + + @Override + public Graph.@NotNull Builder append(@NotNull Argument argument, @NotNull Consumer 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 next) implements Graph.Node { + static NodeImpl fromBuilder(BuilderImpl builder) { + final List 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 commands) { + return ConversionNode.rootConv(commands).toNode(); + } + } + + record ExecutorImpl(Predicate 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, 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 commands) { + Map, 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; + } + }; + } +} diff --git a/src/main/java/net/minestom/server/command/Node.java b/src/main/java/net/minestom/server/command/Node.java deleted file mode 100644 index be4db7bc0..000000000 --- a/src/main/java/net/minestom/server/command/Node.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/net/minestom/server/command/NodeGraph.java b/src/main/java/net/minestom/server/command/NodeGraph.java deleted file mode 100644 index 548b9403b..000000000 --- a/src/main/java/net/minestom/server/command/NodeGraph.java +++ /dev/null @@ -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 nodes; - private final Node root; - - NodeGraph(ObjectSet 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 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()); - } -} diff --git a/src/test/java/net/minestom/server/command/CommandPacketFilteringTest.java b/src/test/java/net/minestom/server/command/CommandPacketFilteringTest.java new file mode 100644 index 000000000..0ce8a207f --- /dev/null +++ b/src/test/java/net/minestom/server/command/CommandPacketFilteringTest.java @@ -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); + } +} diff --git a/src/test/java/net/minestom/server/command/CommandPacketTest.java b/src/test/java/net/minestom/server/command/CommandPacketTest.java new file mode 100644 index 000000000..f064de7e5 --- /dev/null +++ b/src/test/java/net/minestom/server/command/CommandPacketTest.java @@ -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) { + } +} diff --git a/src/test/java/net/minestom/server/command/CommandTestUtils.java b/src/test/java/net/minestom/server/command/CommandTestUtils.java new file mode 100644 index 000000000..6a46fd8fa --- /dev/null +++ b/src/test/java/net/minestom/server/command/CommandTestUtils.java @@ -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 expectedList = NodeStructure.fromString("0\n0=$root$\n" + expectedStructure); + final List 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>> functions = Map.of( + '!', s -> { + final String[] strings = splitDeclaration(s); + final ArrayList 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 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 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 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 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 preProcessString(String string) { + final List strings = Arrays.stream(string.split("\n")).toList(); + final ArrayList 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 fromString(String input) { + Map references = new HashMap<>(); + Map nodes = new HashMap<>(); + final List 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 result = new ArrayList<>(); + List redirectSetters = new ArrayList<>(); + resolveNode(rootId, references, nodes, result, new HashMap<>(), redirectSetters, ""); + redirectSetters.forEach(Runnable::run); + return result; + } + + private static String resolveNode(String id, Map references, + Map nodes, ArrayList result, + Map nameToMetaPath, + List 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 children, String meta, AtomicReference 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 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(); + } + +} diff --git a/src/test/java/net/minestom/server/command/GraphConversionExecutorTest.java b/src/test/java/net/minestom/server/command/GraphConversionExecutorTest.java new file mode 100644 index 000000000..f1f6f67de --- /dev/null +++ b/src/test/java/net/minestom/server/command/GraphConversionExecutorTest.java @@ -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) { + } +} diff --git a/src/test/java/net/minestom/server/command/GraphConversionTest.java b/src/test/java/net/minestom/server/command/GraphConversionTest.java new file mode 100644 index 000000000..0a8776f60 --- /dev/null +++ b/src/test/java/net/minestom/server/command/GraphConversionTest.java @@ -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) { + } +} diff --git a/src/test/java/net/minestom/server/command/GraphMergeTest.java b/src/test/java/net/minestom/server/command/GraphMergeTest.java new file mode 100644 index 000000000..d257c0bd8 --- /dev/null +++ b/src/test/java/net/minestom/server/command/GraphMergeTest.java @@ -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 ""; + }); + } +} diff --git a/src/test/java/net/minestom/server/command/GraphTest.java b/src/test/java/net/minestom/server/command/GraphTest.java new file mode 100644 index 000000000..388768e3b --- /dev/null +++ b/src/test/java/net/minestom/server/command/GraphTest.java @@ -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) { + } +} diff --git a/src/test/java/net/minestom/server/command/NodeGraphTest.java b/src/test/java/net/minestom/server/command/NodeGraphTest.java deleted file mode 100644 index 4251c90a5..000000000 --- a/src/test/java/net/minestom/server/command/NodeGraphTest.java +++ /dev/null @@ -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) { - - } -} diff --git a/src/test/java/net/minestom/server/command/SubcommandTest.java b/src/test/java/net/minestom/server/command/SubcommandTest.java index 2d6b31a16..6ed762fa2 100644 --- a/src/test/java/net/minestom/server/command/SubcommandTest.java +++ b/src/test/java/net/minestom/server/command/SubcommandTest.java @@ -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?!"); - } - }