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_REGISTERevent. It runs when a world opens. nameis the command without the/. Names may useatoz,0to9,_and-. Registering a name twice returnsCL_E_ALREADY_EXISTS.executeis the function that runs. It gets a command context,ctx, and the descriptor’suserpointer.- The return value is the command’s result: a positive number for success,
0or less for failure.return 1is 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.