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 function. The function gets everything typed after the name as one string, and you take it apart yourself.

A first command

static int32_t cmd_hello(cl_cmd_ctx ctx, void* user) {
    (void)user;
    cl_text t = cl_text_literal(CL_STR("Hello there!"));
    cl_cmd_send_success(ctx, t, false);
    cl_text_release(t);
    return 1;
}

static void on_command_register(void* payload, void* user) {
    (void)payload;
    (void)user;
    cl_cmd_desc d = CL_CMD_DESC_INIT;
    d.name = CL_STR("hello");
    d.usage = CL_STR("/hello");
    d.execute = cmd_hello;
    cl_cmd_register(&d);
}

static cl_result mod_main(void) {
    return CL_ON(CL_EV_SERVER_COMMAND_REGISTER, on_command_register);
}

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

  • Commands are registered in the CL_EV_SERVER_COMMAND_REGISTER event. It runs when a world opens.
  • name is the command without the /. Names may use a to z, 0 to 9, _ and -. Registering a name twice returns CL_E_ALREADY_EXISTS.
  • execute is the function that runs. It gets a command context, ctx, and the descriptor’s user pointer.
  • The return value is the command’s result: a positive number for success, 0 or less for failure. return 1 is normal.

The command context

Function Gives or does
cl_cmd_ctx_player(ctx) The player who typed it, or a null handle for the server console.
cl_cmd_ctx_level(ctx) The world it was typed in (the overworld for the console).
cl_cmd_ctx_args(ctx, buf, cap, &len) Everything typed after the name, as one string.
cl_cmd_send_success(ctx, text, false) Sends a reply.
cl_cmd_send_failure(ctx, text) Sends an error reply, in red.

The reply functions take a cl_text, so you can colour and translate replies like chat messages. A tiny helper keeps commands short:

static void reply(cl_cmd_ctx ctx, bool ok, const char* message) {
    cl_text t = cl_text_literal(CL_STR_C(message));
    if (ok) cl_cmd_send_success(ctx, t, false);
    else cl_cmd_send_failure(ctx, t);
    cl_text_release(t);
}

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

static int32_t cmd_heal(cl_cmd_ctx ctx, void* user) {
    (void)user;
    cl_entity p = cl_cmd_ctx_player(ctx);
    if (CL_IS_NULL(p)) {
        reply(ctx, false, "Only players can be healed.");
        return 0;
    }
    cl_living_set_health(p, 20.0f);
    reply(ctx, true, "Healed.");
    return 1;
}

Arguments

cl_cmd_ctx_args gives you the rest of the line. For /rubies 5, that is "5"; for /home set base, it is "set base". Read it into an array and take it apart with the usual C tools:

static int32_t cmd_rubies(cl_cmd_ctx ctx, void* user) {
    (void)user;
    cl_entity p = cl_cmd_ctx_player(ctx);
    if (CL_IS_NULL(p)) return 0;

    char args[64];
    size_t len = 0;
    cl_cmd_ctx_args(ctx, args, sizeof args, &len);

    int amount = 1;   // "/rubies" alone gives one
    if (len > 0) {
        char* end = NULL;
        long n = strtol(args, &end, 10);
        if (end == args || *end != '\0' || n < 1 || n > 64) {
            reply(ctx, false, "Usage: /rubies [1-64]");
            return 0;
        }
        amount = (int)n;
    }
    cl_easy_give(p, "my_mod:ruby", amount);
    char msg[64];
    snprintf(msg, sizeof msg, "Here are %d rubies.", amount);
    reply(ctx, true, msg);
    return amount;
}

strtol (from <stdlib.h>) turns text into a number and tells you where it stopped reading, so "5x" and "five" are refused. Words are easiest to split with strtok or sscanf from <string.h> and <stdio.h>:

char sub[16] = "", name[32] = "";
int count = sscanf(args, "%15s %31s", sub, name);
if (count >= 1 && strcmp(sub, "set") == 0) {
    // "/home set" or "/home set <name>"
}

The %15s limits the word to 15 characters, so it always fits into sub[16]. W3Schools covers these functions in String functions and User input (sscanf works like scanf, only on a string).

Command trees and players as arguments

For commands with several parts, the tree builder reads the arguments for you: cl_cmd_literal makes a fixed word, cl_cmd_argument(name, type) a part the player fills in, cl_cmd_then hangs one under another, and cl_cmd_executes says which function runs. cl_cmd_register_tree registers the whole tree:

