Registering commands

Declare and handle commands from a C++ plugin, plus the usage-string syntax.

This page covers how to declare and handle commands in C++, from a one-line command up to sub-commands and sender checks, plus the usage-string syntax that drives parameters, types, and tab-completion. The examples alias the namespace with namespace es = endstone; in my_plugin.h to keep type names short.

Declare a command

Inside the ENDSTONE_PLUGIN macro, declare commands with the command(...) builder:

src/my_plugin.cpp
#include "my_plugin.h"

ENDSTONE_PLUGIN("my_plugin", "0.1.0", MyPlugin)
{
    description = "My first C++ plugin for Endstone servers";

    command("hello")
        .description("Greet the command sender.")
        .usages("/hello");
}

Chain .description(...) and .usages(...) to describe the command and the usage strings the client parses for tab-completion.

Control who can run it

By default every command requires the operator permission. Attach one with .permissions(...) and set its default with the permission(...) builder - see Permissions.

Handle the command

Override onCommand in your plugin class. Return true if you handled it, or false to show the usage message:

include/my_plugin.h
#include <endstone/endstone.hpp>

namespace es = endstone;

class MyPlugin : public es::Plugin {
public:
    bool onCommand(es::CommandSender &sender, const es::Command &command,
                   const std::vector<std::string> &args) override
    {
        if (command.getName() == "hello") {
            sender.sendMessage("Hello World!");
        }
        return true;
    }
};

Read parameters

Declaring /hello [msg: message] makes msg optional; args holds the parsed values in order:

include/my_plugin.h
    bool onCommand(es::CommandSender &sender, const es::Command &command,
                   const std::vector<std::string> &args) override
    {
        if (command.getName() == "hello") {
            if (args.empty()) {
                sender.sendMessage("Hello World!");
            } else {
                sender.sendMessage(args[0]);
            }
        }
        return true;
    }

Remember to update the usage string to "/hello [msg: message]" in the command(...) builder.

Branch on a sub-command

A single command can offer a fixed set of actions with an enum parameter. Declare the choices in the usage string (command("warp").usages("/warp <add|list|del>")) and dispatch on the first argument:

include/my_plugin.h
    bool onCommand(es::CommandSender &sender, const es::Command &command,
                   const std::vector<std::string> &args) override
    {
        if (command.getName() == "warp") {
            const std::string &action = args[0];
            if (action == "add") {
                sender.sendMessage("Warp added.");
            } else if (action == "list") {
                sender.sendMessage("Listing warps...");
            } else if (action == "del") {
                sender.sendMessage("Warp deleted.");
            }
        }
        return true;
    }

Because the parameter is mandatory, the client only accepts one of the listed values, so args[0] is always one of them.

Restrict to players

A command can be run from the console as well as in-game. When your logic needs an actual player, cast the sender with as<es::Player>() - it returns nullptr for a non-player - and bail out gracefully otherwise:

include/my_plugin.h
    bool onCommand(es::CommandSender &sender, const es::Command &command,
                   const std::vector<std::string> &args) override
    {
        if (command.getName() == "ping") {
            auto *player = sender.as<es::Player>();
            if (!player) {
                sender.sendMessage("Only players can run this command.");
                return true;
            }
            player->sendPopup("Pong!");
        }
        return true;
    }

Usage strings

Endstone commands are declared as usage strings: you write the command the way a player types it, and Endstone parses that string to build the argument structure and wire up Bedrock's native, client-side tab-completion for you. What you write is what the player sees.

Most server frameworks make you trade one thing for another:

  • Brigadier-style builders (Bukkit/Paper) have you assemble a tree of argument nodes by hand, nesting builder calls inside builder calls. It's powerful, but verbose, and the shape of the command is buried in the code.
  • Nukkit-style registration keeps the code flat, but you lose Bedrock's client-side autocomplete - players get no parameter hints as they type.

Endstone takes neither path. You declare a single line - /give <player: target> <item: str> [amount: int] - and Endstone parses it into typed parameters, registers them so the Bedrock client shows native tab-completion and parameter hints, and hands your handler the parsed arguments.

Parameters

A usage string is the command name followed by zero or more parameters. There are two kinds:

KindDescriptionSyntax
MandatoryMust be provided<name: type>
OptionalCan be omitted[name: type]

So /hello [msg: message] declares one optional msg parameter of type message; switch the brackets to /hello <msg: message> to make it mandatory. Parameters read left to right, exactly as the player types them.

Overloads

A command can declare multiple usage strings, and each one is an overload. The client suggests whichever overloads match what the player has typed so far - the same way vanilla commands like /give offer different forms. Endstone merges them into a single autocomplete tree.

Built-in types

Each parameter has a type that drives both validation and the client's suggestions. Endstone ships these out of the box:

TypeAliasDescriptionExample
intAn integer10
floatA floating-point number3.14
boolA booleantrue
targetactor, entity, playerA target selector@e, @r, PlayerName
strstringA string, terminated by a spaceHello
block_posvec3iA 3D integer position1 2 3
posvec3, vec3fA 3D float position1.0 2.0 3.0
messageEverything until the end of the lineHello World!
jsonA JSON string{"key": "value"}
blockA block typewood
block_statesBlock states["wood_type"="birch","stripped_bit"=true]
entity_typeAn entity typeminecraft:creeper

Because the message type captures everything up to the end of the line, it must be the last parameter in a usage string - Endstone refuses to register a command that places any parameter after a message.

Enum parameters

To accept one of a fixed set of values, use an enum parameter. There are two ways to write one.

Short form

List the values right inside the parameter brackets, separated by |, with no name or type:

  • Mandatory: <value1|value2|value3>
  • Optional: [value1|value2|value3]

For example, /home <add|list|del> lets the player pick add, list, or del, and the client suggests exactly those. Endstone generates the parameter name and the enum type for you, so this is the quickest way to a choice parameter - prefer it for up to about five values.

Full form

The Bedrock client only spells out the inline add|list|del hint while the set is small. Past about five values it stops listing them and shows the parameter as <name: EnumType> instead - and with the short form that name is the one Endstone auto-generated by concatenating the command name and every value (/home <add|list|del> becomes HomeAddListDel), which reads as noise to the player. Past about five values, name the type yourself so the hint stays meaningful: put the values in parentheses, then give the parameter a name and an enum type:

  • Mandatory: (value1|value2|value3)<name: EnumType>
  • Optional: (value1|value2|value3)[name: EnumType]

For example, /warp (home|spawn|shop|mine|nether|end)<destination: WarpTarget> declares a destination parameter of type WarpTarget. A named type stays readable and can be reused across commands.

Enum types behave like string-literal unions in TypeScript or Python: a named set of allowed constants. Because the client autocompletes them, players never have to guess the valid values.

On this page