Skip to content

Keys and client events

Lesson 9.1

Client mods react to what the player does at the keyboard and what the game window shows. This lesson covers keybinds, the client tick, and the client events for chat and keys. Everything here is client-side: register it in CL_EV_LIFECYCLE_CLIENT_SETUP.

A keybind

static cl_keybind g_say_hi;

static void on_client_tick(void* payload, void* user) {
    (void)payload;
    (void)user;
    while (cl_keybind_consume_click(g_say_hi)) {
        CL_LOG("G was pressed");
    }
}

static void on_client_setup(void* payload, void* user) {
    (void)payload;
    (void)user;
    cl_keybind_desc k = CL_KEYBIND_DESC_INIT;
    k.id = CL_STR("my_mod:say_hi");
    k.default_key = CL_STR("key.keyboard.g");
    k.category = CL_STR("key.categories.my_mod");
    if (cl_keybind_register(&k, &g_say_hi) == CL_OK) {
        CL_ON(CL_EV_TICK_CLIENT_POST, on_client_tick);
    }
}

static cl_result mod_main(void) {
    return CL_ON(CL_EV_LIFECYCLE_CLIENT_SETUP, on_client_setup);
}
  • Register keybinds in CL_EV_LIFECYCLE_CLIENT_SETUP. They are client-only; on a dedicated server, registering returns CL_E_WRONG_SIDE.
  • default_key uses vanilla’s key names: "key.keyboard.g", "key.keyboard.f6", "key.keyboard.left.alt", "key.keyboard.keypad.5", "key.keyboard.space", and for mouse buttons "key.mouse.left", "key.mouse.right", "key.mouse.middle", "key.mouse.4" and "key.mouse.5".
  • cl_keybind_consume_click returns true once for each press since the last check, then false. Check it every client tick; the while handles two quick presses in one tick.
  • cl_keybind_is_down is true while the key is held, for things like “zoom while holding”.

By default a keybind only reacts in the game, not while a menu or the chat is open. Set k.context = CL_KEYCTX_GUI to make it work only in menus, or CL_KEYCTX_ANY for everywhere.

Every mod keybind appears in Options > Controls > Key Binds under its category. Players can bind it to any key or mouse button and reset it, and the choice is saved in options.txt, so always read it through consume_click / is_down. cl_keybind_key(key, buf, cap, &len) gives the key it is bound to now, for hints like “Press G”.

Give the keybind and its category a name in your language file. The keys are key. plus the id with : replaced by ., and the category as you wrote it. Without a language entry the Controls screen shows k.label if you set one, else the id made readable, and your mod’s name when you left category out.

{
  "key.my_mod.say_hi": "Say hi",
  "key.categories.my_mod": "My Mod"
}

The client tick

CL_EV_TICK_CLIENT_POST runs 20 times a second in the game window, also on servers without your mod. It is the right place for checking keys and updating anything your HUD shows:

static int g_height = 0;

static void on_client_tick(void* payload, void* user) {
    (void)payload;
    (void)user;
    cl_entity me = cl_client_player();
    if (CL_IS_NULL(me)) return;   // no world open
    cl_vec3 pos;
    if (cl_entity_position(me, &pos) == CL_OK) g_height = (int)floor(pos.y);
}

cl_client_player() is you, the player at this computer, or a null handle when no world is open. On the client you can read it like any player: position, health, cl_player_inventory_get. Changing things (giving, teleporting) is for the server.

Chat on the client

CL_EV_CLIENT_CHAT_SEND happens when you press Enter in chat, before the message leaves your game. Cancel it to keep the message to yourself, which makes chat-style commands for client mods possible:

static bool str_is(cl_str s, const char* text) {
    size_t n = strlen(text);
    return s.len == n && memcmp(s.ptr, text, n) == 0;
}

static void on_chat_send(void* payload, void* user) {
    (void)user;
    cl_ev_chat_send* e = CL_EVENT(cl_ev_chat_send, payload);
    if (str_is(e->message, "!where")) {
        cl_event_cancel(&e->header);   // never sent to the server
        cl_vec3 pos;
        if (cl_entity_position(cl_client_player(), &pos) == CL_OK) {
            CL_LOG("I am at %.0f %.0f %.0f", pos.x, pos.y, pos.z);
        }
    }
}

