package net.minestom.server.command; 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; import net.minestom.server.command.builder.*; import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.condition.CommandCondition; import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerCommandEvent; import net.minestom.server.network.packet.server.play.DeclareCommandsPacket; import net.minestom.server.utils.ArrayUtils; import net.minestom.server.utils.callback.CommandCallback; import net.minestom.server.utils.validate.Check; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.*; /** * Manager used to register {@link Command} and {@link CommandProcessor}. *

* It is also possible to simulate a command using {@link #execute(CommandSender, String)}. */ public final class CommandManager { public static final String COMMAND_PREFIX = "/"; private volatile boolean running = true; private final ServerSender serverSender = new ServerSender(); private final ConsoleSender consoleSender = new ConsoleSender(); private final CommandDispatcher dispatcher = new CommandDispatcher(); private final Map commandProcessorMap = new HashMap<>(); private CommandCallback unknownCommandCallback; public CommandManager() { } /** * Stops the console responsible for the console commands processing. *

* WARNING: it cannot be re-run later. */ public void stopConsoleThread() { running = false; } /** * Registers a {@link Command}. * * @param command the command to register * @throws IllegalStateException if a command with the same name already exists */ public synchronized void register(@NotNull Command command) { Check.stateCondition(commandExists(command.getName()), "A command with the name " + command.getName() + " is already registered!"); if (command.getAliases() != null) { for (String alias : command.getAliases()) { Check.stateCondition(commandExists(alias), "A command with the name " + alias + " is already registered!"); } } 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); } /** * 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 */ @Nullable public Command getCommand(@NotNull String commandName) { return dispatcher.findCommand(commandName); } /** * Registers a {@link CommandProcessor}. * * @param commandProcessor the command to register * @throws IllegalStateException if a command with the same name already exists */ 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); } } } /** * 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 */ @Nullable public CommandProcessor getCommandProcessor(@NotNull String commandName) { return commandProcessorMap.get(commandName.toLowerCase()); } /** * 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; } /** * Executes a command for a {@link ConsoleSender}. * * @param sender the sender of the command * @param command the raw command string (without the command prefix) * @return the execution result */ @NotNull public CommandResult execute(@NotNull CommandSender sender, @NotNull String command) { // Command event if (sender instanceof Player) { Player player = (Player) sender; PlayerCommandEvent playerCommandEvent = new PlayerCommandEvent(player, command); player.callEvent(PlayerCommandEvent.class, playerCommandEvent); if (playerCommandEvent.isCancelled()) return CommandResult.withType(CommandResult.Type.CANCELLED); command = playerCommandEvent.getCommand(); } // Process the command { // 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); } // Execute the legacy-command final String[] args = command.substring(command.indexOf(StringUtils.SPACE) + 1).split(StringUtils.SPACE); commandProcessor.process(sender, commandName, args); return CommandResult.withType(CommandResult.Type.SUCCESS); } } } /** * Executes the command using a {@link ServerSender} to do not * print the command messages, and rely instead on the command return data. * * @see #execute(CommandSender, String) */ @Nullable public CommandResult executeServerCommand(@NotNull String command) { return execute(serverSender, command); } @NotNull public CommandDispatcher getDispatcher() { return dispatcher; } /** * 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; } /** * Gets the {@link ConsoleSender} (which is used as a {@link CommandSender}). * * @return the {@link ConsoleSender} */ @NotNull 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(); } /** * Gets the {@link DeclareCommandsPacket} for a specific player. *

* Can be used to update a player auto-completion list. * * @param player the player to get the commands packet * @return the {@link DeclareCommandsPacket} for {@code player} */ @NotNull public DeclareCommandsPacket createDeclareCommandsPacket(@NotNull Player player) { return buildPacket(player); } /** * Builds the {@link DeclareCommandsPacket} for a {@link Player}. * * @param player the player to build the packet for * @return the commands packet for the specific player */ @NotNull private DeclareCommandsPacket buildPacket(@NotNull Player player) { DeclareCommandsPacket declareCommandsPacket = new DeclareCommandsPacket(); List nodes = new ArrayList<>(); // Contains the children of the main node (all commands name) IntList rootChildren = new IntArrayList(); // Root node DeclareCommandsPacket.Node rootNode = new DeclareCommandsPacket.Node(); rootNode.flags = 0; nodes.add(rootNode); // Brigadier-like commands for (Command command : dispatcher.getCommands()) { // Check if player should see this command final CommandCondition commandCondition = command.getCondition(); if (commandCondition != null) { // Do not show command if return false if (!commandCondition.canUse(player, null)) { continue; } } // The main root of this command IntList cmdChildren = new IntArrayList(); final Collection syntaxes = command.getSyntaxes(); // Create command for main name int mainNodeIndex = createCommand(player, nodes, cmdChildren, command.getName(), syntaxes, rootChildren); // Use redirection to hook aliases with the command 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); } } // Pair final Object2BooleanMap commandsPair = new Object2BooleanOpenHashMap<>(); for (CommandProcessor commandProcessor : commandProcessorMap.values()) { final boolean enableTracking = commandProcessor.enableWritingTracking(); // Do not show command if return false if (!commandProcessor.hasAccess(player)) continue; commandsPair.put(commandProcessor.getCommandName(), enableTracking); final String[] aliases = commandProcessor.getAliases(); if (aliases == null || aliases.length == 0) continue; for (String alias : aliases) { commandsPair.put(alias, enableTracking); } } for (Object2BooleanMap.Entry 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); } DeclareCommandsPacket.Node literalNode = new DeclareCommandsPacket.Node(); literalNode.flags = getFlag(NodeType.LITERAL, true, false, false); literalNode.name = name; literalNode.children = new int[]{nodes.size() - 1}; rootChildren.add(nodes.size()); nodes.add(literalNode); } // Add root node children rootNode.children = ArrayUtils.toArray(rootChildren); declareCommandsPacket.nodes = nodes.toArray(new DeclareCommandsPacket.Node[0]); declareCommandsPacket.rootIndex = 0; return declareCommandsPacket; } /** * Adds the command's syntaxes to the nodes list. * * @param sender the potential sender of the command * @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 */ private int createCommand(@NotNull CommandSender sender, @NotNull List nodes, @NotNull IntList cmdChildren, @NotNull String name, @NotNull Collection syntaxes, @NotNull IntList rootChildren) { DeclareCommandsPacket.Node literalNode = createMainNode(name, syntaxes.isEmpty()); rootChildren.add(nodes.size()); nodes.add(literalNode); // Contains the arguments of the already-parsed syntaxes List[]> syntaxesArguments = new ArrayList<>(); // Contains the nodes of an argument Map, DeclareCommandsPacket.Node[]> storedArgumentsNodes = new HashMap<>(); 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; } NodeMaker nodeMaker = new NodeMaker(); // Represent the last nodes computed in the last iteration //DeclareCommandsPacket.Node[] lastNodes = null; // Represent the children of the last node IntList argChildren = null; final Argument[] arguments = syntax.getArguments(); for (int i = 0; i < arguments.length; i++) { final Argument argument = arguments[i]; final boolean isFirst = i == 0; final boolean isLast = i == arguments.length - 1; // Find shared part boolean foundSharedPart = false; for (Argument[] parsedArguments : syntaxesArguments) { if (ArrayUtils.sameStart(arguments, parsedArguments, i + 1)) { final Argument sharedArgument = parsedArguments[i]; argChildren = new IntArrayList(); nodeMaker.setLastNodes(storedArgumentsNodes.get(sharedArgument)); foundSharedPart = true; } } if (foundSharedPart) { continue; } argument.processNodes(nodeMaker, isLast); final DeclareCommandsPacket.Node[] argumentNodes = nodeMaker.getCurrentNodes(); storedArgumentsNodes.put(argument, argumentNodes); 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); } final DeclareCommandsPacket.Node[] lastNodes = nodeMaker.getLastNodes(); if (lastNodes != null) { final int[] children = ArrayUtils.toArray(argChildren); for (DeclareCommandsPacket.Node lastNode : lastNodes) { lastNode.children = lastNode.children == null ? children : ArrayUtils.concatenateIntArrays(lastNode.children, children); } } 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 final int[] children = new int[0]; for (DeclareCommandsPacket.Node node : argumentNodes) { node.children = children; } } else { // Create children list which will be filled during next iteration argChildren = new IntArrayList(); nodeMaker.setLastNodes(argumentNodes); } } syntaxesArguments.add(arguments); } final int[] children = ArrayUtils.toArray(cmdChildren); //System.out.println("test " + children.length + " : " + children[0]); literalNode.children = children; if (children.length > 0) { literalNode.redirectedNode = children[0]; } return nodes.indexOf(literalNode); } @NotNull private DeclareCommandsPacket.Node createMainNode(@NotNull String name, boolean executable) { 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) { result |= 0x10; } return result; } public enum NodeType { ROOT(0), LITERAL(0b1), ARGUMENT(0b10), NONE(0x11); private final int mask; NodeType(int mask) { this.mask = mask; } } }