All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.jorel.commandapi.CommandAPIHandler Maven / Gradle / Ivy

There is a newer version: 9.5.3
Show newest version
/*******************************************************************************
 * Copyright 2018, 2021 Jorel Ali (Skepter) - MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *******************************************************************************/
package dev.jorel.commandapi;

import java.awt.Component;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.regex.Pattern;

import com.mojang.brigadier.Command;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.ParsedArgument;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;

import dev.jorel.commandapi.arguments.AbstractArgument;
import dev.jorel.commandapi.arguments.ArgumentSuggestions;
import dev.jorel.commandapi.arguments.CustomProvidedArgument;
import dev.jorel.commandapi.arguments.Literal;
import dev.jorel.commandapi.arguments.MultiLiteral;
import dev.jorel.commandapi.arguments.PreviewInfo;
import dev.jorel.commandapi.arguments.Previewable;
import dev.jorel.commandapi.commandsenders.AbstractCommandSender;
import dev.jorel.commandapi.executors.CommandArguments;
import dev.jorel.commandapi.executors.ExecutionInfo;
import dev.jorel.commandapi.preprocessor.RequireField;
import dev.jorel.commandapi.wrappers.PreviewableFunction;

/**
 * The "brains" behind the CommandAPI.
 * Handles command registration
 *
 * @param  The implementation of AbstractArgument being used
 * @param  The class for running platform commands
 * @param  The class for running Brigadier commands
 */
