Skip to content

Commands

Griffin Kubesa edited this page Jun 17, 2020 · 21 revisions

BNCore has a custom commands framework built with Bear Nation in mind.

Instead of having one giant method with a ton of if statements and switch cases, each function of the command has it's own method in the class.

Before vs After

Please note that it is still under development and may change.

Features

  • Neat, concise, easy to read
  • No editing the plugin.yml
  • Automatic command registration
  • Permission and executor checks (e.g. console only)
  • Error handling
  • Event based (cancellable by other processes)
  • Built in and extendable tab completion

Technology behind it

The main resource that the commands framework utilizes is Annotations. Annotations are descriptors or metadata on classes, methods, variables, or parameters. By themselves they do nothing, but using reflection (reading that metadata in runtime), you can utilize those annotations to make real decisions. Some examples of how the framework utilizes annotations are:

  • Command aliases (@Aliases)
  • Command permissions (@Permission)
  • Command argument paths (@Path)
  • Command arguments and default argument values (@Arg)

The framework also utilizes an abstract class that each command must extend which provides all of the magic you can use when implementing your command. For example, to throw an error, simply write: error("Your message here"); and the framework handles the rest.

Example command

Below is an example command that covers a wide range of the framework's features. Refer to the full documentation below for more info on each part of the command.

// Define at least one alias (required)
@Aliases({"examplecommand", "examplecmd"}) // Note that annotations require curly braces when defining a list
@Permission("example.command")
// Extend CustomCommand (required)
public class ExampleCommand extends CustomCommand { 
    // Class variables
    // Load shared variables here (e.g. services)

    // Constructor (required)
    public ExampleCommand(CommandEvent event) {
        super(event);
        // Initialize other things
    }

    @Path
    void run() {
        reply("You didn't pass any arguments that match what we expected, so this is method is executed by default!");
    }

    @Path("hello")
    void helloWorld() {
        reply("Hello, World!");
    }

    // Inject arguments
    @Path("add <int> <int>")
    void add(int num1, int num2) {
        reply("Result: " + (num1 + num2));
    }

    @Path("msg <player> <string...>")
    // Require permission: example.command.message
    @Permission("message")
    void message(Player recipient, String message) {
        recipient.sendMessage("From " + player() + ": " + message);
        reply("To " + recipient.getName() + ": " + message);
    }

    // Default arguments
    @Path("default [string...]")
    // One minute cooldown
    @Cooldown(@Part(Time.MINUTE))
    void defaultArg(@Arg("Hello!") String arg) {
        // arg will have a default value if not passed
        reply("Argument: " + arg);
    }
}

All done! Nothing else is needed.

Annotations

@Aliases

Location: Class
Required: No

Define all aliases of the command. The class name is used as the default command name (e.g. ExampleCommand.class -> /example)

Examples:

@Aliases("examplecommand")
@Aliases({"examplecommand", "examplecmd"})

@Permission

Location: Class and Method
Required: No

If the annotation exists on the class, all methods will require that permission.

If the annotation exists on the method, the permission is only required for that method.

If the annotation exists on both, both the class permission and the method permission are required. By default, the method permission extends the class permission. For example, if the class requires example.command, and the method should require example.command.dosomething, then you would write @Permission("dosomething"). The framework concatenates the two together.

To require a permission not under the class permission's path, write @Permission(value = "other.permission", absolute = true)

Examples:

@Permission("example.command")

If you want to have a method level permission that does not fall under the class's permission namespace, use:

@Permission(value = "some.permission.here", absolute = true)

@Path

Location: Method
Required: On all executable methods

Define the argument path needed to execute the method. When the command is run, the framework checks each available paths and decides which one the arguments best match. If no match is found, it will look for an empty path (nothing in the parenthesis).

There cannot be multiple paths with all variable arguments.

Syntax

  • Literal words are treated as such
  • (one|uno) - Define argument aliases
    • These are treated as literal words. They are not passed into the method. If you want to make a decision based off which one is present, you should either use {string} or create a new path and method.
  • <type> - Define a required variable argument.
  • [type] - Define an optional variable argument.

