The event list
Lesson 5.3
This page lists every event the game sends today, with its payload struct and fields. Come back to it whenever you write a new handler.
How to read the tables:
- Side says where the event happens. Server events run where the world is simulated: in singleplayer, or on a server that has your mod. Client events run in the player’s game window. Contexts and sides explains the difference.
- Cancel marks events you can stop with
cl_event_cancel(&e->header). - Fields in bold can be changed (
/* mut */inclight.h). - Every payload also starts with
header, which you only need for cancelling.
Startup and shutdown
These run once, in this order, while the game starts or a world opens. Their payload is cl_ev_lifecycle, and you won’t need its fields.
| Event | Side | Use it for |
|---|---|---|
CL_EV_LIFECYCLE_REGISTER |
both | Registering items, blocks and network channels. |
CL_EV_LIFECYCLE_REGISTRIES_FROZEN |
both | After this, nothing more can be registered. |
CL_EV_LIFECYCLE_COMMON_SETUP |
both | Setup that needs the registered content. |
CL_EV_LIFECYCLE_CLIENT_SETUP |
client | Keybinds, HUD layers and other client things. |
CL_EV_LIFECYCLE_SERVER_SETUP |
server | Server setup. |
CL_EV_LIFECYCLE_SERVER_STARTING |
server | A world is about to open. |
CL_EV_LIFECYCLE_SERVER_STARTED |
server | The world is open. |
CL_EV_LIFECYCLE_SERVER_STOPPING |
server | The world is closing. Save your data here. |
CL_EV_LIFECYCLE_SERVER_STOPPED |
server | The world is closed. |
CL_EV_LIFECYCLE_SHUTDOWN |
both | The game is quitting. |
mod_main runs before all of them, so subscribing there catches every one. In singleplayer, each world you open runs SERVER_STARTING to SERVER_STOPPED again.
A few more come while the game runs:
| Event | Side | Happens when |
|---|---|---|
CL_EV_LIFECYCLE_LEVEL_LOAD |
server | A world (level) is loaded. |
CL_EV_LIFECYCLE_LEVEL_SAVE |
server | A world is saved: on autosave, /save-all and when closing. A good moment to save your own data. |
CL_EV_LIFECYCLE_LEVEL_UNLOAD |
server | A world is unloaded. |
CL_EV_LIFECYCLE_DATA_RELOADED |
server | Data packs were reloaded with /reload. |
CL_EV_LIFECYCLE_RESOURCES_RELOADED |
client | Resource packs were reloaded (F3 + T, or a pack change). |
CL_EV_LIFECYCLE_MOD_RELOAD |
both | A mod’s code was hot reloaded. Today only Lua mods hot reload, so a C mod sees this for other mods only. |
Ticks
The game runs 20 ticks per second. These events run every tick, so keep their handlers short. Payload: cl_ev_tick.
| Event | Side | Fields |
|---|---|---|
CL_EV_TICK_SERVER_PRE |
server | tick_count: the number of the tick |
CL_EV_TICK_SERVER_POST |
server | tick_count |
CL_EV_TICK_LEVEL_PRE |
server | level, tick_count: once per world per tick |
CL_EV_TICK_LEVEL_POST |
server | level, tick_count |
CL_EV_TICK_CLIENT_PRE |
client | |
CL_EV_TICK_CLIENT_POST |
client |
PRE runs before the game’s own work for that tick, POST after it. When in doubt, use POST. The level ticks run for each world (overworld, Nether, End) separately, with level set.
Players
| Event | Payload | Side | Cancel | Fields |
|---|---|---|---|---|
CL_EV_PLAYER_JOIN |
cl_ev_player |
server | player |
|
CL_EV_PLAYER_LEAVE |
cl_ev_player |
server | player |
|
CL_EV_PLAYER_RESPAWN |
cl_ev_player |
server | player |
|
CL_EV_PLAYER_CHAT |
cl_ev_player_chat |
server | yes | player, message |
CL_EV_PLAYER_COMMAND |
cl_ev_player_command |
server | yes | player, command (without the /) |
CL_EV_PLAYER_INTERACT_BLOCK |
cl_ev_player_interact_block |
server | yes | player, level, hand, pos, face |
CL_EV_PLAYER_INTERACT_ITEM |
cl_ev_player_interact_item |
server | yes | player, level, hand, stack |
CL_EV_PLAYER_ATTACK_ENTITY |
cl_ev_player_attack_entity |
server | yes | player, target |
hand is CL_HAND_MAIN or CL_HAND_OFF. face is the side of the block that was clicked, CL_DIRECTION_DOWN to CL_DIRECTION_EAST. stack is a borrowed copy of the stack in the hand: read it, don’t release it.
Blocks
| Event | Payload | Side | Cancel | Fields |
|---|---|---|---|---|
CL_EV_BLOCK_BREAK |
cl_ev_block_break |
server | yes | player, level, pos, state, drop_items |
CL_EV_BLOCK_PLACE |
cl_ev_block_place |
server | yes | placer, level, pos, state, hand, replaced_state |
state is the block state as a number. Compare it with cl_block_state_is(e->state, CL_STR("minecraft:stone")).
tool is a borrowed copy of the stack in the player’s main hand (read it, don’t release it), and xp is the experience the block will drop. Set xp to 0 to drop none, or raise it for a bonus. Both are filled in for vanilla blocks; your own mod blocks drop no experience unless you set xp.
Entities
| Event | Payload | Side | Cancel | Fields |
|---|---|---|---|---|
CL_EV_ENTITY_DAMAGE |
cl_ev_entity_damage |
server | yes | entity, level, source, attacker, amount |
CL_EV_ENTITY_DEATH |
cl_ev_entity_death |
server | yes | entity, level, source, attacker |
entity is the hurt or dying entity; cl_entity_is_player(e->entity) tells you whether it is a player. attacker is whoever caused it, or a null handle. source is the damage type as an interned id; get its name with cl_ident_name:
char src[64];
size_t len = 0;
cl_ident_name(e->source, src, sizeof src, &len); // "minecraft:fall", "minecraft:player_attack", ...
Cancelling entity.death skips the death. Give the entity some health back with cl_living_set_health(e->entity, 4.0f) in the same handler, or it has none left.
Commands
| Event | Payload | Side | Fields |
|---|---|---|---|
CL_EV_SERVER_COMMAND_REGISTER |
cl_ev_command_register |
server | dedicated: true on a dedicated server |
Register your commands in here. Commands shows how.
Client
| Event | Payload | Side | Cancel | Fields |
|---|---|---|---|---|
CL_EV_CLIENT_CONNECT |
cl_ev_client_connect |
client | address: the server address |
|
CL_EV_CLIENT_DISCONNECT |
cl_ev_lifecycle |
client | ||
CL_EV_CLIENT_SCREEN_OPEN |
cl_ev_screen_open |
client | screen_id |
|
CL_EV_CLIENT_KEY |
cl_ev_key |
client | yes | key, scancode, action, modifiers, screen_open |
CL_EV_CLIENT_CHAT_SEND |
cl_ev_chat_send |
client | yes | message: what the player is about to send |
CL_EV_CLIENT_CHAT_RECEIVED |
cl_ev_chat_received |
client | yes | message, overlay (true for action-bar text) |
CL_EV_CLIENT_SOUND_PLAY |
cl_ev_sound_play |
client | yes | sound, category, pos, volume, pitch, positional |
Client events also fire on servers that don’t have your mod. That is what makes client-only mods possible.
Most of these events can be filtered to one block, item, entity type or screen id; Changing and cancelling shows how.
Try it
Write one handler that greets a player when they join, and another that logs when a player died and what killed them (the damage source).
Show a solution
#include <clight_easy.h>
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_easy_send_messagef(player, "Hi %s, have fun!", name);
}
static void on_death(void* payload, void* user) {
(void)user;
cl_ev_entity_death* e = CL_EVENT(cl_ev_entity_death, payload);
if (!cl_entity_is_player(e->entity)) return; // only players
char name[64], src[64];
size_t len = 0;
cl_easy_player_name(e->entity, name, sizeof name);
if (cl_ident_name(e->source, src, sizeof src, &len) != CL_OK) snprintf(src, sizeof src, "something");
CL_LOG("%s died (%s)", name, src);
}
static cl_result mod_main(void) {
cl_result r = CL_ON(CL_EV_PLAYER_JOIN, on_player_join);
if (r == CL_OK) r = CL_ON(CL_EV_ENTITY_DEATH, on_death);
return r;
}
CLIGHT_MOD_ENTRY(mod_main)
cl_easy_player_name works on e->entity because it is a player here; for other entities it would give an empty name.
Recap: startup events register and set up, tick events run 20 times a second, and player, block, entity and client events report what happens; the table tells you the payload struct to use with CL_EVENT.