Listening to events
Lesson 5.1
Almost everything a mod does starts with an event: a player joins, a block breaks, a tick passes. You tell the game which events you care about, and it calls your lambda each time one happens.
clight::on
clight::on<clight::ev::PlayerJoin>([](cl_ev_player& e) {
clight::Entity player = e.player;
clight::log::info("{} joined", player.name());
}).detach();
The event type goes between the <>, the handler between the (). The handler can be any lambda (or function) that takes the event’s payload by reference. That is all most handlers need.
Subscriptions and detach()
clight::on gives back a clight::Subscription. Like clight::Text, it is an RAII object: while it exists, your handler is subscribed, and when it is destroyed, the handler is unsubscribed.
That is why the examples end with .detach(). Without it, the subscription would be a temporary object that is destroyed at the end of the line, and your handler would never run:
// Wrong: the subscription is destroyed at the semicolon.
clight::on<clight::ev::PlayerJoin>([](cl_ev_player& e) { clight::Entity(e.player).give("minecraft:bread"); });
// Right: detach() keeps the handler for as long as the mod runs.
clight::on<clight::ev::PlayerJoin>([](cl_ev_player& e) { clight::Entity(e.player).give("minecraft:bread"); }).detach();
The compiler catches the wrong version: Subscription is marked [[nodiscard]], so dropping one gives a warning, “a dropped clight::Subscription unsubscribes at once: keep it in a member, or call .detach()”. Treat that warning as an error.
When you want to stop a handler later, keep the subscription instead of detaching it. A member of your mod class is a good place:
class CountMod final : public clight::Mod {
public:
void on_construct() override {
tick_sub_ = clight::on<clight::ev::TickServerPost>([this](cl_ev_tick&) {
ticks_seen_ = ticks_seen_ + 1;
if (ticks_seen_ == 100) {
clight::log::info("100 ticks seen, stopping");
tick_sub_.reset(); // unsubscribe
}
});
}
private:
clight::Subscription tick_sub_;
int ticks_seen_ = 0;
};
reset() unsubscribes right away and frees the lambda, even from inside the handler itself: the wrapper waits until the handler has returned. Assigning a new subscription to tick_sub_ would unsubscribe the old one first.
Capturing state
A handler often needs some state: a counter, a setting, the mod itself. The lambda’s capture list [...] decides what it takes along.
[this]captures the mod object. Members liketicks_seen_above are then reachable, and they live as long as the mod does. This is the most common choice.[name]copies a value into the lambda when it is created.[&name]captures a reference. Don’t do this with local variables: the handler runs long after the function that subscribed it has returned, and the variable is gone by then.
void on_construct() override {
std::string greeting = "Welcome!";
clight::on<clight::ev::PlayerJoin>([greeting](cl_ev_player& e) { // a copy: safe
clight::Entity(e.player).send_message(greeting);
}).detach();
}
W3Schools explains capture lists in Lambdas.
Options
clight::on takes a second argument with options, a clight::SubOpts. Write the fields you want, by name:
clight::on<clight::ev::PlayerJoin>([](cl_ev_player& e) {
clight::Entity(e.player).send_message("You are the first one today!");
}, {.flags = CL_SUB_ONCE}).detach();
CL_SUB_ONCE removes the handler after its first call. The other options (priority, filter and level) come in Changing and cancelling events. Fields you leave out keep their defaults. They must be written in the order they are declared: .priority, .flags, .filter, .level.
Event payloads only live during the call
The payload reference is valid while your handler runs. When it returns, the game reuses that memory. Strings inside it (like a chat message) are gone too. If you need a value later, copy it:
class LastBreakerMod final : public clight::Mod {
public:
void on_construct() override {
clight::on<clight::ev::BlockBreak>([this](cl_ev_block_break& e) {
last_breaker_ = e.player; // a handle is a small value: copying it is fine
// keeping a pointer or reference to e itself would not work later
}).detach();
}
private:
clight::Entity last_breaker_;
};
Handles can be copied like numbers. Strings must be copied into a std::string, as Handles and strings showed.
Many handlers, many files
You can subscribe to the same event as often as you like, also from different .cpp files. Each handler runs on its own. This keeps code tidy: one file for chat features, another for blocks.
// src/chat.hpp
#pragma once
void chat_init();
// src/chat.cpp
#include <clight.hpp>
#include "chat.hpp"
void chat_init() {
clight::on<clight::ev::PlayerChat>([](cl_ev_player_chat& e) {
clight::log::info("chat: {}", clight::sv(e.message));
}).detach();
}
// src/main.cpp
#include <clight.hpp>
#include "chat.hpp"
class ChatMod final : public clight::Mod {
public:
void on_construct() override { chat_init(); }
};
CLIGHT_MOD(ChatMod)
clight build compiles every source file in src/ and finds headers there too. Only one file may contain CLIGHT_MOD. #pragma once makes sure a header is read only once per file.
The C API underneath
clight::on is a thin layer over the C function cl_event_subscribe:
cl_result cl_event_subscribe(cl_event_type type, const cl_subscribe_opts* opts,
cl_handler_fn fn, void* user, cl_sub* out);
The wrapper puts your lambda into a small box on the heap, passes the box as user, and gives the game a plain function that unpacks it. The boxes are tiny and stay until the mod unloads, so subscribing in a loop thousands of times is the only way to waste memory with it.
Every event also has a text name, like "block.break", and clight::events::type_lookup("block.break") finds its number at run time. The clight::ev types are simpler, and a typo in them is a compile error instead of a CL_E_NOT_FOUND at run time.
Try it
Log a line every time a player places a block, with the player’s name and the position. Then make it stop by itself after 5 placements.
Show a solution
#include <clight.hpp>
class PlaceLogMod final : public clight::Mod {
public:
void on_construct() override {
place_sub_ = clight::on<clight::ev::BlockPlace>([this](cl_ev_block_place& e) {
clight::Entity placer = e.placer;
std::string name = placer.is_player() ? placer.name() : "something";
placed_ = placed_ + 1;
clight::log::info("{} placed a block at {} {} {}", name, e.pos.x, e.pos.y, e.pos.z);
if (placed_ >= 5) {
place_sub_.reset();
clight::log::info("stopped logging");
}
});
}
private:
clight::Subscription place_sub_;
int placed_ = 0;
};
CLIGHT_MOD(PlaceLogMod)
The block-place event calls its entity placer, not player, because dispensers and other things can place blocks too. name becomes “something” for those cases.
Recap: clight::on<clight::ev::...>(lambda) subscribes a handler and returns a Subscription that unsubscribes when destroyed, so .detach() it or keep it in a member; capture [this] or values, never local references, and copy what you need from the payload.