Supported types

  • String
    • Use <string...> to capture all subsequent arguments
  • Player, OfflinePlayer
    • Default to self to default to executor
  • LocalDate
  • Int(eger), Double, Float, Short, Long, Byte
  • All Enums
  • Your own! See Custom Argument Types

Paths also control tab completion if the type has an available tab completer. See Tab Completion

Examples:

@Path("example")
@Path("example2 (one|uno)")
@Path("<number>")
@Path("dosomething <player> [description]")

@Arg

Location: Parameter
Required: No

Define a method argument to be a command argument. Method arguments must match the path's arguments in order. All variable arguments in the @Path must have a cooresponding variable in the method's signature.

Note that defaults are only respected within the framework's wiring. Calling the method directly in code doesn't respect the defaults, you will have to pass them yourself.

Specify a tab completer with @Arg(tabCompleter = YourClass.class). See See Tab Completion for more info.

If your argument is a List, you will need to specify the class due to java erasure. @Arg(type = YourClass.class)

CustomCommand class

All commands must extend this class, and the command's constructor must call super(event);. The class mostly consists of helper methods.

Helper methods

Chat

  • getPrefix() - Returns the command's prefix, which is the class name without Command in our default format. Override it by setting the PREFIX variable.
  • send(player, message) - Send a colored message to a player
  • send(player, message, delay) - Send a delayed (ms) message to a player
  • reply(message) - Send a message to the executor
  • newline() - Send an empty line to the executor
  • error(message) - Throw an error that is displayed to the player. Stops execution.

Executor

  • sender() - Returns the executor

Calling the following performs an executor check (e.g. to make a command console only, call console())

  • player() - Checks executor is a player and returns the player object
  • console() - Checks executor is console and returns the console object
  • commandBlock() - Checks executor is a command block and returns the command block object

Arguments

Arrays and indexes are hard. There are multiple methods to return the argument at a certain index (starting at 1, not 0). It will return null if the index doesn't exist instead of throwing an exception.

  • arg(index) - Returns a string
  • arg(index, rest) - Returns a string of all the arguments at and after the index supplied.
  • intArg(index) - Returns an integer. Throws an exception if not a number.
  • doubleArg(index) - Returns a double. Throws an exception if not a number.
  • booleanArg(index) - Returns a boolean (accepts true, enable, on, yes, and 1)
  • playerArg(index) - Returns a Player or an OfflinePlayer

Tab Completion

Tab completion is built into each command based off available paths. You can also add tab completers for custom classes by adding a method annotated by @TabCompleterFor inside your command class.

@TabCompleterFor(YourClass.class)
List<String> yourClassTabComplete(String filter) {
    return YourManager.find(filter);
}

If necessary, you can also provide a contextual object to tab complete your arguments.

void addSpawnpoint(Arena arena, @Arg(contextArg = 1) Team team) {
@TabCompleterFor(Team.class)
List<String> tabCompleteTeam(String filter, Arena context) {
    return context.getTeams().stream()
            .map(Team::getName)
            .filter(name -> name.startsWith(filter))
            .collect(Collectors.toList());
}

Custom Argument Types

You can add custom classes to your injectable arguments writing custom converters.

@ConverterFor(LocalDate.class)
LocalDate convertToLocalDate(String value) {
    try { return parseShortDate(value); } catch (Exception ignore) {}
    throw new InvalidInputException("Could not parse date, correct format is MM/DD/YYYY");
}

If necessary, you can also provide a contextual object to convert your arguments.

void addSpawnpoint(Arena arena, @Arg(contextArg = 1) Team team) {
@ConverterFor(Team.class)
Team convertToTeam(String value, Arena context) {
    if ("current".equalsIgnoreCase(value))
        return minigamer.getTeam();

    return context.getTeams().stream()
        .filter(team -> team.getName().startsWith(value))
        .findFirst()
        .orElseThrow(() -> new InvalidInputException("Team not found"));
}

Clone this wiki locally