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 function each time one happens.
CL_ON, the short way
static void on_player_join(void* payload, void* user) {
(void)user;
cl_entity player = CL_EVENT(cl_ev_player, payload)->player;
char name[64];
cl_easy_player_name(player, name, sizeof name);
CL_LOG("%s joined", name);
}
// in mod_main:
CL_ON(CL_EV_PLAYER_JOIN, on_player_join);
That is all most handlers need. Behind it is cl_event_subscribe, the full function, which can do more.
cl_event_subscribe, the full way
cl_result cl_event_subscribe(cl_event_type type, const cl_subscribe_opts* opts,
cl_handler_fn fn, void* user, cl_sub* out);
| Parameter | Means |
|---|---|
type |
The event, like CL_EV_PLAYER_JOIN. |
opts |
Options such as the priority, or NULL for the defaults. |
fn |
Your handler. |
user |
Any pointer you like. The game passes it back to your handler as user. |
out |
Where to store the subscription, so you can stop it later. NULL if you don’t need it. |
CL_ON(event, fn) is exactly cl_event_subscribe(event, NULL, fn, NULL, NULL).
The user pointer
user lets one handler serve several purposes. The game doesn’t look at it; it only hands it back:
static void on_say(void* payload, void* user) {
cl_entity player = CL_EVENT(cl_ev_player, payload)->player;
cl_easy_send_message(player, (const char*)user);
}
// in mod_main:
cl_event_subscribe(CL_EV_PLAYER_JOIN, NULL, on_say, (void*)"Welcome!", NULL);
cl_event_subscribe(CL_EV_PLAYER_RESPAWN, NULL, on_say, (void*)"Welcome back to life!", NULL);
The pointer must stay valid as long as the subscription exists. String literals and static variables are safe; a local variable of mod_main is not, because it is gone when mod_main returns.
Stopping a handler
Keep the subscription and pass it to cl_event_unsubscribe:
static cl_sub tick_sub;
static int ticks_seen = 0;
static void on_tick(void* payload, void* user) {
(void)payload;
(void)user;
ticks_seen = ticks_seen + 1;
if (ticks_seen == 100) {
CL_LOG("100 ticks seen, stopping");
cl_event_unsubscribe(tick_sub);
}
}
// in mod_main:
cl_event_subscribe(CL_EV_TICK_SERVER_POST, NULL, on_tick, NULL, &tick_sub);
If you only want the first time, ask for it with the CL_SUB_ONCE flag. The subscription removes itself after one call:
cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT;
o.flags = CL_SUB_ONCE;
cl_event_subscribe(CL_EV_PLAYER_JOIN, &o, on_first_join, NULL, NULL);
Option structs and _INIT
cl_subscribe_opts is the first of many descriptor structs in the API: structs you fill in and pass to a function. Each one has a matching _INIT macro that sets every field to its default:
cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT; // all defaults
o.priority = CL_PRIORITY_EARLY; // change only what you need
Always start from the _INIT macro. The first field of these structs is their own size, which tells the game which version of the struct your mod was built with. The macro fills it in; a struct you set up by hand would be missing it, and the game rejects it with CL_E_INVALID_ARGUMENT.
Event payloads only live during the call
The payload pointer 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:
static cl_entity last_breaker;
static void on_block_break(void* payload, void* user) {
(void)user;
cl_ev_block_break* e = CL_EVENT(cl_ev_block_break, payload);
last_breaker = e->player; // a handle is a small value: copying it is fine
// keeping the pointer `e` itself would not work later
}
Handles can be copied like numbers. Strings must be copied into your own array, as Handles and strings showed.
Many handlers, many files
You can subscribe to the same event as often as you like, also from different .c files. Each handler runs on its own. This keeps code tidy: one file for chat features, another for blocks.
// src/chat.h
cl_result chat_init(void);
// src/chat.c
#include <clight_easy.h>
#include "chat.h"
static void on_chat(void* payload, void* user) {
(void)user;
cl_ev_player_chat* e = CL_EVENT(cl_ev_player_chat, payload);
CL_LOG("chat: %.*s", (int)e->message.len, e->message.ptr);
}
cl_result chat_init(void) {
return CL_ON(CL_EV_PLAYER_CHAT, on_chat);
}
// src/main.c
#include <clight_easy.h>
#include "chat.h"
static cl_result mod_main(void) {
return chat_init();
}
CLIGHT_MOD_ENTRY(mod_main)
clight build compiles every .c file in src/ and finds headers there too. Only one file may contain CLIGHT_MOD_ENTRY. W3Schools explains splitting code into files in Organizing code.
Looking up events by name
Every event also has a text name, like "block.break". You can look it up at run time:
cl_event_type t = 0;
if (cl_event_type_lookup(CL_STR("block.break"), &t) == CL_OK) {
cl_event_subscribe(t, NULL, on_block_break, NULL, NULL);
}
The CL_EV_ constants 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_easy.h>
static cl_sub place_sub;
static int placed = 0;
static void on_block_place(void* payload, void* user) {
(void)user;
cl_ev_block_place* e = CL_EVENT(cl_ev_block_place, payload);
char name[64] = "something";
if (cl_entity_is_player(e->placer)) cl_easy_player_name(e->placer, name, sizeof name);
placed = placed + 1;
CL_LOG("%s placed a block at %d %d %d", name, e->pos.x, e->pos.y, e->pos.z);
if (placed >= 5) {
cl_event_unsubscribe(place_sub);
CL_LOG("stopped logging");
}
}
static cl_result mod_main(void) {
return cl_event_subscribe(CL_EV_BLOCK_PLACE, NULL, on_block_place, NULL, &place_sub);
}
CLIGHT_MOD_ENTRY(mod_main)
The block-place event calls its entity placer, not player, because dispensers and other things can place blocks too. name starts as “something” for those cases.
Recap: CL_ON(event, fn) subscribes a handler, cl_event_subscribe adds options, a user pointer and a subscription you can stop, descriptor structs start from their _INIT macro, and payloads are only valid during the call.