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: set it up in on_client_setup.
A keybind
class SayHiMod final : public clight::Mod {
public:
void on_client_setup() override {
clight::keybind::add("my_mod:say_hi", "key.keyboard.g", [] {
clight::log::info("G was pressed");
}, {.category = "key.categories.my_mod"});
}
};
- Register keybinds in
on_client_setup. They are client-only; on a dedicated server, registering fails withCL_E_WRONG_SIDE. clight::keybind::add(id, default_key, lambda, options)registers the keybind and calls the lambda once for every press, also for two quick presses in one tick. The lambda may capture what it needs ([this]is common).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".- The options are
.category,.labeland.context, all optional.
add returns an Expected<clight::Keybind>. Keep the clight::Keybind when you want to ask about the key yourself:
consume_click()returnstrueonce for each press since the last check, thenfalse. Check it every client tick, in awhileloop for two quick presses. (Leave the lambda out ofaddwhen you do this, or the lambda takes the presses first.)is_down()istruewhile the key is held, for things like “zoom while holding”.
class ZoomMod final : public clight::Mod {
public:
void on_client_setup() override {
if (auto k = clight::keybind::add("my_mod:zoom", "key.keyboard.z")) zoom_ = *k;
clight::on<clight::ev::TickClientPost>([this](cl_ev_tick&) {
zooming_ = zoom_.is_down();
}).detach();
}
private:
clight::Keybind zoom_;
bool zooming_ = false;
};
zoom_.on_press(lambda) adds a press handler to a keybind you already have. It returns a clight::Subscription; drop it with .reset() to stop listening, or .detach() it.
By default a keybind only reacts in the game, not while a menu or the chat is open. Pass .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 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 the .label option 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
ev::TickClientPost 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:
class HeightMod final : public clight::Mod {
public:
void on_client_setup() override {
clight::on<clight::ev::TickClientPost>([this](cl_ev_tick&) {
clight::Entity me = clight::entity::client_player();
if (!me) return; // no world open
if (auto pos = me.position()) height_ = static_cast<int>(std::floor(pos->y));
}).detach();
}
private:
int height_ = 0;
};
clight::entity::client_player() is you, the player at this computer, or an empty handle when no world is open. On the client you can read it like any player: position, health, inventory_get. Changing things (giving, teleporting) is for the server.
Chat on the client
ev::ClientChatSend 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:
clight::on<clight::ev::ClientChatSend>([](cl_ev_chat_send& e) {
if (clight::sv(e.message) == "!where") {
clight::cancel(e.header); // never sent to the server
if (auto pos = clight::entity::client_player().position()) {
clight::log::info("I am at {:.0f} {:.0f} {:.0f}", pos->x, pos->y, pos->z);
}
}
}).detach();
ev::ClientChatReceived 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. std::string_view can search inside it directly:
// Hide join and leave messages.
clight::on<clight::ev::ClientChatReceived>([](cl_ev_chat_received& e) {
std::string_view msg = clight::sv(e.message);
if (msg.find("joined the game") != std::string_view::npos || msg.find("left the game") != std::string_view::npos) {
clight::cancel(e.header);
}
}).detach();
find returns the position of the text, or npos when it isn’t there.
Raw keys
ev::ClientKey reports every key press and release, before the game handles it. Payload: cl_ev_key.
clight::on<clight::ev::ClientKey>([](cl_ev_key& e) {
if (e.action == 1 && !e.screen_open) clight::log::info("key code {}", e.key);
}).detach();
| 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 ev::ClientKey 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 |
|---|---|
ev::ClientConnect |
You join a server (e.address) or a singleplayer world. |
ev::ClientDisconnect |
You leave it. |
ev::ClientScreenOpen |
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.hpp>
class HealthKeyMod final : public clight::Mod {
public:
void on_client_setup() override {
const clight::keybind::KeyOpts opts{.category = "key.categories.my_mod"};
if (auto alt = clight::keybind::add("my_mod:with_position", "key.keyboard.left.alt", opts)) alt_key_ = *alt;
clight::keybind::add("my_mod:show_health", "key.keyboard.h", [this] {
clight::Entity me = clight::entity::client_player();
if (!me) return;
clight::log::info("health: {:.1f}", me.health());
if (alt_key_.is_down()) {
cl_vec3 pos = me.pos();
clight::log::info("position: {:.1f} {:.1f} {:.1f}", pos.x, pos.y, pos.z);
}
}, opts);
}
private:
clight::Keybind alt_key_;
};
CLIGHT_MOD(HealthKeyMod)
The second keybind has no lambda and is only used with is_down(), so it works as a modifier. The options are kept in one KeyOpts so both keys land in the same category.
Recap: register keybinds with clight::keybind::add(id, key, lambda) in on_client_setup (the lambda runs once per press), or keep the clight::Keybind and ask is_down() / consume_click(); the client chat and key events let client mods react to chat and keys.