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 returnsCL_E_WRONG_SIDE. default_keyuses 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_clickreturnstrueonce for each press since the last check, thenfalse. Check it every client tick; thewhilehandles two quick presses in one tick.cl_keybind_is_downistruewhile 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.