Skip to content

Commands

Lesson 8.1

Commands like /rubies or /home set give players a direct way to use your mod. In C++, a command is a name and a lambda. The lambda gets everything typed after the name as one string, and you take it apart yourself.

A first command

class HelloMod final : public clight::Mod {
public:
    void on_construct() override {
        clight::on<clight::ev::ServerCommandRegister>([](cl_ev_command_register&) {
            clight::cmd::add("hello", [](clight::CmdCtx ctx) -> int32_t {
                ctx.reply("Hello there!");
                return 1;
            }, 0, "/hello");
        }).detach();
    }
};

Now typing /hello in chat answers “Hello there!”.

  • Commands are added in the ev::ServerCommandRegister event. It runs when a world opens.
  • clight::cmd::add(name, lambda, permission_level, usage) adds one. The last two are optional.
  • The name is the command without the /. Names may use a to z, 0 to 9, _ and -. Adding a name twice fails with CL_E_ALREADY_EXISTS; add returns an Expected<void> that says so.
  • The lambda runs every time somebody types the command. It gets a command context, ctx, and may capture what it likes ([this] is common).
  • The return value is the command’s result: a positive number for success, 0 or less for failure. return 1 is normal. Write -> int32_t after the parameter list so every return agrees on the type.

The command context

Method Gives or does
ctx.player() The player who typed it, or an empty clight::Entity for the server console.
ctx.level() The world it was typed in (the overworld for the console).
ctx.args() Everything typed after the name, as an Expected<std::string>.
ctx.reply("text") Sends a reply.
ctx.send_failure("text") Sends an error reply, in red.
ctx.send_success(text, true) A reply that operators also see in their chat, like vanilla’s /give feedback.

The reply methods take plain strings (std::string, std::format(...), a literal) and return true when the reply went out. They take a clight::Text too, so you can colour and translate replies like chat messages: ctx.send_success(clight::text::translatable("my_mod.done")).

Commands can come from the console, so check for a player before you use one:

int32_t cmd_heal(clight::CmdCtx ctx) {
    clight::Entity p = ctx.player();
    if (!p) {
        ctx.send_failure("Only players can be healed.");
        return 0;
    }
    p.set_health(20.0f);
    ctx.reply("Healed.");
    return 1;
}

// in the ServerCommandRegister handler:
// clight::cmd::add("heal", cmd_heal);

A plain function works as well as a lambda.

Arguments

ctx.args() gives you the rest of the line. For /rubies 5, that is "5"; for /home set base, it is "set base". Read numbers with std::from_chars, which refuses anything that isn’t a clean number (see Errors and Expected):

int32_t cmd_rubies(clight::CmdCtx ctx) {
    clight::Entity p = ctx.player();
    if (!p) return 0;

    std::string args = ctx.args().value_or("");
    int amount = 1;   // "/rubies" alone gives one
    if (!args.empty()) {
        auto [end, err] = std::from_chars(args.data(), args.data() + args.size(), amount);
        if (err != std::errc{} || end != args.data() + args.size() || amount < 1 || amount > 64) {
            ctx.send_failure("Usage: /rubies [1-64]");
            return 0;
        }
    }
    p.give("my_mod:ruby", amount);
    ctx.reply(std::format("Here are {} rubies.", amount));
    return amount;
}

Words are easiest to split with std::string_view’s find and substr:

// "set base" -> {"set", "base"}; "set" -> {"set", ""}
std::pair<std::string_view, std::string_view> first_word(std::string_view s) {
    size_t space = s.find(' ');
    if (space == std::string_view::npos) return {s, {}};
    return {s.substr(0, space), s.substr(space + 1)};
}

// auto [sub, rest] = first_word(args);
// if (sub == "set") { ... }

auto [sub, rest] = ... takes the pair apart into two names. W3Schools’ string reference lists find, substr and friends; they work the same on std::string_view.

Command trees and players as arguments

For commands with several parts, the tree builder reads the arguments for you. literal and argument make nodes, .then(...) hangs one under another, .executes(...) takes a function or lambda, and .register_command() registers the tree:

using clight::cmd::argument;
using clight::cmd::literal;

