Minestom/src/main/java/net/minestom/server/command/CommandManager.java

554 lines
20 KiB
Java
Raw Normal View History

2020-04-24 03:25:58 +02:00
package net.minestom.server.command;
2020-04-05 10:15:21 +02:00
2020-08-03 10:35:46 +02:00
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap;
import net.minestom.server.MinecraftServer;
2021-02-11 02:51:42 +01:00
import net.minestom.server.command.builder.*;
import net.minestom.server.command.builder.arguments.Argument;
2020-07-10 18:12:29 +02:00
import net.minestom.server.command.builder.condition.CommandCondition;
2020-04-24 03:25:58 +02:00
import net.minestom.server.entity.Player;
import net.minestom.server.event.player.PlayerCommandEvent;
2020-04-24 03:25:58 +02:00
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
import net.minestom.server.utils.ArrayUtils;
2020-11-02 04:13:43 +01:00
import net.minestom.server.utils.callback.CommandCallback;
2020-05-23 04:20:01 +02:00
import net.minestom.server.utils.validate.Check;
import org.apache.commons.lang3.StringUtils;
2020-10-24 16:58:27 +02:00
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
2020-04-05 10:15:21 +02:00
2020-11-20 14:07:09 +01:00
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
2020-06-21 14:01:03 +02:00
import java.util.*;
2020-04-05 10:15:21 +02:00
2020-10-11 18:35:32 +02:00
/**
* Manager used to register {@link Command} and {@link CommandProcessor}.
* <p>
* It is also possible to simulate a command using {@link #execute(CommandSender, String)}.
*/
public final class CommandManager {
2020-04-05 10:15:21 +02:00
public static final String COMMAND_PREFIX = "/";
2020-12-29 00:04:15 +01:00
private volatile boolean running = true;
2020-04-05 10:15:21 +02:00
2021-02-08 03:42:35 +01:00
private final ServerSender serverSender = new ServerSender();
2020-10-22 12:55:53 +02:00
private final ConsoleSender consoleSender = new ConsoleSender();
2020-06-21 14:01:03 +02:00
2020-10-22 12:55:53 +02:00
private final CommandDispatcher dispatcher = new CommandDispatcher();
private final Map<String, CommandProcessor> commandProcessorMap = new HashMap<>();
2020-04-05 10:15:21 +02:00
2020-11-02 04:13:43 +01:00
private CommandCallback unknownCommandCallback;
2020-06-21 14:01:03 +02:00
public CommandManager() {
}
2020-07-23 07:36:49 +02:00
/**
2020-10-15 21:16:31 +02:00
* Stops the console responsible for the console commands processing.
2020-07-23 07:36:49 +02:00
* <p>
2020-10-15 21:16:31 +02:00
* WARNING: it cannot be re-run later.
2020-07-23 07:36:49 +02:00
*/
public void stopConsoleThread() {
running = false;
2020-06-21 14:01:03 +02:00
}
2020-07-21 18:04:02 +02:00
/**
2020-10-15 21:16:31 +02:00
* Registers a {@link Command}.
2020-07-21 18:04:02 +02:00
*
* @param command the command to register
2020-11-14 07:06:46 +01:00
* @throws IllegalStateException if a command with the same name already exists
2020-07-21 18:04:02 +02:00
*/
public synchronized void register(@NotNull Command command) {
Check.stateCondition(commandExists(command.getName()),
"A command with the name " + command.getName() + " is already registered!");
2021-02-09 15:26:53 +01:00
if (command.getAliases() != null) {
for (String alias : command.getAliases()) {
Check.stateCondition(commandExists(alias),
"A command with the name " + alias + " is already registered!");
}
}
2020-04-05 10:15:21 +02:00
this.dispatcher.register(command);
}
/**
* Removes a command from the currently registered commands.
* Does nothing if the command was not registered before
*
* @param command the command to remove
*/
public void unregister(@NotNull Command command) {
this.dispatcher.unregister(command);
}
/**
2020-10-15 21:16:31 +02:00
* Gets the {@link Command} registered by {@link #register(Command)}.
*
* @param commandName the command name
* @return the command associated with the name, null if not any
*/
2020-10-24 16:58:27 +02:00
@Nullable
public Command getCommand(@NotNull String commandName) {
return dispatcher.findCommand(commandName);
}
2020-07-21 18:04:02 +02:00
/**
2020-10-15 21:16:31 +02:00
* Registers a {@link CommandProcessor}.
2020-07-21 18:04:02 +02:00
*
* @param commandProcessor the command to register
2020-11-14 07:06:46 +01:00
* @throws IllegalStateException if a command with the same name already exists
2020-07-21 18:04:02 +02:00
*/
public synchronized void register(@NotNull CommandProcessor commandProcessor) {
final String commandName = commandProcessor.getCommandName().toLowerCase();
Check.stateCondition(commandExists(commandName),
"A command with the name " + commandName + " is already registered!");
this.commandProcessorMap.put(commandName, commandProcessor);
// Register aliases
final String[] aliases = commandProcessor.getAliases();
if (aliases != null && aliases.length > 0) {
for (String alias : aliases) {
Check.stateCondition(commandExists(alias),
"A command with the name " + alias + " is already registered!");
this.commandProcessorMap.put(alias.toLowerCase(), commandProcessor);
}
}
}
/**
2020-10-15 21:16:31 +02:00
* Gets the {@link CommandProcessor} registered by {@link #register(CommandProcessor)}.
*
* @param commandName the command name
* @return the command associated with the name, null if not any
*/
2020-10-24 16:58:27 +02:00
@Nullable
public CommandProcessor getCommandProcessor(@NotNull String commandName) {
return commandProcessorMap.get(commandName.toLowerCase());
2020-04-05 10:15:21 +02:00
}
/**
* Gets if a command with the name {@code commandName} already exists or name.
*
* @param commandName the command name to check
* @return true if the command does exist
*/
public boolean commandExists(@NotNull String commandName) {
commandName = commandName.toLowerCase();
return dispatcher.findCommand(commandName) != null ||
commandProcessorMap.get(commandName) != null;
}
2020-07-21 18:04:02 +02:00
/**
2020-10-15 21:16:31 +02:00
* Executes a command for a {@link ConsoleSender}.
2020-07-21 18:04:02 +02:00
*
* @param sender the sender of the command
* @param command the raw command string (without the command prefix)
* @return the execution result
2020-07-21 18:04:02 +02:00
*/
@NotNull
public CommandResult execute(@NotNull CommandSender sender, @NotNull String command) {
2020-04-05 10:15:21 +02:00
2020-10-12 02:56:30 +02:00
// Command event
2020-06-21 14:01:03 +02:00
if (sender instanceof Player) {
Player player = (Player) sender;
2020-06-21 14:01:03 +02:00
PlayerCommandEvent playerCommandEvent = new PlayerCommandEvent(player, command);
player.callEvent(PlayerCommandEvent.class, playerCommandEvent);
2020-06-21 14:01:03 +02:00
if (playerCommandEvent.isCancelled())
return CommandResult.withType(CommandResult.Type.CANCELLED);
2020-06-21 14:01:03 +02:00
command = playerCommandEvent.getCommand();
}
2020-10-12 02:56:30 +02:00
// Process the command
{
2020-07-21 18:04:02 +02:00
// Check for rich-command
final CommandResult result = this.dispatcher.execute(sender, command);
if (result.getType() != CommandResult.Type.UNKNOWN) {
return result;
} else {
// Check for legacy-command
final String[] splitCommand = command.split(StringUtils.SPACE);
final String commandName = splitCommand[0];
final CommandProcessor commandProcessor = commandProcessorMap.get(commandName.toLowerCase());
if (commandProcessor == null) {
if (unknownCommandCallback != null) {
this.unknownCommandCallback.apply(sender, command);
}
return CommandResult.withType(CommandResult.Type.CANCELLED);
2020-11-02 04:13:43 +01:00
}
2020-04-05 10:15:21 +02:00
// Execute the legacy-command
final String[] args = command.substring(command.indexOf(StringUtils.SPACE) + 1).split(StringUtils.SPACE);
2021-02-08 03:42:35 +01:00
commandProcessor.process(sender, commandName, args);
return CommandResult.withType(CommandResult.Type.SUCCESS);
}
2020-04-05 10:15:21 +02:00
}
}
2021-02-08 03:42:35 +01:00
/**
* Executes the command using a {@link ServerSender} to do not
* print the command messages, and rely instead on the command return data.
2021-02-08 04:08:22 +01:00
*
* @see #execute(CommandSender, String)
2021-02-08 03:42:35 +01:00
*/
@Nullable
public CommandResult executeServerCommand(@NotNull String command) {
2021-02-08 03:42:35 +01:00
return execute(serverSender, command);
}
2021-02-11 02:51:42 +01:00
@NotNull
public CommandDispatcher getDispatcher() {
return dispatcher;
}
2020-11-02 04:13:43 +01:00
/**
* Gets the callback executed once an unknown command is run.
*
* @return the unknown command callback, null if not any
*/
@Nullable
public CommandCallback getUnknownCommandCallback() {
return unknownCommandCallback;
}
/**
* Sets the callback executed once an unknown command is run.
*
* @param unknownCommandCallback the new unknown command callback,
* setting it to null mean that nothing will be executed
*/
public void setUnknownCommandCallback(@Nullable CommandCallback unknownCommandCallback) {
this.unknownCommandCallback = unknownCommandCallback;
}
2020-07-21 18:04:02 +02:00
/**
2020-10-15 21:16:31 +02:00
* Gets the {@link ConsoleSender} (which is used as a {@link CommandSender}).
2020-07-21 18:04:02 +02:00
*
2020-10-11 18:35:32 +02:00
* @return the {@link ConsoleSender}
2020-07-21 18:04:02 +02:00
*/
2020-10-24 16:58:27 +02:00
@NotNull
2020-06-21 14:01:03 +02:00
public ConsoleSender getConsoleSender() {
return consoleSender;
}
/**
* Starts the thread responsible for executing commands from the console.
*/
public void startConsoleThread() {
Thread consoleThread = new Thread(() -> {
BufferedReader bi = new BufferedReader(new InputStreamReader(System.in));
while (running) {
try {
if (bi.ready()) {
final String command = bi.readLine();
execute(consoleSender, command);
}
} catch (IOException e) {
MinecraftServer.getExceptionManager().handleException(e);
continue;
}
// Prevent permanent looping
try {
Thread.sleep(200);
} catch (InterruptedException e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}
try {
bi.close();
} catch (IOException e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}, "ConsoleCommand-Thread");
consoleThread.setDaemon(true);
consoleThread.start();
}
2020-07-21 18:04:02 +02:00
/**
2020-10-15 21:16:31 +02:00
* Gets the {@link DeclareCommandsPacket} for a specific player.
2020-07-21 18:04:02 +02:00
* <p>
* Can be used to update a player auto-completion list.
2020-07-21 18:04:02 +02:00
*
* @param player the player to get the commands packet
* @return the {@link DeclareCommandsPacket} for {@code player}
*/
2020-10-24 16:58:27 +02:00
@NotNull
public DeclareCommandsPacket createDeclareCommandsPacket(@NotNull Player player) {
2020-07-10 18:12:29 +02:00
return buildPacket(player);
2020-04-05 10:15:21 +02:00
}
2020-08-03 06:36:42 +02:00
/**
2020-10-15 21:16:31 +02:00
* Builds the {@link DeclareCommandsPacket} for a {@link Player}.
2020-08-03 06:36:42 +02:00
*
* @param player the player to build the packet for
* @return the commands packet for the specific player
*/
2020-10-24 16:58:27 +02:00
@NotNull
private DeclareCommandsPacket buildPacket(@NotNull Player player) {
DeclareCommandsPacket declareCommandsPacket = new DeclareCommandsPacket();
2020-04-05 10:15:21 +02:00
List<DeclareCommandsPacket.Node> nodes = new ArrayList<>();
2020-07-10 18:12:29 +02:00
// Contains the children of the main node (all commands name)
2020-08-03 10:35:46 +02:00
IntList rootChildren = new IntArrayList();
2020-04-05 10:15:21 +02:00
// Root node
DeclareCommandsPacket.Node rootNode = new DeclareCommandsPacket.Node();
rootNode.flags = 0;
nodes.add(rootNode);
// Brigadier-like commands
for (Command command : dispatcher.getCommands()) {
2020-07-10 18:12:29 +02:00
// Check if player should see this command
2020-08-03 06:36:42 +02:00
final CommandCondition commandCondition = command.getCondition();
2020-07-10 18:12:29 +02:00
if (commandCondition != null) {
// Do not show command if return false
if (!commandCondition.canUse(player, null)) {
2020-07-10 18:12:29 +02:00
continue;
}
}
// The main root of this command
2020-08-03 10:35:46 +02:00
IntList cmdChildren = new IntArrayList();
final Collection<CommandSyntax> syntaxes = command.getSyntaxes();
2020-07-10 18:12:29 +02:00
// Create command for main name
int mainNodeIndex = createCommand(player, nodes, cmdChildren, command.getName(), syntaxes, rootChildren);
// Use redirection to hook aliases with the command
2021-02-09 15:26:53 +01:00
if (command.getAliases() == null)
continue;
for (String alias : command.getAliases()) {
DeclareCommandsPacket.Node node = new DeclareCommandsPacket.Node();
node.flags = getFlag(NodeType.LITERAL, false, true, false);
node.name = alias;
node.redirectedNode = mainNodeIndex;
nodes.add(node);
2020-07-10 18:12:29 +02:00
}
2020-04-05 10:15:21 +02:00
2020-07-10 18:12:29 +02:00
}
2020-07-10 22:50:05 +02:00
// Pair<CommandName,EnabledTracking>
final Object2BooleanMap<String> commandsPair = new Object2BooleanOpenHashMap<>();
2020-07-10 18:12:29 +02:00
for (CommandProcessor commandProcessor : commandProcessorMap.values()) {
final boolean enableTracking = commandProcessor.enableWritingTracking();
2020-07-10 18:12:29 +02:00
// Do not show command if return false
if (!commandProcessor.hasAccess(player))
continue;
2020-04-05 10:15:21 +02:00
commandsPair.put(commandProcessor.getCommandName(), enableTracking);
2020-08-03 06:36:42 +02:00
final String[] aliases = commandProcessor.getAliases();
2020-07-10 18:12:29 +02:00
if (aliases == null || aliases.length == 0)
continue;
for (String alias : aliases) {
commandsPair.put(alias, enableTracking);
2020-07-10 18:12:29 +02:00
}
}
for (Object2BooleanMap.Entry<String> entry : commandsPair.object2BooleanEntrySet()) {
final String name = entry.getKey();
final boolean tracking = entry.getBooleanValue();
// Server suggestion (ask_server)
{
DeclareCommandsPacket.Node tabNode = new DeclareCommandsPacket.Node();
tabNode.flags = getFlag(NodeType.ARGUMENT, true, false, tracking);
tabNode.name = tracking ? "tab_completion" : "args";
tabNode.parser = "brigadier:string";
tabNode.properties = packetWriter -> packetWriter.writeVarInt(2); // Greedy phrase
tabNode.children = new int[0];
if (tracking) {
tabNode.suggestionsType = "minecraft:ask_server";
}
nodes.add(tabNode);
}
2020-04-05 10:15:21 +02:00
DeclareCommandsPacket.Node literalNode = new DeclareCommandsPacket.Node();
2020-07-10 18:12:29 +02:00
literalNode.flags = getFlag(NodeType.LITERAL, true, false, false);
literalNode.name = name;
literalNode.children = new int[]{nodes.size() - 1};
2020-04-05 10:15:21 +02:00
rootChildren.add(nodes.size());
nodes.add(literalNode);
}
// Add root node children
2020-04-05 10:15:21 +02:00
rootNode.children = ArrayUtils.toArray(rootChildren);
declareCommandsPacket.nodes = nodes.toArray(new DeclareCommandsPacket.Node[0]);
declareCommandsPacket.rootIndex = 0;
return declareCommandsPacket;
2020-04-05 10:15:21 +02:00
}
2020-08-03 06:36:42 +02:00
/**
* Adds the command's syntaxes to the nodes list.
2020-08-03 06:36:42 +02:00
*
* @param sender the potential sender of the command
2020-08-03 06:36:42 +02:00
* @param nodes the nodes of the packet
* @param cmdChildren the main root of this command
* @param name the name of the command (or the alias)
* @param syntaxes the syntaxes of the command
* @param rootChildren the children of the main node (all commands name)
* @return The index of the main node for alias redirection
2020-08-03 06:36:42 +02:00
*/
private int createCommand(@NotNull CommandSender sender,
@NotNull List<DeclareCommandsPacket.Node> nodes,
@NotNull IntList cmdChildren,
@NotNull String name,
@NotNull Collection<CommandSyntax> syntaxes,
@NotNull IntList rootChildren) {
2020-07-10 18:12:29 +02:00
DeclareCommandsPacket.Node literalNode = createMainNode(name, syntaxes.isEmpty());
rootChildren.add(nodes.size());
nodes.add(literalNode);
// Contains the arguments of the already-parsed syntaxes
2021-01-08 23:47:31 +01:00
List<Argument<?>[]> syntaxesArguments = new ArrayList<>();
// Contains the nodes of an argument
2021-02-09 00:33:01 +01:00
Map<Argument<?>, DeclareCommandsPacket.Node[]> storedArgumentsNodes = new HashMap<>();
2020-07-10 18:12:29 +02:00
for (CommandSyntax syntax : syntaxes) {
final CommandCondition commandCondition = syntax.getCommandCondition();
if (commandCondition != null && !commandCondition.canUse(sender, null)) {
// Sender does not have the right to use this syntax, ignore it
continue;
}
2021-02-11 02:51:42 +01:00
NodeMaker nodeMaker = new NodeMaker();
2020-07-10 18:12:29 +02:00
// Represent the last nodes computed in the last iteration
2021-02-11 02:51:42 +01:00
//DeclareCommandsPacket.Node[] lastNodes = null;
2020-07-10 18:12:29 +02:00
// Represent the children of the last node
2020-08-03 10:35:46 +02:00
IntList argChildren = null;
2020-07-10 18:12:29 +02:00
2021-01-08 23:47:31 +01:00
final Argument<?>[] arguments = syntax.getArguments();
2020-07-10 18:12:29 +02:00
for (int i = 0; i < arguments.length; i++) {
2021-01-08 23:47:31 +01:00
final Argument<?> argument = arguments[i];
2020-07-10 18:12:29 +02:00
final boolean isFirst = i == 0;
final boolean isLast = i == arguments.length - 1;
2021-01-08 23:47:31 +01:00
// Find shared part
boolean foundSharedPart = false;
2021-01-08 23:47:31 +01:00
for (Argument<?>[] parsedArguments : syntaxesArguments) {
if (ArrayUtils.sameStart(arguments, parsedArguments, i + 1)) {
2021-01-08 23:47:31 +01:00
final Argument<?> sharedArgument = parsedArguments[i];
argChildren = new IntArrayList();
2021-02-11 02:51:42 +01:00
nodeMaker.setLastNodes(storedArgumentsNodes.get(sharedArgument));
foundSharedPart = true;
}
}
if (foundSharedPart) {
continue;
}
2020-07-10 18:12:29 +02:00
2021-02-11 02:51:42 +01:00
argument.processNodes(nodeMaker, isLast);
final DeclareCommandsPacket.Node[] argumentNodes = nodeMaker.getCurrentNodes();
2021-02-09 00:33:01 +01:00
storedArgumentsNodes.put(argument, argumentNodes);
2020-07-10 18:12:29 +02:00
for (DeclareCommandsPacket.Node node : argumentNodes) {
final int childId = nodes.size();
if (isFirst) {
// Add to main command child
cmdChildren.add(childId);
} else {
// Add to previous argument children
argChildren.add(childId);
}
2021-02-11 02:51:42 +01:00
final DeclareCommandsPacket.Node[] lastNodes = nodeMaker.getLastNodes();
2020-07-10 18:12:29 +02:00
if (lastNodes != null) {
2020-07-23 07:36:49 +02:00
final int[] children = ArrayUtils.toArray(argChildren);
2021-02-09 00:33:01 +01:00
for (DeclareCommandsPacket.Node lastNode : lastNodes) {
lastNode.children = lastNode.children == null ?
children :
ArrayUtils.concatenateIntArrays(lastNode.children, children);
}
2020-07-10 18:12:29 +02:00
}
nodes.add(node);
}
//System.out.println("debug: " + argument.getId() + " : " + isFirst + " : " + isLast);
//System.out.println("debug2: " + i);
//System.out.println("size: " + (argChildren != null ? argChildren.size() : "NULL"));
if (isLast) {
// Last argument doesn't have children
2020-07-23 07:36:49 +02:00
final int[] children = new int[0];
2021-02-09 00:33:01 +01:00
for (DeclareCommandsPacket.Node node : argumentNodes) {
node.children = children;
}
2020-07-10 18:12:29 +02:00
} else {
// Create children list which will be filled during next iteration
2020-08-03 10:35:46 +02:00
argChildren = new IntArrayList();
2021-02-11 02:51:42 +01:00
nodeMaker.setLastNodes(argumentNodes);
2020-07-10 18:12:29 +02:00
}
}
syntaxesArguments.add(arguments);
2020-07-10 18:12:29 +02:00
}
2020-07-23 07:36:49 +02:00
final int[] children = ArrayUtils.toArray(cmdChildren);
2020-07-10 18:12:29 +02:00
//System.out.println("test " + children.length + " : " + children[0]);
literalNode.children = children;
if (children.length > 0) {
literalNode.redirectedNode = children[0];
}
return nodes.indexOf(literalNode);
2020-07-10 18:12:29 +02:00
}
2020-10-24 16:58:27 +02:00
@NotNull
private DeclareCommandsPacket.Node createMainNode(@NotNull String name, boolean executable) {
2020-07-10 18:12:29 +02:00
DeclareCommandsPacket.Node literalNode = new DeclareCommandsPacket.Node();
literalNode.flags = getFlag(NodeType.LITERAL, executable, false, false);
literalNode.name = name;
return literalNode;
}
public byte getFlag(@NotNull NodeType type, boolean executable, boolean redirect, boolean suggestionType) {
byte result = (byte) type.mask;
if (executable) {
result |= 0x04;
}
if (redirect) {
result |= 0x08;
}
if (suggestionType) {
2020-07-10 18:12:29 +02:00
result |= 0x10;
}
return result;
}
public enum NodeType {
ROOT(0), LITERAL(0b1), ARGUMENT(0b10), NONE(0x11);
2020-09-29 21:45:16 +02:00
private final int mask;
NodeType(int mask) {
this.mask = mask;
}
}
2020-04-05 10:15:21 +02:00
}