CL_EV_CLIENT_CHAT_RECEIVED happens when a message arrives. Change message or cancel the event to hide it. message is the text without colours, and overlay is true for action-bar messages. To search inside a cl_str, copy it into a \0-terminated array first, so you can use strstr:

// Hide join and leave messages.
static void on_chat_received(void* payload, void* user) {
    (void)user;
    cl_ev_chat_received* e = CL_EVENT(cl_ev_chat_received, payload);
    char msg[512];
    size_t n = e->message.len < sizeof msg - 1 ? e->message.len : sizeof msg - 1;
    memcpy(msg, e->message.ptr, n);
    msg[n] = '\0';
    if (strstr(msg, "joined the game") || strstr(msg, "left the game")) {
        cl_event_cancel(&e->header);
    }
}

These snippets use <string.h> and <math.h>; include them at the top of your file.

Raw keys

CL_EV_CLIENT_KEY reports every key press and release, before the game handles it. Payload: cl_ev_key.

static void on_key(void* payload, void* user) {
    (void)user;
    cl_ev_key* e = CL_EVENT(cl_ev_key, payload);
    if (e->action == 1 && !e->screen_open) CL_LOG("key code %d", e->key);
}
Field Means
key The key’s code, as in GLFW (71 is G, 32 is space).
scancode The physical key on the keyboard.
action 1 pressed, 0 released, 2 held down and repeating.
modifiers Held modifier keys: add 1 shift, 2 control, 4 alt. Test with e->modifiers & 2.
screen_open true while a menu or the chat is open.

Cancelling CL_EV_CLIENT_KEY hides the key from the game. Use it sparingly: players expect their keys to work. For your own features, a keybind is nearly always the better choice.

Other client events

Event Happens when
CL_EV_CLIENT_CONNECT You join a server (e->address) or a singleplayer world.
CL_EV_CLIENT_DISCONNECT You leave it.
CL_EV_CLIENT_SCREEN_OPEN A menu opens (e->screen_id).

Try it

Make a keybind on H that logs your current health. Holding Left Alt while you press it should log your position too.

Show a solution
#include <clight_easy.h>

static cl_keybind g_health_key, g_alt_key;

static void on_client_tick(void* payload, void* user) {
    (void)payload;
    (void)user;
    while (cl_keybind_consume_click(g_health_key)) {
        cl_entity me = cl_client_player();
        if (CL_IS_NULL(me)) continue;
        CL_LOG("health: %.1f", cl_living_health(me));
        cl_vec3 pos;
        if (cl_keybind_is_down(g_alt_key) && cl_entity_position(me, &pos) == CL_OK) {
            CL_LOG("position: %.1f %.1f %.1f", pos.x, pos.y, pos.z);
        }
    }
}

static void on_client_setup(void* payload, void* user) {
    (void)payload;
    (void)user;
    cl_keybind_desc k = CL_KEYBIND_DESC_INIT;
    k.id = CL_STR("my_mod:show_health");
    k.default_key = CL_STR("key.keyboard.h");
    k.category = CL_STR("key.categories.my_mod");
    cl_keybind_register(&k, &g_health_key);

    k.id = CL_STR("my_mod:with_position");
    k.default_key = CL_STR("key.keyboard.left.alt");
    cl_keybind_register(&k, &g_alt_key);

    CL_ON(CL_EV_TICK_CLIENT_POST, on_client_tick);
}

static cl_result mod_main(void) {
    return CL_ON(CL_EV_LIFECYCLE_CLIENT_SETUP, on_client_setup);
}

CLIGHT_MOD_ENTRY(mod_main)

The same descriptor is reused for the second keybind: only the fields that differ are changed. The second keybind is only used with cl_keybind_is_down, so it works as a modifier.

Recap: register keybinds with a cl_keybind_desc in CL_EV_LIFECYCLE_CLIENT_SETUP and check cl_keybind_consume_click in CL_EV_TICK_CLIENT_POST; the client chat and key events let client mods react to chat and keys.