Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
dev.jorel.commandapi.CommandAPIHandler Maven / Gradle / Ivy
/*******************************************************************************
* 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 extends CommandSender> 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 extends CommandSender> 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 extends CommandSender> 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 extends CommandSender> 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 extends Argument> superArg = (MultiLiteral extends Argument>) 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 extends Argument> literalArgument = (Literal extends Argument>) 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 extends Argument> literalArgument = (Literal extends Argument>) 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);
}
}
}
}