cloud-core#
cloud-core
contains the main cloud API.
Generally you’ll want to depend on a platform module which implements Cloud for your specific platform, as
cloud-core
does not have any platform-specific code.
Links#
Installation#
Cloud is available through Maven Central.
<dependencies>
<dependency>
<groupId>org.incendo</groupId>
<artifactId>cloud-core</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
implementation("org.incendo:cloud-core:2.0.0")
implementation 'org.incendo:cloud-core:2.0.0'
Command#
A command is a chain of components. Each unique chain makes up a unique command. A command can have some properties associated with it, such as a permission, description, etc.
Example
All of these are examples of unique commands:
/foo bar one
/foo bar two <arg1> -- Command with a required variable
/bar [arg] -- Command with an optional variable
Components#
There are four different types of components:
- Literals: A string literal with optional aliases.
- Required variables: A variable component which gets parsed by a parser, that must be present.
- Optional variables: A variable component which gets parsed by a parser, that must not necessarily be present. An optional component may have a default value.
- Flags: Named components that are always optional. They may have an associated variable component.
Examples:
--flag value
,-abc
.
Cloud requires that the command chains are unambiguous. This means that you may not have a required component following an optional component. You may also not have two conflicting variable components on the same level, as it would not be clear which of them gets to parse the input. You may have one variable component alongside literals, where the literals always get priority.
Not allowed:
/foo <arg1> one
/foo <arg2> two
/foo bar
Allowed:
/foo <arg1> one
/foo bar two
/foo bar baz
Variable Components#
A variable component is associated with an output type. It takes the incoming input and attempts to parse it into the output type using a parser. See the section on parsers for more information.
Suggestions#
Cloud can generate suggestions for values which, depending on the platform, can be displayed to the player to help them complete the command input. Most standard parsers generate suggestions, and by default Cloud will ask the parser for suggestions. You may provide your own suggestions using a suggestion provider.
The standard parsers produce simple string suggestions. Cloud supports custom suggestion types which helps support platform-native suggestions, such rich suggestions in cloud-spring and suggestions with tooltips in cloud-brigadier.
Command Manager#
Execution coordinators#
The execution coordinator is responsible for coordinating command parsing and execution.
You may create a simple execution coordinator by using
ExecutionCoordinator.simpleCoordinator()
which will not
enforce any particular executor and both parsing and suggestion generation will take place on the calling
thread unless the parser or suggestion provider redirects to another executor.
You may also use
ExecutionCoordinator.asyncCoordinator()
to create an execution coordinator that will perform
parsing and suggestion generation asynchronously. You may customize the asynchronous coordinator by using
ExecutionCoordinator.builder()
and supply different executors for different execution steps, or use
ExecutionCoordinator.coordinatorFor(Executor)
to supply an executor which is used at every execution step.
Building a command#
Commands are created using a command builder.
You may either create a new builder by calling
Command#newBuilder
or through the command manager using
CommandManager#commandBuilder
.
It is recommended to use the command manager to create a new command builder, as this ties the command builder
to the parser registry.
The command builders are immutable, and each method returns a new command builder instance. This allows you to store intermediate steps and reuse them to build multiple distinct commands.
You must register your command to the command manager for it to be recognized.
You do this by calling
CommandManager#command(Command)
or
CommandManager#command(Command.Builder)
.
Descriptions#
Both commands (chains of components) and the individual components can have descriptions. These descriptions show up in the help system as well as in the platform-native help systems for platforms that support that.
Certain modules may support more advanced description implementations.
For example, cloud-minecraft-extras contains a RichDescription
implementation
that makes use of adventure components.
Component descriptions#
Component descriptions can be specified both through the component builder, or through the command builder methods.
// through Command.Builder:
builder.required("name", someParser(), Description.of("The description"))
// through CommandComponent.Builder:
CommandComponent.Builder<?, ?> builder = // ...
builder.description(Description.of("The description"))
Command descriptions#
Command descriptions can be added through the command builder by calling
Command.Builder#commandDescription(CommandDescription)
.
The
CommandDescription
instance contains two instances of
Description
,
one short version and an optional verbose version.
// Using the CommandDescription.commandDescription static import with strings:
builder.commandDescription(commandDescription("The description"));
builder.commandDescription(commandDescription("The short description", "The verbose description"));
// Using the CommandDescription.commandDescription static import with description objects:
builder.commandDescription(commandDescription(Description.of("The description")));
Note
It is important to differentiate between the description for the root command literal, and the command descriptions. The root literal may be used by multiple command chains, and is therefore not bound to a specific command. The command description describes a unique command chain.
Permissions#
A command may have a permission attached to it. This determines who is and isn’t allowed to execute the command. Depending on the platform, it might also determine who is allowed to see the command.
The permission is ultimately evaluated by the platform integration. Though, cloud has support for some more complex permission types, such as:
Permission.anyOf(Permission...)
: Takes in multiple permissions and evaluates totrue
if any of the permissions evaluate totrue
.Permission.allOf(Permission...)
: Takes in multiple permissions and evaluates totrue
if all the permissions evaluate totrue
.PredicatePermission.of(Predicate)
: Evaluates totrue
if the predicate evaluates totrue
.
Sender types#
Most classes in Cloud take in a generic parameter type <C>
, which is used for the “command sender type.”
The command sender is the entity executing the command, and this represents different things depending
on the platform.
You may use a sender type that is not native to the platform, and the platform command managers take in a function that maps between your custom type and the native command sender type.
When you create a command you may override the sender type for that specific command, as long as the new
sender type has <C>
as its supertype.
This is done by using the
Command.Builder#senderType(Class)
method.
Cloud will make sure that the sender is of the right type when executing the command, and will fail exceptionally
if it isn’t.
Example sender type usage
Assume that SubSender
extends Sender
.
Command.Builder<Sender> builder = manager.commandBuilder("command");
Command.Builder<SubSender> subBuilder = builder.senderType(SubSender.class);
Command meta#
Command meta-data is used to attach key-value pairs to the commands, which may then be used by different components throughout the command execution chain. Examples of systems that make use of command meta-data are confirmations and the Bukkit help menu.
The meta-data can be configured in the command builder:
final CloudKey<String> metaKey = CloudKey.of("your-key", String.class);
commandBuilder.meta(metaKey, "your value");
or when creating the command builder:
final CloudKey<String> metaKey = CloudKey.of("your-key", String.class);
commandManager.commandBuilder("command", CommandMeta.builder().with(metaKey, "your value").build());
Components#
Literals#
Command literals are fixed strings, and represent what you might think of as a “subcommand.” They may have secondary aliases, depending on the platform you’re targeting. Literals may be placed after required variable components, but never after optional variable components.
The literals are created by using the various different
Command.Builder#literal
methods, for example:
builder
.literal("foo")
.literal(
"bar",
Description.of("A literal with a description and an alias"),
"b"
);
Literals are always required.
Variable#
Variable components are parsed using parsers.
You can create a variable component either by using a
CommandComponent.Builder
that you create using
CommandComponent.builder
,
or by using one of the many different
Command.Builder
overloads.
The component wraps a parser, but in many cases you will want to work with a
ParserDescriptor
instead.
A
ParserDescriptor
is a structure containing an
ArgumentParser
as well as a TypeToken
that describes the object
produced by the parser.
If you do not provide a parser descriptor, then you will have to manually specify the value type.
All variable components have a name.
When you want to extract the parsed values in a command handler you do so using the component name.
You may use a
CloudKey<T>
instead of the name, which then allows you to retrieve the parsed values
in a type-safe manner.
Required#
You can create a required variable component either by using
CommandComponent.Builder#required()
or any
of the many different overloaded required
factory methods in
Command.Builder
.
Optional#
You can create a required variable component either by using
CommandComponent.Builder#optional()
or any
of the many different overloaded optional
factory methods in
Command.Builder
.
When creating an optional variable component you may supply a default value. The default value will be used in the case that the user has not supplied any input for the component. There are three different types of default values:
DefaultValue.constant(Value)
: A constant default value.DefaultValue.dynamic(Function)
: A dynamic value that is evaluated when the command is parsed.DynamicValue.parsed(String)
: A string that is parsed by the component parser when the command is parsed.
Component pre-processing#
You may attach pre-processors to your command components, either when building the component or after it has been built. The pre-processor gets to filter out the input before it reaches the component parser. This allows you to easily add input validation to existing parsers.
Cloud has a built-in processor that validates the input using a regular expression. You can find it here: RegexPreprocessor
Command context#
The CommandContext
is used to store values throughout the parsing process, such as parsed component values,
values from preprocessors, parsed flags, etc.
You can fetch values from the command context using both strings and
CloudKey
. It is recommended to use
keys to access values from the context as they are type-safe.
// Access a parsed value.
final CloudKey<String> nameKey = CloudKey.of("name", String.class);
final String parsedName = commandContext.getOrDefault(nameKey, "Default Name");
// Check for the presence of a flag.
final boolean overrideFlag = commandContext.flags().hasFlag("override");
// Get the sender.
final CommandSender sender = commandContext.sender();
// Inject a value from the injection services.
final List<Cat> cats = commandContext.inject(new TypeToken<List<Cat>>() {});
Handler#
The command handler is an instance of
CommandExecutionHandler
and is invoked when a command has been parsed successfully.
Depending on the command execution coordinator the handler might be invoked asynchronously.
The handler is passed an instance of the command context.
builder.handler(ctx -> {
// your command handling...
});
You may implement
CommandExecutionHandler.FutureCommandExecutionHandler
to have the handler be a future-returning
function. Cloud will wait for the future to complete and will handle any completion exceptions gracefully. You may
use the futureHandler
command builder method to specify a future-returning handler:
builder.futureHandler(ctx -> CompletableFuture.completedFuture(null))
You may delegate to other handlers using
CommandExecutionHandler.delegatingExecutionHandler
.
The command builder also has some utility functions for creating handlers that delegate to the existing handler, like
Command.Builder#prependHandler
and
Command.Builder#appendHandler
.
Registering commands#
The command may be registered to the command manager by using
CommandManager#command(Command)
.
You may also register a command builder using
CommandManager#command(Command.Builder)
in which case the command will be built by the manager.
Commands may also be registered by passing a
CommandFactory
to the command manager.
The command factory is an interface which outputs a list of commands.
Customizing the command manager#
Pre- & Postprocessing#
When a command is entered by a command sender, it goes through the following stages:
- It is converted into a
CommandInput
instance. - A command context is created for the input.
- The context is passed to the preprocessors, which may alter the command input or write to the context.
- If a command processor causes an interrupt using
ConsumerService.interrupt()
the context will be filtered out and the command will not be parsed. - The input is parsed into a command chain, and the parsed values are written to the context.
- If the input cannot be parsed into a command that the sender is allowed to execute, the sender is notified and the parsing is canceled.
- The command postprocessors get to act on the command, and may alter the command context. They may also postpone command execution, which can be used to require confirmations, etc.
- If a postprocessor causes an interrupt using
ConsumerService.interrupt()
the command will not be executed. - The command is executed using the command executor.
The pre- and post-processors can be registered to the command manager using
CommandManager#registerCommandPreProcessor
and
CommandManager#registerCommandPostProcessor
.
Incendo maintains some processors that you may depend on in your projects:
Exception handling#
Cloud v2 introduced a new exception handling system.
You may register exception handlers through the
ExceptionController
,
which can be retrieved using
CommandManager#exceptionController
.
Cloud will attempt to match a thrown exception to any of the registered exception handlers,
giving preference to the most specific exception type and to the last registered handler.
This means that it is possible to register a fallback handler for Throwable
/Exception
and more
precise handlers for specific exception types.
You may register multiple exception handlers for the same exception type.
Cloud will iterate over the exception handlers (giving preference to the last registered handler) until a handler
consumes the exception, which allows for the registration of default handlers.
Some exception types, such as
ArgumentParseException
and
CommandExecutionException
wrap the actual exceptions
thrown by the parser or command handler.
By default, Cloud will forward the wrapper exceptions.
If you instead want to be able to register exception handlers for the causes, then you may use the
ExceptionHandler.unwrappingHandler()
methods to unwrap these exceptions.
You can choose to unwrap all instances of a given exception type, all instances with a given cause type or
all instances that pass a given predicate.
Command exceptions are thrown whenever a command cannot be parsed or executed normally. This can be for several reasons, such as:
- The command sender does not have the required permission (
NoPermissionException
) - The command sender is of the wrong type (
InvalidCommandSenderException
) - The requested command does not exist (
NoSuchCommandException
) - The provided command input is invalid (
InvalidSyntaxException
) - The parser cannot parse the input provided (
ArgumentParseException
)
Localization#
The default exception handlers make use of translatable captions. You may learn more about customizing the messages in the section about Localization.
Parsers#
Parser Registry#
The parser registry stores mappings between types and suppliers of parsers that produce those types. The parser registry is primarily used in two different places:
- When only a value type has been supplied to a command component to look up the relevant parser.
- When using annotated command methods.
If you are creating a library or using cloud-annotations, it is recommended to register your parser
in the parser registry. You can access the parser registry via
CommandManager#parserRegistry
.
The parser suppliers get access to a structure containing parser parameters.
These parameters are most often mapped to annotations, and allow for the customization of the parsers
when using cloud-annotations.
Example parser registration
parserRegistry.registerParserSupplier(TypeToken.get(Integer.class), options ->
new IntegerParser<>(
(int) options.get(StandardParameters.RANGE_MIN, Integer.MIN_VALUE),
(int) options.get(StandardParameters.RANGE_MAX, Integer.MAX_VALUE)
));
Standard Parsers#
Cloud ships with parsers for all Java primitives as well as strings, enums, UUIDs and durations.
String#
Cloud has four different string “modes”:
- Single: A single space-delimited string.
- Quoted: Either a single space-delimited string, or multiple space-delimited strings surrounded by a pair of single or double quotes.
- Greedy: All remaining input.
- Greedy flag yielding: All remaining input until a flag is encountered.
The string parsers do not produce suggestions by default.
The string parsers can be created using the static factory methods found in
StringParser
.
String Array#
Cloud can parse string arrays, in which case it captures all remaining input. A string array parser may also be flag yielding, in which case it will only capture input until it encounters a flag.
The string array parser does not produce suggestions by default.
The string array parser can be created using the static factory methods found in
StringArrayParser
.
Character#
This parses a single space-delimited character. The character parser does not produce suggestions by default.
The character parser can be created using the static factory methods found in
CharacterParser
.
Numbers#
Cloud has parsers for bytes, shorts, integers, longs, doubles and floats. The numerical values may have min- and max-values set.
Cloud will generate suggestions for all numerical types, except for float
and double
.
The numerical parsers can be created using the static factory methods found in:
Boolean#
The boolean parser can either be strict or liberal.
A strict boolean parser only accepts (independent of the case) true
and false
.
A liberal boolean parser also accepts yes
, no
, on
and off
.
The boolean parser can be created using the static factory methods found in
BooleanParser
.
Enum#
The enum parser matches the input (independent of the case) to the names of an enum. The parser will return the enum values as suggestions.
The enum parser can be created using the static factory methods found in
EnumParser
.
UUID#
The UUID parser parses dash-separated UUIDs.
The UUID parser can be created using the static factory methods found in
UUIDParser
.
Flags#
Flags are named optional values that can either have an associated value (value flags), or have their value be determined by whether the flag is present (presence flags).
Flags are always optional. You cannot have required flags. If you want required values, then they should be part of a deterministic command chain. Flags are parsed at the tail of a command chain.
Flags can have aliases alongside their full names.
When referring to the full name of a flag, you use --name
whereas an alias uses the syntax -a
.
You can chain the aliases of multiple presence flags together, such that -a -b -c
is equivalent to -abc
.
The flag values are contained in
FlagContext
which can be retrieved using
CommandContext#flags()
.
Example of a command with a presence flag
manager.commandBuilder("command")
.flag(manager.flagBuilder("flag").withAliases("f"))
.handler(context -> {
boolean present = context.flags().isPresent("flag");
));
Aggregate Parsers#
Aggregate parsers are a new concept as of Cloud v2, and they supersede the old compound argument concept. An aggregate parser is a combination of multiple parsers that maps the intermediate results into an output type using a mapper.
You may either implement the
AggregateParser
interface, or using construct the parser by using a builder
that you create by calling
AggregateParser.builder()
.
final AggregateParser<CommandSender, Location> locationParser = AggregateParser
.<CommandSender>builder()
.withComponent("world", stringParser())
.withComponent("x", integerParser())
.withComponent("y", integerParser())
.withComponent("z", integerParser())
.withMapper(Location.class, (commandContext, aggregateCommandContext) -> {
final String world = aggregateCommandContext.get("world");
final int x = aggregateCommandContext.get("x");
final int y = aggregateCommandContext.get("y");
final int z = aggregateCommandContext.get("z");
return ArgumentParseResult.successFuture(new Location(world, x, y, z));
}).build();
Either#
You may use
ArgumentParser.firstOf(ParserDescriptor, ParserDescriptor)
to create a parser for the type
Either<A, B>
.
The parser will first attempt to parse the primary type A
, and if this fails it will fall back on the
fallback type B
. The suggestions of both the primary and fallback parsers will be joined when using the parser
as the suggestion provider.
commandBuilder.required("either", ArgumentParser.firstOf(integerParser(), booleanParser()))
.handler(context -> {
Either<Integer, Boolean> either = context.get("either");
if (either.primary().isPresent()) {
int integer = either.primary().get();
} else {
boolean bool = either.fallback().get();
}
});
Custom Parsers#
Cloud allows you to create your own parsers. A parser accepts a command context and a command input, and produces a result (or a future that completes with a result).
The context allows the parser to accept parsed results from other command components, which can be useful when the result of the parser depends on other parsed components. The command input is a structure that allows you to consume the input supplied by the command sender by peeking & then reading primitive values and strings.
A parser can fail when the input does not match the expectations. The command manager will turn the failure into a command syntax exception which can then be displayed to the sender, informing them about what went wrong.
Note
Cloud v2 does not require you to peek before consuming from the CommandInput
. The input is defensively copied
before being passed to the parser, unlike in Cloud v1. You may still want to peek in order to determine how
to parse the input, but it is no longer considered incorrect not to do so.
The recommended way of parsing an argument is to:
- Peek the command input.
- Attempt to parse the object.
- If the object cannot be parsed, a failure is returned.
- Pop from the command input.
- Return the parsed value.
Warning
If the read values are not popped from the command input the command engine will assume that the syntax is wrong and an error message is sent to the command sender.
The parser has two different choices when it comes to which method to implement.
If the parser implements
ArgumentParser
then the signature looks like
public ArgumentParseResult<OutputType> parse(
CommandContext<SenderType> context,
CommandInput input) { ... }
where the
ArgumentParseResult
can either be a
ArgumentParseResult.success(OutputType)
or
ArgumentParseResult.failure(Exception)
.
The parser may also implement
ArgumentParser.FutureArgumentParser
in which case the signature looks like
public CompletableFuture<OutputType> parseFuture(
CommandContext<SenderType> context,
CommandInput input) { ... }
in which case, a successful result is returned as a completed future, and a failure is instead returned as an exceptionally completed future. Returning a future is useful when the parsing needs to take place on a specific thread.
public class UUIDParser<C> implements ArgumentParser<C, UUID> {
@Override
public @NonNull ArgumentParseResult<UUID> parse(
@NonNull CommandContext<C> context,
@NonNull CommandInput commandInput
) {
final String input = commandInput.peekString(); // Does not remove the string from the input!
try {
final UUID uuid = UUID.fromString(input);
commandInput.readString(); // Removes the string from the input.
return ArgumentParseResult.success(uuid);
} catch (final IllegalArgumentException e) {
return ArgumentParseResult.failure(new UUIDParseException(input, context));
}
}
}
- The command sender type.
Exceptions#
It is recommended to make use of
ParserException
when returning a failed result. This allows for integration with the caption system,
see exception handling for more information.
Suggestions#
The parser may return suggestions for command input. These suggestions will be used to provide suggestions for the component using the parser, unless the component is created using a custom suggestion provider.
Parsers implement
SuggestionProviderHolder
which means that they can return a suggestion provider by overriding
the
suggestionProvider
method.
However, the recommended way of providing suggestions is by implementing one of the suggestion provider
interfaces (SuggestionProvider
,
BlockingSuggestionProvider
,
or
BlockingSuggestionProvider.Strings
).
If the parser implements a suggestion provider interface it does not need to override the
suggestionProvider
method, as it’ll return this
by default.
Extra#
Help generation#
Cloud has a system that assists in querying for command information.
This is accessible through the
HelpHandler
that can be accessed using
CommandManager#createHelpHandler
.
This invokes a
HelpHandlerFactory
.
You may replace the default
HelpHandlerFactory
using
CommandManager#helpHandlerFactory(HelpHandlerFactory)
to change how the information is generated.
The help handler will try to output as much information as it can, depending on how precise the query is. There are three types of query results:
- Index: Returns a list of commands.
- Multiple: Returns a list of partial results.
- Verbose: Returns verbose information about a specific command.
You may query for results by using
HelpHandler#query(HelpQuery)
.
The help handler does not display any information, this is instead done by a
HelpRenderer
.
cloud-core
does not contain any implementations of the help renderer as this is highly platform-specific,
but cloud-minecraft-extras
contains an opinionated implementation of the help system for Minecraft.
You can find examples on GitHub for either Builders or Annotations.