// /rubygift <player> <amount>
literal("rubygift")
    .then(argument("target", "player").then(argument("amount", "integer").executes([](clight::CmdCtx c) {
        auto targets = c.arg_entities("target");     // one entry for a "player" argument
        int32_t amount = c.arg_i32("amount").value_or(1);
        if (targets && !targets->empty()) (*targets)[0].give("my_mod:ruby", amount);
        return amount;
    })))
    .register_command();
Type Matches Read with
"word" one word c.arg_str(name)
"string" one word or "several in quotes" c.arg_str(name) (without the quotes)
"greedy" the rest of the line c.arg_str(name)
"integer" / "double" / "bool" numbers, true / false c.arg_i32, c.arg_f64, c.arg_bool
"player" / "entity" one: a name, a UUID or a selector like @p c.arg_entities(name) (one entry)
"players" / "entities" several: @a, @e[type=zombie], … c.arg_entities(name)

Player and entity arguments understand every vanilla selector. If nothing matches, the player gets Minecraft’s own error (“No player was found”) and your code doesn’t run.

Who may use it

The third argument of clight::cmd::add makes a command for operators only:

clight::cmd::add("heal", cmd_heal, 2, "/heal");   // like /gamemode
Level Who
0 Everyone (the default).
2 Operators: the level of commands like /gamemode and /tp.
4 Only the highest operator level, like /stop.

Players below the level get “Unknown command”, like for vanilla’s op commands. In singleplayer you are an operator when cheats are on. The console may always run every command.

In a tree, any node can be limited with .requires_level(2) or .requires_permission("my_mod.admin") (a named permission means operators). The node and everything below it doesn’t exist for other players:

literal("ruby")
    .executes([](clight::CmdCtx) { return 1; })                                            // /ruby: everyone
    .then(literal("reset").requires_permission("my_mod.admin").executes([](clight::CmdCtx) { return 1; }))  // operators
    .register_command();

c.permission_level() tells you the level of whoever runs the command (4 for the console).

Running vanilla commands

clight::cmd::run runs any command as the server, without the /, and gives you its result number:

clight::cmd::run("weather clear");
clight::cmd::run("time set day");
if (auto r = clight::cmd::run("effect give @a minecraft:speed 30")) clight::log::info("result {}", *r);

That is a handy way to reach game features the API doesn’t cover yet, like effects or the weather.

Try it

Make a /dice command. /dice rolls a number from 1 to 6. /dice 20 rolls from 1 to 20. Reply with the result, and refuse sides below 2.

Show a solution
#include <clight.hpp>
#include <charconv>
#include <random>

class DiceMod final : public clight::Mod {
public:
    void on_construct() override {
        rng_.seed(static_cast<uint32_t>(clight::core::time_now_ns()));

        clight::on<clight::ev::ServerCommandRegister>([this](cl_ev_command_register&) {
            clight::cmd::add("dice", [this](clight::CmdCtx ctx) -> int32_t {
                std::string args = ctx.args().value_or("");
                int sides = 6;
                if (!args.empty()) std::from_chars(args.data(), args.data() + args.size(), sides);

                if (sides < 2) {
                    ctx.send_failure("A die needs at least 2 sides.");
                    return 0;
                }
                int n = std::uniform_int_distribution<int>(1, sides)(rng_);
                ctx.reply(std::format("You rolled {} (1-{})", n, sides));
                return n;
            }, 0, "/dice [sides]");
        }).detach();
    }

private:
    std::mt19937 rng_;
};

CLIGHT_MOD(DiceMod)

std::mt19937 is a random number generator from <random>, and uniform_int_distribution(1, sides) turns its numbers into a fair roll. It gives the same numbers every start unless you seed it; clight::core::time_now_ns() is the current time in nanoseconds, which is different every start. W3Schools’ Random numbers page shows the older rand(), which works too.

Recap: add commands with clight::cmd::add(name, lambda, permission_level, usage) in the ev::ServerCommandRegister event, read the rest of the line with ctx.args(), reply with ctx.reply("...") or ctx.send_failure("...") (plain strings or a clight::Text), and return a positive number for success.