@RequireField(in = CommandContext.class, name = "arguments", ofType = Map.class)
public class CommandAPIHandler
/// @endcond
, CommandSender, Source> {
	private static final SafeVarHandle, Map>> commandContextArguments;

	// Compute all var handles all in one go so we don't do this during main server
	// runtime
	static {
		commandContextArguments = SafeVarHandle.ofOrNull(CommandContext.class, "arguments", "arguments", Map.class);
	}

	/**
	 * Returns the raw input for an argument for a given command context and its
	 * key. This effectively returns the string value that is currently typed for
	 * this argument
	 * 
	 * @param  the command source type
	 * @param cmdCtx               the command context which is used to run this
	 *                             command
	 * @param key                  the node name for the argument
	 * @return the raw input string for this argument
	 */
	public static  String getRawArgumentInput(CommandContext cmdCtx, String key) {
		final ParsedArgument parsedArgument = commandContextArguments.get(cmdCtx).get(key);
		
		// TODO: Issue #310: Parsing this argument via /execute run  doesn't have the value in
		//  the arguments for this command context (most likely because it's a redirected command).
		//  We need to figure out how to handle this case.
		if (parsedArgument != null) {
			// Sanity check: See https://github.com/JorelAli/CommandAPI/wiki/Implementation-details#chatcomponentargument-raw-arguments
			StringRange range = parsedArgument.getRange();
			if (range.getEnd() > cmdCtx.getInput().length()) {
				range = StringRange.between(range.getStart(), cmdCtx.getInput().length());
			}
			return range.get(cmdCtx.getInput());
		} else {
			return "";
		}
	}

	// TODO: Need to ensure this can be safely "disposed of" when we're done (e.g. on reloads).
	// I hiiiiiiighly doubt we're storing class caches of classes that can be unloaded at runtime,
	// but this IS a generic class caching system and we don't want derpy memory leaks
	private static final Map FIELDS = new HashMap<>();

	final CommandAPIPlatform platform;
	final TreeMap registeredPermissions = new TreeMap<>();
	final List registeredCommands; // Keep track of what has been registered for type checking
	final Map, Previewable> previewableArguments; // Arguments with previewable chat
	static final Pattern NAMESPACE_PATTERN = Pattern.compile("[0-9a-z_.-]+");

	private static CommandAPIHandler instance;

	protected CommandAPIHandler(CommandAPIPlatform platform) {
		this.platform = platform;
		this.registeredCommands = new ArrayList<>();
		this.previewableArguments = new HashMap<>();

		CommandAPIHandler.instance = this;
	}

	public void onLoad(CommandAPIConfig config) {
		checkDependencies();
		platform.onLoad(config);
	}

	private void checkDependencies() {
		// Check for common dependencies
		try {
			Class.forName("com.mojang.brigadier.CommandDispatcher");
		} catch (ClassNotFoundException e) {
			new ClassNotFoundException("Could not hook into Brigadier (Are you running Minecraft 1.13 or above?)")
				.printStackTrace();
		}
	}

	public void onEnable() {
		platform.onEnable();
	}

	public void onDisable() {
		platform.onDisable();
		CommandAPIHandler.resetInstance();
	}
	
	private static void resetInstance() {
		CommandAPIHandler.instance = null;
	}

	public static CommandAPIHandler getInstance() {
		if(CommandAPIHandler.instance != null) {
			return CommandAPIHandler.instance;
		} else {
			throw new IllegalStateException("Tried to access CommandAPIHandler instance, but it was null! Are you using CommandAPI features before calling CommandAPI#onLoad?");
		}
	}

	public CommandAPIPlatform getPlatform() {
		return this.platform;
	}

	/**
	 * Generates a command to be registered by the CommandAPI.
	 * 
	 * @param args       set of ordered argument pairs which contain the prompt text
	 *                   and their argument types
	 * @param executor   code to be ran when the command is executed
	 * @param converted  True if this command is being converted from another plugin, and false otherwise
	 * @return a brigadier command which is registered internally
	 * @throws CommandSyntaxException if an error occurs when the command is ran
	 */
	Command generateCommand(Argument[] args, CommandAPIExecutor> executor, boolean converted) {

		// Generate our command from executor
		return cmdCtx -> {
			AbstractCommandSender sender = platform.getSenderForCommand(cmdCtx, executor.isForceNative());
			CommandArguments commandArguments = argsToCommandArgs(cmdCtx, args);
			ExecutionInfo> executionInfo = new ExecutionInfo<>() {
				@Override
				public CommandSender sender() {
					return sender.getSource();
				}

				@Override
				public AbstractCommandSender senderWrapper() {
					return sender;
				}

				@Override
				public CommandArguments args() {
					return commandArguments;
				}
			};
			if (converted) {
				int resultValue = 0;

				// Return a String[] of arguments for converted commands
				String[] argsAndCmd = cmdCtx.getRange().get(cmdCtx.getInput()).split(" ");
				String[] result = new String[argsAndCmd.length - 1];
				ExecutionInfo> convertedExecutionInfo = new ExecutionInfo<>() {
					@Override
					public CommandSender sender() {
						return sender.getSource();
					}

					@Override
					public AbstractCommandSender senderWrapper() {
						return sender;
					}

					@Override
					public CommandArguments args() {
						return new CommandArguments(result, new LinkedHashMap<>(), result, new LinkedHashMap<>(), "/" + cmdCtx.getInput());
					}
				};

				System.arraycopy(argsAndCmd, 1, result, 0, argsAndCmd.length - 1);

				// As stupid as it sounds, it's more performant and safer to use
				// a List[] instead of a List>, due to NPEs and AIOOBEs.
				@SuppressWarnings("unchecked")
				List[] entityNamesForArgs = new List[args.length];
				for (int i = 0; i < args.length; i++) {
					entityNamesForArgs[i] = args[i].getEntityNames(commandArguments.get(i));
				}
				List> product = CartesianProduct.getDescartes(Arrays.asList(entityNamesForArgs));

				// These objects in obj are List
				for (List strings : product) {
					// We assume result.length == strings.size
					if (result.length == strings.size()) {
						for (int i = 0; i < result.length; i++) {
							if (strings.get(i) != null) {
								result[i] = strings.get(i);
							}
						}
					}
					resultValue += executor.execute(convertedExecutionInfo);
				}

				return resultValue;
			} else {
				return executor.execute(executionInfo);
			}
		};
	}

	/**
	 * Converts the List<Argument> into a {@link CommandArguments} for command execution
	 * 
	 * @param cmdCtx the command context that will execute this command
	 * @param args   the map of strings to arguments
	 * @return an CommandArguments object which can be used in (sender, args) ->
	 * @throws CommandSyntaxException
	 */
	CommandArguments argsToCommandArgs(CommandContext cmdCtx, Argument[] args)
			throws CommandSyntaxException {
		// Array for arguments for executor
		List argList = new ArrayList<>();

		// LinkedHashMap for arguments for executor
		Map argsMap = new LinkedHashMap<>();

		// List for raw arguments
		List rawArguments = new ArrayList<>();

		// LinkedHashMap for raw arguments
		Map rawArgumentsMap = new LinkedHashMap<>();

		// Populate array
		for (Argument argument : args) {
			if (argument.isListed()) {
				Object parsedArgument = parseArgument(cmdCtx, argument.getNodeName(), argument, new CommandArguments(argList.toArray(), argsMap, rawArguments.toArray(new String[0]), rawArgumentsMap, "/" + cmdCtx.getInput()));

				// Add the parsed argument
				argList.add(parsedArgument);
				argsMap.put(argument.getNodeName(), parsedArgument);

				// Add the raw argument
				String rawArgumentString = getRawArgumentInput(cmdCtx, argument.getNodeName());

				rawArguments.add(rawArgumentString);
				rawArgumentsMap.put(argument.getNodeName(), rawArgumentString);
			}
		}

		return new CommandArguments(argList.toArray(), argsMap, rawArguments.toArray(new String[0]), rawArgumentsMap, "/" + cmdCtx.getInput());
	}

	/**
	 * Parses an argument and converts it into its object
	 *
	 * @param cmdCtx the command context
	 * @param key    the key (declared in arguments)
	 * @param value  the value (the argument declared in arguments)
	 * @return the Argument's corresponding object
	 * @throws CommandSyntaxException when the input for the argument isn't formatted correctly
	 */
	Object parseArgument(CommandContext cmdCtx, String key, Argument value, CommandArguments previousArgs) throws CommandSyntaxException {
		if (value.isListed()) {
			return value.parseArgument(cmdCtx, key, previousArgs);
		} else {
			return null;
		}
	}

	//////////////////////////////////////////////////////////////////////////////////////////////////////
	// SECTION: Permissions //
	//////////////////////////////////////////////////////////////////////////////////////////////////////

	/**
	 * This permission generation setup ONLY works iff:
	 * 
    *
  • You register the parent permission node FIRST.
  • *
  • Example:
    * /mycmd - permission node: my.perm
    * /mycmd <arg> - permission node: my.perm.other
  • *
* * The my.perm.other permission node is revoked for the COMMAND * REGISTRATION, however: *
    *
  • The permission node IS REGISTERED.
  • *
  • The permission node, if used for an argument (as in this case), will be * used for suggestions for said argument
  • *
* * @param requirements An arbitrary additional check to perform on the CommandSender * after the permissions check */ Predicate generatePermissions(String commandName, CommandPermission permission, Predicate requirements, String namespace) { // If namespace:commandName was already registered, always use the first permission used String namespacedCommand = namespace.isEmpty() ? commandName.toLowerCase() : namespace.toLowerCase() + ":" + commandName.toLowerCase(); if (registeredPermissions.containsKey(namespacedCommand)) { permission = registeredPermissions.get(namespacedCommand); } else { registeredPermissions.put(namespacedCommand, permission); // The first command to be registered determines the permission for the `commandName` version of the command registeredPermissions.putIfAbsent(commandName.toLowerCase(), permission); } // Register permission to the platform's registry, if both exist permission.getPermission().ifPresent(platform::registerPermission); // Generate predicate for the permission and requirement check CommandPermission finalPermission = permission; return (Source css) -> permissionCheck(platform.getCommandSenderFromCommandSource(css), finalPermission, requirements); } /** * Checks if a sender has a given permission. * * @param sender the sender to check permissions of * @param permission the CommandAPI CommandPermission permission to check * @return true if the sender satisfies the provided permission */ static boolean permissionCheck(AbstractCommandSender sender, CommandPermission permission, Predicate requirements) { boolean satisfiesPermissions; if (sender == null) { satisfiesPermissions = true; } else { if (permission.equals(CommandPermission.NONE)) { // No permission set satisfiesPermissions = true; } else if (permission.equals(CommandPermission.OP)) { // Op permission set satisfiesPermissions = sender.isOp(); } else { final Optional optionalPerm = permission.getPermission(); if(optionalPerm.isPresent()) { satisfiesPermissions = sender.hasPermission(optionalPerm.get()); } else { satisfiesPermissions = true; } } } if (permission.isNegated()) { satisfiesPermissions = !satisfiesPermissions; } return satisfiesPermissions && requirements.test(sender == null ? null : sender.getSource()); } ////////////////////////////////////////////////////////////////////////////////////////////////////// // SECTION: Registration // ////////////////////////////////////////////////////////////////////////////////////////////////////// /* * Expands multiliteral arguments and registers all expansions of * MultiLiteralArguments throughout the provided command. Returns true if * multiliteral arguments were present (and expanded) and returns false if * multiliteral arguments were not present. */ @SuppressWarnings({ "unchecked", "rawtypes" }) private boolean expandMultiLiterals(CommandMetaData meta, final Argument[] args, CommandAPIExecutor> executor, boolean converted, String namespace) { // "Expands" our MultiLiterals into Literals for (int index = 0; index < args.length; index++) { // Find the first multiLiteral in the for loop if (args[index] instanceof MultiLiteral) { MultiLiteral superArg = (MultiLiteral) args[index]; String nodeName = superArg.instance().getNodeName(); // Add all of its entries for (String literal: superArg.getLiterals()) { // TODO: We only expect nodeName to be null here because the constructor for a MultiLiteralArgument // without a nodeName is currently deprecated but not removed. Once that constructor is removed, // this `nodeName == null` statement can probably be removed as well Argument litArg = platform.newConcreteLiteralArgument(nodeName == null ? literal : nodeName, literal); litArg.setListed(superArg.instance().isListed()) .withPermission(superArg.instance().getArgumentPermission()) .withRequirement((Predicate) superArg.instance().getRequirements()); // Reconstruct the list of arguments and place in the new literals Argument[] newArgs = Arrays.copyOf(args, args.length); newArgs[index] = litArg; register(meta, newArgs, executor, converted, namespace); } return true; } } return false; } // Prevent nodes of the same name but with different types: // allow /race invite player // disallow /race invite player // Return true if conflict was present, otherwise return false private boolean hasCommandConflict(String commandName, Argument[] args, String argumentsAsString) { List regArgs = new ArrayList<>(); for (RegisteredCommand rCommand : registeredCommands) { if (rCommand.commandName().equals(commandName)) { for (String str : rCommand.argsAsStr()) { regArgs.add(str.split(":")); } // We just find the first entry that causes a conflict. If this // were some industry-level code, we would probably generate a // list of all commands first, then check for command conflicts // all in one go so we can display EVERY command conflict for // all commands, but this works perfectly and isn't important. break; } } for (int i = 0; i < args.length; i++) { // Avoid IAOOBEs and ensure all node names are the same if ((regArgs.size() == i && regArgs.size() < args.length) || (!regArgs.get(i)[0].equals(args[i].getNodeName()))) { break; } // This only applies to the last argument if (i == args.length - 1 && !regArgs.get(i)[1].equals(args[i].getClass().getSimpleName())) { // Command it conflicts with StringBuilder builder2 = new StringBuilder(); for (String[] arg : regArgs) { builder2.append(arg[0]).append("<").append(arg[1]).append("> "); } CommandAPI.logError(""" Failed to register command: %s %s Because it conflicts with this previously registered command: %s %s """.formatted(commandName, argumentsAsString, commandName, builder2.toString())); return true; } } return false; } // Links arg -> Executor private ArgumentBuilder generateInnerArgument(Command command, Argument[] args) { Argument innerArg = args[args.length - 1]; // Handle Literal arguments if (innerArg instanceof Literal) { @SuppressWarnings("unchecked") Literal literalArgument = (Literal) innerArg; return getLiteralArgumentBuilderArgument(literalArgument.getLiteral(), innerArg.getArgumentPermission(), innerArg.getRequirements()).executes(command); } // Handle arguments with built-in suggestion providers else if (innerArg instanceof CustomProvidedArgument customProvidedArg && innerArg.getOverriddenSuggestions().isEmpty()) { return getRequiredArgumentBuilderWithProvider(innerArg, args, platform.getSuggestionProvider(customProvidedArg.getSuggestionProvider())).executes(command); } // Handle every other type of argument else { return getRequiredArgumentBuilderDynamic(args, innerArg).executes(command); } } // Links arg1 -> arg2 -> ... argN -> innermostArgument private ArgumentBuilder generateOuterArguments(ArgumentBuilder innermostArgument, Argument[] args) { ArgumentBuilder outer = innermostArgument; for (int i = args.length - 2; i >= 0; i--) { Argument outerArg = args[i]; // Handle Literal arguments if (outerArg instanceof Literal) { @SuppressWarnings("unchecked") Literal literalArgument = (Literal) outerArg; outer = getLiteralArgumentBuilderArgument(literalArgument.getLiteral(), outerArg.getArgumentPermission(), outerArg.getRequirements()).then(outer); } // Handle arguments with built-in suggestion providers else if (outerArg instanceof CustomProvidedArgument customProvidedArg && outerArg.getOverriddenSuggestions().isEmpty()) { outer = getRequiredArgumentBuilderWithProvider(outerArg, args, platform.getSuggestionProvider(customProvidedArg.getSuggestionProvider())).then(outer); } // Handle every other type of argument else { outer = getRequiredArgumentBuilderDynamic(args, outerArg).then(outer); } } return outer; } /** * Handles previewable arguments. This stores the path to previewable arguments * in {@link CommandAPIHandler#previewableArguments} for runtime resolving * * @param commandName the name of the command * @param args the declared arguments * @param aliases the command's aliases */ private void handlePreviewableArguments(String commandName, Argument[] args, String[] aliases) { if (args.length > 0 && args[args.length - 1] instanceof Previewable previewable) { List path = new ArrayList<>(); path.add(commandName); for (Argument arg : args) { path.add(arg.getNodeName()); } previewableArguments.put(List.copyOf(path), previewable); // And aliases for (String alias : aliases) { path.set(0, alias); previewableArguments.put(List.copyOf(path), previewable); } } } // Builds a command then registers it void register(CommandMetaData meta, final Argument[] args, CommandAPIExecutor> executor, boolean converted, String namespace) { // "Expands" our MultiLiterals into Literals if (expandMultiLiterals(meta, args, executor, converted, namespace)) { return; } // Create the human-readable command syntax of arguments final String humanReadableCommandArgSyntax; { StringBuilder builder = new StringBuilder(); for (Argument arg : args) { builder.append(arg.toString()).append(" "); } humanReadableCommandArgSyntax = builder.toString().trim(); } // #312 Safeguard against duplicate node names. This only applies to // required arguments (i.e. not literal arguments) if(!checkForDuplicateArgumentNodeNames(args, humanReadableCommandArgSyntax, meta.commandName)) { return; } // Expand metaData into named variables String commandName = meta.commandName; CommandPermission permission = meta.permission; String[] aliases = meta.aliases; Predicate requirements = meta.requirements; Optional shortDescription = meta.shortDescription; Optional fullDescription = meta.fullDescription; Optional usageDescription = meta.usageDescription; Optional helpTopic = meta.helpTopic; // Handle command conflicts boolean hasRegisteredCommand = false; for (int i = 0, size = registeredCommands.size(); i < size && !hasRegisteredCommand; i++) { hasRegisteredCommand |= registeredCommands.get(i).commandName().equals(commandName); } if (hasRegisteredCommand && hasCommandConflict(commandName, args, humanReadableCommandArgSyntax)) { return; } List argumentsString = new ArrayList<>(); for (Argument arg : args) { argumentsString.add(arg.getNodeName() + ":" + arg.getClass().getSimpleName()); } RegisteredCommand registeredCommandInformation = new RegisteredCommand(commandName, argumentsString, List.of(args), shortDescription, fullDescription, usageDescription, helpTopic, aliases, permission, namespace); registeredCommands.add(registeredCommandInformation); // Handle previewable arguments handlePreviewableArguments(commandName, args, aliases); platform.preCommandRegistration(commandName); String namespacedCommandName = namespace.isEmpty() ? commandName : namespace + ":" + commandName; CommandAPI.logInfo("Registering command /" + namespacedCommandName + " " + humanReadableCommandArgSyntax); // Generate the actual command Command command = generateCommand(args, executor, converted); /* * The innermost argument needs to be connected to the executor. Then that * argument needs to be connected to the previous argument etc. Then the first * argument needs to be connected to the command name, so we get: CommandName -> * Args1 -> Args2 -> ... -> ArgsN -> Executor */ LiteralCommandNode resultantNode; List> aliasNodes = new ArrayList<>(); if (args.length == 0) { // Link command name to the executor resultantNode = platform.registerCommandNode(getLiteralArgumentBuilder(commandName) .requires(generatePermissions(commandName, permission, requirements, namespace)).executes(command), namespace); // Register aliases for (String alias : aliases) { CommandAPI.logInfo("Registering alias /" + alias + " -> " + resultantNode.getName()); aliasNodes.add(platform.registerCommandNode(getLiteralArgumentBuilder(alias) .requires(generatePermissions(alias, permission, requirements, namespace)).executes(command), namespace)); } } else { // Generate all of the arguments, following each other and finally linking to // the executor ArgumentBuilder commandArguments = generateOuterArguments( generateInnerArgument(command, args), args); // Link command name to first argument and register resultantNode = platform.registerCommandNode(getLiteralArgumentBuilder(commandName) .requires(generatePermissions(commandName, permission, requirements, namespace)).then(commandArguments), namespace); // Register aliases for (String alias : aliases) { if (CommandAPI.getConfiguration().hasVerboseOutput()) { CommandAPI.logInfo("Registering alias /" + alias + " -> " + resultantNode.getName()); } aliasNodes.add(platform.registerCommandNode(getLiteralArgumentBuilder(alias) .requires(generatePermissions(alias, permission, requirements, namespace)).then(commandArguments), namespace)); } } // TODO: Do something when ambiguities are found // platform.getBrigadierDispatcher().findAmbiguities( // (CommandNode parent, // CommandNode child, // CommandNode sibling, // Collection inputs) -> { // if(resultantNode.equals(parent)) { // // Byeeeeeeeeeeeeeeeeeeeee~ // } // }); // We never know if this is "the last command" and we want dynamic (even if // partial) command registration. Generate the dispatcher file! writeDispatcherToFile(); platform.postCommandRegistration(registeredCommandInformation, resultantNode, aliasNodes); } /** * Checks for duplicate argument node names and logs them as errors in the * console * * @param args the list of arguments * @param humanReadableCommandArgSyntax the human readable command argument * syntax * @param commandName the name of the command * @return true if there were no duplicate argument node names, false otherwise */ private boolean checkForDuplicateArgumentNodeNames(Argument[] args, String humanReadableCommandArgSyntax, String commandName) { Set argumentNames = new HashSet<>(); for (Argument arg : args) { // We shouldn't find MultiLiteralArguments at this point, only LiteralArguments if (!(arg instanceof Literal)) { if (argumentNames.contains(arg.getNodeName())) { CommandAPI.logError(""" Failed to register command: %s %s Because the following argument shares the same node name as another argument: %s """.formatted(commandName, humanReadableCommandArgSyntax, arg.toString())); return false; } else { argumentNames.add(arg.getNodeName()); } } } return true; } public void writeDispatcherToFile() { File file = CommandAPI.getConfiguration().getDispatcherFile(); if (file != null) { try { file.getParentFile().mkdirs(); if (file.createNewFile()) { // Cool, we've created the file assert true; } } catch (IOException e) { CommandAPI.logError("Failed to create the required directories for " + file.getName() + ": " + e.getMessage()); return; } try { platform.createDispatcherFile(file, platform.getBrigadierDispatcher()); } catch (IOException e) { CommandAPI.logError("Failed to write command registration info to " + file.getName() + ": " + e.getMessage()); } } } LiteralCommandNode namespaceNode(LiteralCommandNode original, String namespace) { // Adapted from a section of `CraftServer#syncCommands` LiteralCommandNode clone = new LiteralCommandNode<>( namespace + ":" + original.getLiteral(), original.getCommand(), original.getRequirement(), original.getRedirect(), original.getRedirectModifier(), original.isFork() ); for (CommandNode child : original.getChildren()) { clone.addChild(child); } return clone; } ////////////////////////////////////////////////////////////////////////////////////////////////////// // SECTION: Argument Builders // ////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Creates a literal for a given name. * * @param commandName the name of the literal to create * @return a brigadier LiteralArgumentBuilder representing a literal */ LiteralArgumentBuilder getLiteralArgumentBuilder(String commandName) { return LiteralArgumentBuilder.literal(commandName); } /** * Creates a literal for a given name that requires a specified permission. * * @param commandName the name fo the literal to create * @param permission the permission required to use this literal * @return a brigadier LiteralArgumentBuilder representing a literal */ LiteralArgumentBuilder getLiteralArgumentBuilderArgument(String commandName, CommandPermission permission, Predicate requirements) { LiteralArgumentBuilder builder = LiteralArgumentBuilder.literal(commandName); return builder.requires((Source css) -> permissionCheck(platform.getCommandSenderFromCommandSource(css), permission, requirements)); } // Gets a RequiredArgumentBuilder for a DynamicSuggestedStringArgument RequiredArgumentBuilder getRequiredArgumentBuilderDynamic(final Argument[] args, Argument argument) { final SuggestionProvider suggestions; if (argument.getOverriddenSuggestions().isPresent()) { suggestions = toSuggestions(argument, args, true); } else if (argument.getIncludedSuggestions().isPresent()) { // TODO(#317): Merge the suggestions included here instead? suggestions = (cmdCtx, builder) -> argument.getRawType().listSuggestions(cmdCtx, builder); } else { suggestions = null; } return getRequiredArgumentBuilderWithProvider(argument, args, suggestions); } // Gets a RequiredArgumentBuilder for an argument, given a SuggestionProvider RequiredArgumentBuilder getRequiredArgumentBuilderWithProvider(Argument argument, Argument[] args, SuggestionProvider provider) { SuggestionProvider newSuggestionsProvider = provider; // If we have suggestions to add, combine provider with the suggestions if (argument.getIncludedSuggestions().isPresent() && argument.getOverriddenSuggestions().isEmpty()) { SuggestionProvider addedSuggestions = toSuggestions(argument, args, false); newSuggestionsProvider = (cmdCtx, builder) -> { // Heavily inspired by CommandDispatcher#listSuggestions, with combining // multiple CompletableFuture into one. CompletableFuture addedSuggestionsFuture = addedSuggestions.getSuggestions(cmdCtx, builder); CompletableFuture providerSuggestionsFuture = provider.getSuggestions(cmdCtx, builder); CompletableFuture result = new CompletableFuture<>(); CompletableFuture.allOf(addedSuggestionsFuture, providerSuggestionsFuture).thenRun(() -> { List suggestions = new ArrayList<>(); suggestions.add(addedSuggestionsFuture.join()); suggestions.add(providerSuggestionsFuture.join()); result.complete(Suggestions.merge(cmdCtx.getInput(), suggestions)); }); return result; }; } RequiredArgumentBuilder requiredArgumentBuilder = RequiredArgumentBuilder .argument(argument.getNodeName(), argument.getRawType()); return requiredArgumentBuilder.requires(css -> permissionCheck(platform.getCommandSenderFromCommandSource(css), argument.getArgumentPermission(), argument.getRequirements())).suggests(newSuggestionsProvider); } CommandArguments generatePreviousArguments(CommandContext context, Argument[] args, String nodeName) throws CommandSyntaxException { // Populate Object[], which is our previously filled arguments List previousArguments = new ArrayList<>(); // LinkedHashMap for arguments Map argsMap = new LinkedHashMap<>(); // List for raw arguments List rawArguments = new ArrayList<>(); // LinkedHashMap for raw arguments Map rawArgumentsMap = new LinkedHashMap<>(); for (Argument arg : args) { if (arg.getNodeName().equals(nodeName) && !(arg instanceof Literal)) { break; } Object result; try { result = parseArgument(context, arg.getNodeName(), arg, new CommandArguments(previousArguments.toArray(), argsMap, rawArguments.toArray(new String[0]), rawArgumentsMap, "/" + context.getInput())); } catch (IllegalArgumentException e) { /* * Redirected commands don't parse previous arguments properly. Simplest way to * determine what we should do is simply set it to null, since there's nothing * else we can do. I thought about letting this simply be an empty array, but * then it's even more annoying to deal with - I wouldn't expect an array of * size n to suddenly, randomly be 0, but I would expect random NPEs because * let's be honest, this is Java we're dealing with. */ result = null; } if (arg.isListed()) { // Add the parsed argument previousArguments.add(result); argsMap.put(arg.getNodeName(), result); // Add the raw argument String rawArgumentString = getRawArgumentInput(context, arg.getNodeName()); rawArguments.add(rawArgumentString); rawArgumentsMap.put(arg.getNodeName(), rawArgumentString); } } return new CommandArguments(previousArguments.toArray(), argsMap, rawArguments.toArray(new String[0]), rawArgumentsMap, "/" + context.getInput()); } SuggestionProvider toSuggestions(Argument theArgument, Argument[] args, boolean overrideSuggestions) { return (CommandContext context, SuggestionsBuilder builder) -> { // Construct the suggestion info SuggestionInfo suggestionInfo = new SuggestionInfo<>(platform.getCommandSenderFromCommandSource(context.getSource()).getSource(), generatePreviousArguments(context, args, theArgument.getNodeName()), builder.getInput(), builder.getRemaining()); // Get the suggestions Optional> suggestionsToAddOrOverride = overrideSuggestions ? theArgument.getOverriddenSuggestions() : theArgument.getIncludedSuggestions(); return suggestionsToAddOrOverride.orElse(ArgumentSuggestions.empty()).suggest(suggestionInfo, builder); }; } /** * Looks up the function to generate a chat preview for a path of nodes in the * command tree. This is a method internal to the CommandAPI and isn't expected * to be used by plugin developers (but you're more than welcome to use it as * you see fit). * * @param path a list of Strings representing the path (names of command nodes) * to (and including) the previewable argument * @return a function that takes in a {@link PreviewInfo} and returns a * {@link Component}. If such a function is not available, this will * return a function that always returns null. */ @SuppressWarnings("unchecked") public Optional> lookupPreviewable(List path) { final Previewable previewable = previewableArguments.get(path); if (previewable != null) { return (Optional>) (Optional) previewable.getPreview(); } else { return Optional.empty(); } } /** * * @param path a list of Strings representing the path (names of command nodes) * to (and including) the previewable argument * @return Whether a previewable is legacy (non-Adventure) or not */ public boolean lookupPreviewableLegacyStatus(List path) { final Previewable previewable = previewableArguments.get(path); if (previewable != null && previewable.getPreview().isPresent()) { return previewable.isLegacy(); } else { return true; } } ///////////////////////// // SECTION: Reflection // ///////////////////////// /** * Caches a field using reflection if it is not already cached, then return the * field of a given class. This will also make the field accessible. * * @param clazz the class where the field is declared * @param name the name of the field * @return a Field reference */ public static Field getField(Class clazz, String name) { return getField(clazz, name, name); } /** * Caches a field using reflection if it is not already cached, then return the * field of a given class. This will also make the field accessible. * * @param clazz the class where the field is declared * @param name the name of the field * @param mojangMappedName the name of a field under Mojang mappings * @return a Field reference */ public static Field getField(Class clazz, String name, String mojangMappedName) { ClassCache key = new ClassCache(clazz, name, mojangMappedName); if (FIELDS.containsKey(key)) { return FIELDS.get(key); } else { Field result; try { result = clazz.getDeclaredField(SafeVarHandle.USING_MOJANG_MAPPINGS ? mojangMappedName : name); } catch (ReflectiveOperationException e) { e.printStackTrace(); return null; } result.setAccessible(true); FIELDS.put(key, result); return result; } } ////////////////////////////// // SECTION: Private classes // ////////////////////////////// /** * Class to store cached methods and fields *

* This is required because each key is made up of a class and a field or method * name */ private record ClassCache(Class clazz, String name, String mojangMappedName) { } /** * A class to compute the Cartesian product of a number of lists. Source: * https://www.programmersought.com/article/86195393650/ */ private static final class CartesianProduct { // Shouldn't be instantiated private CartesianProduct() { } /** * Returns the Cartesian product of a list of lists * * @param the underlying type of the list of lists * @param list the list to calculate the Cartesian product of * @return a List of lists which represents the Cartesian product of all * elements of the input */ public static List> getDescartes(List> list) { List> returnList = new ArrayList<>(); descartesRecursive(list, 0, returnList, new ArrayList()); return returnList; } /** * Recursive implementation Principle: traverse sequentially from 0 of the * original list to the end * * @param the underlying type of the list of lists * @param originalList original list * @param position The position of the current recursion in the original * list * @param returnList return result * @param cacheList temporarily saved list */ private static void descartesRecursive(List> originalList, int position, List> returnList, List cacheList) { List originalItemList = originalList.get(position); for (int i = 0; i < originalItemList.size(); i++) { // The last one reuses cacheList to save memory List childCacheList = (i == originalItemList.size() - 1) ? cacheList : new ArrayList<>(cacheList); childCacheList.add(originalItemList.get(i)); if (position == originalList.size() - 1) {// Exit recursion to the end returnList.add(childCacheList); continue; } descartesRecursive(originalList, position + 1, returnList, childCacheList); } } } }