// /rubygift <player> <amount>
static int32_t rubygift(cl_cmd_ctx ctx, void* user) {
    (void)user;
    cl_entity target = cl_easy_arg_entity(ctx, "target");
    int32_t amount = cl_easy_arg_int(ctx, "amount", 1);
    cl_easy_give(target, "my_mod:ruby", amount);
    return amount;
}

static void on_commands(void* payload, void* user) {
    (void)payload;
    (void)user;
    cl_cmd_node root = cl_cmd_literal(CL_STR("rubygift"));
    cl_cmd_node target = cl_cmd_argument(CL_STR("target"), CL_STR("player"));
    cl_cmd_node amount = cl_cmd_argument(CL_STR("amount"), CL_STR("integer"));
    cl_cmd_executes(amount, rubygift, NULL);
    cl_cmd_then(target, amount);
    cl_cmd_then(root, target);
    cl_cmd_register_tree(root);
}
Type Matches Read with
"word" one word cl_easy_arg_str / cl_cmd_arg_str
"string" one word or "several in quotes" cl_easy_arg_str (without the quotes)
"greedy" the rest of the line cl_easy_arg_str
"integer" / "double" / "bool" numbers, true / false cl_easy_arg_int, cl_cmd_arg_i32, cl_cmd_arg_f64, cl_cmd_arg_bool
"player" / "entity" one: a name, a UUID or a selector like @p cl_easy_arg_entity
"players" / "entities" several: @a, @e[type=zombie], … cl_cmd_arg_entities(ctx, name, buf, cap, &count)

Player and entity arguments understand every vanilla selector. If nothing matches, the player gets Minecraft’s own error (“No player was found”) and your function doesn’t run. A part that doesn’t match its type gives “Incorrect argument for command”.

Who may use it

permission_level makes a command for operators only:

d.permission_level = 2;   // 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: cl_cmd_requires_level(node, 2) or cl_cmd_requires_permission(node, CL_STR("my_mod.admin")) (a named permission means operators). The node and everything below it doesn’t exist for other players:

cl_cmd_node reset = cl_cmd_literal(CL_STR("reset"));
cl_cmd_requires_permission(reset, CL_STR("my_mod.admin"));   // /ruby reset: operators only, /ruby: everyone

cl_cmd_ctx_permission_level(ctx) tells you the level of whoever runs the command (4 for the console).

Running vanilla commands

cl_cmd_run runs any command as the server, without the /:

int32_t result = 0;
cl_cmd_run(CL_STR("weather clear"), &result);
cl_cmd_run(CL_STR("time set day"), &result);

It gives you the command’s result number. That is a handy way to reach game features the API doesn’t cover yet, for example effect give <player> minecraft:speed 30 or weather thunder.

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_easy.h>
#include <stdlib.h>

static int32_t cmd_dice(cl_cmd_ctx ctx, void* user) {
    (void)user;
    char args[32];
    size_t len = 0;
    cl_cmd_ctx_args(ctx, args, sizeof args, &len);

    long sides = 6;
    if (len > 0) sides = strtol(args, NULL, 10);

    char msg[64];
    cl_text t;
    if (sides < 2) {
        t = cl_text_literal(CL_STR("A die needs at least 2 sides."));
        cl_cmd_send_failure(ctx, t);
        cl_text_release(t);
        return 0;
    }
    int n = 1 + rand() % (int)sides;
    snprintf(msg, sizeof msg, "You rolled %d (1-%ld)", n, sides);
    t = cl_text_literal(CL_STR_C(msg));
    cl_cmd_send_success(ctx, t, false);
    cl_text_release(t);
    return n;
}

static void on_command_register(void* payload, void* user) {
    (void)payload;
    (void)user;
    cl_cmd_desc d = CL_CMD_DESC_INIT;
    d.name = CL_STR("dice");
    d.usage = CL_STR("/dice [sides]");
    d.execute = cmd_dice;
    cl_cmd_register(&d);
}

static cl_result mod_main(void) {
    srand((unsigned)cl_time_now_ns());
    return CL_ON(CL_EV_SERVER_COMMAND_REGISTER, on_command_register);
}

CLIGHT_MOD_ENTRY(mod_main)

rand() gives the same numbers every time unless you give it a starting value with srand. cl_time_now_ns() is the current time in nanoseconds, which is different every start. See Random numbers on W3Schools.

Recap: register a cl_cmd_desc (name, execute, optional permission_level) in CL_EV_SERVER_COMMAND_REGISTER, read the rest of the line with cl_cmd_ctx_args, reply with cl_cmd_send_success or cl_cmd_send_failure, and return a positive number for success.