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: 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 with CL_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_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".
  • The options are .category, .label and .context, all optional.

add returns an Expected<clight::Keybind>. Keep the clight::Keybind when you want to ask about the key yourself:

  • consume_click() returns true once for each press since the last check, then false. Check it every client tick, in a while loop for two quick presses. (Leave the lambda out of add when you do this, or the lambda takes the presses first.)
  • is_down() is true while 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.