mirror of
https://github.com/Minestom/Minestom.git
synced 2025-01-08 01:17:47 +01:00
This reverts commit 3999c30961
.
This commit is contained in:
parent
3999c30961
commit
1cdee4a8e1
@ -157,7 +157,6 @@ public final class CommandManager {
|
|||||||
* @return the {@link DeclareCommandsPacket} for {@code player}
|
* @return the {@link DeclareCommandsPacket} for {@code player}
|
||||||
*/
|
*/
|
||||||
public @NotNull DeclareCommandsPacket createDeclareCommandsPacket(@NotNull Player player) {
|
public @NotNull DeclareCommandsPacket createDeclareCommandsPacket(@NotNull Player player) {
|
||||||
final Graph merged = Graph.merge(dispatcher.getCommands());
|
return GraphBuilder.forPlayer(this.dispatcher.getCommands(), player).createPacket();
|
||||||
return GraphConverter.createPacket(merged);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
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 java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
@NotNull List<@NotNull Node> next();
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
204
src/main/java/net/minestom/server/command/GraphBuilder.java
Normal file
204
src/main/java/net/minestom/server/command/GraphBuilder.java
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,158 +0,0 @@
|
|||||||
package net.minestom.server.command;
|
|
||||||
|
|
||||||
import net.minestom.server.command.builder.arguments.*;
|
|
||||||
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) {
|
|
||||||
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)[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) {
|
|
||||||
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);
|
|
||||||
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, List.of()), to, rootRedirect, id, redirect, redirectSetters);
|
|
||||||
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, List.of()), to, rootRedirect, id, null, redirectSetters);
|
|
||||||
last = res;
|
|
||||||
} else {
|
|
||||||
final int[] l = append(new GraphImpl.NodeImpl(entry, List.of()), to, rootRedirect, id, null, redirectSetters);
|
|
||||||
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, List.of()), to, rootRedirect, id, r, setters);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,107 +0,0 @@
|
|||||||
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 java.util.*;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
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(""), children);
|
|
||||||
return new GraphImpl(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean compare(@NotNull Graph graph, @NotNull Comparator comparator) {
|
|
||||||
// We currently do not include execution data in the graph
|
|
||||||
return equals(graph);
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 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, List.of(nodes));
|
|
||||||
}
|
|
||||||
|
|
||||||
static NodeImpl command(Command command) {
|
|
||||||
return ConversionNode.fromCommand(command).toNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
static NodeImpl rootCommands(Collection<Command> commands) {
|
|
||||||
return ConversionNode.rootConv(commands).toNode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private record ConversionNode(Argument<?> argument, Map<Argument<?>, ConversionNode> nextMap) {
|
|
||||||
ConversionNode(Argument<?> argument) {
|
|
||||||
this(argument, 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, List.of(nodes));
|
|
||||||
}
|
|
||||||
|
|
||||||
static ConversionNode fromCommand(Command command) {
|
|
||||||
ConversionNode root = new ConversionNode(Literal(command.getName()));
|
|
||||||
for (var syntax : command.getSyntaxes()) {
|
|
||||||
ConversionNode syntaxNode = root;
|
|
||||||
for (Argument<?> arg : syntax.getArguments()) {
|
|
||||||
syntaxNode = syntaxNode.nextMap.computeIfAbsent(arg, ConversionNode::new);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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(""), next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
105
src/main/java/net/minestom/server/command/Node.java
Normal file
105
src/main/java/net/minestom/server/command/Node.java
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
41
src/main/java/net/minestom/server/command/NodeGraph.java
Normal file
41
src/main/java/net/minestom/server/command/NodeGraph.java
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,437 +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 org.jetbrains.annotations.NotNull;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
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 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)));
|
|
||||||
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));
|
|
||||||
final List<TestNode> expectedList = fromString("0\n0=$root$\n" + expected);
|
|
||||||
final List<TestNode> actualList = fromString(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: " + exportGarphvizDot(packet, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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 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();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
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 GraphCommandConversionTest {
|
|
||||||
@Test
|
|
||||||
public void empty() {
|
|
||||||
final Command foo = new Command("foo");
|
|
||||||
var graph = Graph.builder(Literal("foo")).build();
|
|
||||||
assertEqualsGraph(graph, Graph.fromCommand(foo));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void singleLiteral() {
|
|
||||||
final Command foo = new Command("foo");
|
|
||||||
var first = Literal("first");
|
|
||||||
foo.addSyntax(GraphCommandConversionTest::dummyExecutor, first);
|
|
||||||
var graph = Graph.builder(Literal("foo"))
|
|
||||||
.append(first).build();
|
|
||||||
assertEqualsGraph(graph, Graph.fromCommand(foo));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void literalsPath() {
|
|
||||||
final Command foo = new Command("foo");
|
|
||||||
var first = Literal("first");
|
|
||||||
var second = Literal("second");
|
|
||||||
|
|
||||||
foo.addSyntax(GraphCommandConversionTest::dummyExecutor, first);
|
|
||||||
foo.addSyntax(GraphCommandConversionTest::dummyExecutor, second);
|
|
||||||
|
|
||||||
var graph = Graph.builder(Literal("foo"))
|
|
||||||
.append(first).append(second)
|
|
||||||
.build();
|
|
||||||
assertEqualsGraph(graph, Graph.fromCommand(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(GraphCommandConversionTest::dummyExecutor, bar);
|
|
||||||
foo.addSyntax(GraphCommandConversionTest::dummyExecutor, baz, a);
|
|
||||||
|
|
||||||
var graph = Graph.builder(Literal("foo"))
|
|
||||||
.append(bar)
|
|
||||||
.append(baz, builder ->
|
|
||||||
builder.append(a))
|
|
||||||
.build();
|
|
||||||
assertEqualsGraph(graph, Graph.fromCommand(foo));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void doubleSyntaxMerge() {
|
|
||||||
final Command foo = new Command("foo");
|
|
||||||
|
|
||||||
var bar = Literal("bar");
|
|
||||||
var number = Integer("number");
|
|
||||||
|
|
||||||
foo.addSyntax(GraphCommandConversionTest::dummyExecutor, bar);
|
|
||||||
foo.addSyntax(GraphCommandConversionTest::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, Graph.fromCommand(foo));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void dummyExecutor(CommandSender sender, CommandContext context) {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
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 "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
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) {
|
|
||||||
}
|
|
||||||
}
|
|
47
src/test/java/net/minestom/server/command/NodeGraphTest.java
Normal file
47
src/test/java/net/minestom/server/command/NodeGraphTest.java
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,14 @@
|
|||||||
package net.minestom.server.command;
|
package net.minestom.server.command;
|
||||||
|
|
||||||
import net.minestom.server.command.builder.Command;
|
import net.minestom.server.command.builder.Command;
|
||||||
|
import net.minestom.server.command.builder.exception.IllegalCommandStructureException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
public class SubcommandTest {
|
public class SubcommandTest {
|
||||||
|
|
||||||
@ -65,4 +67,17 @@ public class SubcommandTest {
|
|||||||
assertFalse(parentExecuted.get());
|
assertFalse(parentExecuted.get());
|
||||||
assertFalse(childExecuted.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?!");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user