mirror of
https://github.com/Minestom/Minestom.git
synced 2025-01-06 16:37:38 +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}
|
||||
*/
|
||||
public @NotNull DeclareCommandsPacket createDeclareCommandsPacket(@NotNull Player player) {
|
||||
final Graph merged = Graph.merge(dispatcher.getCommands());
|
||||
return GraphConverter.createPacket(merged);
|
||||
return GraphBuilder.forPlayer(this.dispatcher.getCommands(), player).createPacket();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
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.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class SubcommandTest {
|
||||
|
||||
@ -65,4 +67,17 @@ 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?!");
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user