Events
Status: Implemented. 43 events with priorities, cancellation, results, filters and mutable fields. Events defined by mods and value events are Planned.
Events are the main way mods react to the game. The engine fires an event at a fixed point (a block is broken, a player joins, a tick ends), and every subscribed handler runs in order. The full list is Complete event list; every payload field is in the generated event page.
Anatomy
| Property | Meaning |
|---|---|
| Name | Dotted, lowercase, for example block.break. The C constant is CL_EV_BLOCK_BREAK, the C++ type clight::ev::BlockBreak. |
| Payload struct | cl_ev_block_break in C. Fields are read-only unless marked mut. |
| Thread context | Where handlers run: MAIN (startup), TICK, RENDER or ANY (Threading model). |
| Side | server, client or both. |
| Cancellable | Whether a handler can stop the action. |
| Result | player.interact_block and player.interact_item also carry an interaction result. |
All payloads begin with a common header:
typedef struct cl_event_header {
uint32_t struct_size; // size of the full payload
cl_event_type type;
uint32_t flags; // CL_EVF_CANCELLABLE, CL_EVF_HAS_RESULT, ...
uint8_t cancelled; // mut, if cancellable
int8_t result; // mut, if the event has a result (cl_interaction_result)
uint16_t reserved;
} cl_event_header;
typedef struct cl_ev_block_break {
cl_event_header header;
cl_level level;
cl_block_pos pos;
cl_block_state state;
cl_entity player; // the breaking player
cl_item_stack tool; // borrowed for the duration of the call
int32_t xp; // mut: experience to drop
uint8_t drop_items; // mut: 0 drops nothing (the block is still removed)
} cl_ev_block_break;
Subscribing
typedef void (*cl_handler_fn)(void* payload, void* user);
typedef struct cl_subscribe_opts {
uint32_t struct_size;
int32_t priority; // CL_PRIORITY_NORMAL by default
uint32_t flags; // CL_SUB_RECEIVE_CANCELLED, CL_SUB_ONCE
cl_ident filter; // optional: only for this block, item, entity type or screen (0 = all)
cl_level level; // optional: only this level
} cl_subscribe_opts;
cl_result cl_event_subscribe(cl_event_type type, const cl_subscribe_opts* opts,
cl_handler_fn fn, void* user, cl_sub* out);
cl_result cl_event_unsubscribe(cl_sub sub);
A diamond-ore guard in each language:
static void on_break(void* payload, void* user) {
cl_ev_block_break* e = CL_EVENT(cl_ev_block_break, payload);
(void)user;
if (cl_block_state_is(e->state, CL_STR("minecraft:diamond_ore")))
cl_event_cancel(&e->header);
}
cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT;
o.priority = CL_PRIORITY_EARLY;
cl_event_subscribe(CL_EV_BLOCK_BREAK, &o, on_break, NULL, NULL);
auto sub = clight::on<clight::ev::BlockBreak>([](cl_ev_block_break& e) {
if (cl_block_state_is(e.state, CL_STR("minecraft:diamond_ore"))) clight::cancel(e.header);
}, {.priority = CL_PRIORITY_EARLY}); // keep `sub` (a member), or call .detach()
cl.on("block.break", function(event)
if event.block == "minecraft:diamond_ore" then
event:cancel()
end
end, { priority = "early" })
Lua reads payload fields by name (event.pos, event.state, event.player, event.xp) and adds a few easy fields: event.block (the block id of state), event.item (the item id of stack or tool), event.world (= event.level) and event.player on entity.* events when the entity is a player. Writing a mut field (event.xp = 0) changes it.
cl_event_subscribe may be called in any phase. The subscription is live from the next dispatch. cl.once(name, fn) / CL_SUB_ONCE unsubscribes after the first call.
Priorities and order
| Constant | Value | Lua | Use |
|---|---|---|---|
CL_PRIORITY_FIRST |
-2000 | "first" |
Must see the event before anyone (protection) |
CL_PRIORITY_EARLY |
-1000 | "early" |
Changes other handlers should see |
CL_PRIORITY_NORMAL |
0 | "normal" |
Default |
CL_PRIORITY_LATE |
1000 | "late" |
Reacts to what earlier handlers decided |
CL_PRIORITY_LAST |
2000 | "last" |
Final say |
CL_PRIORITY_MONITOR |
10000 | "monitor" |
Observe the outcome. By convention, don’t change anything here. |
Any int32_t is allowed, and lower runs first. Handlers with equal priority run in subscription order, which follows the mod load order (dependencies first, ties by id). The order is the same on every run.
Cancellation
- Cancelling sets
header.cancelled = 1(cl_event_cancel(&e->header),clight::cancel(e.header),event:cancel()). Cancelling an event that isn’t cancellable does nothing in C and raises an error in Lua. - A handler can un-cancel by writing 0 (
e->header.cancelled = 0). - Handlers after a cancellation are skipped, unless they subscribed with
CL_SUB_RECEIVE_CANCELLED(Lua{ receive_cancelled = true }). - What cancel means is listed per event in Complete event list. For example, a cancelled
block.breakkeeps the block and resyncs it to the client, the way vanilla handles a break in adventure mode.
Results
player.interact_block and player.interact_item carry a cl_interaction_result (vanilla InteractionResult):
| C | Lua | Meaning |
|---|---|---|
CL_RESULT_PASS (default) |
"pass" |
Not handled. Vanilla continues. |
CL_RESULT_SUCCESS |
"success" |
Handled. Swing the arm. |
CL_RESULT_CONSUME |
"consume" |
Handled, no arm swing. |
CL_RESULT_FAIL |
"fail" |
Handled as a failure. |
A result other than pass skips the vanilla handling (cl_event_set_result(&e->header, CL_RESULT_SUCCESS), Lua event:set_result("success")).
Filters
filter limits a handler to one id; the dispatcher skips the handler without calling it. The engine passes a filter key for:
| Event | Filter key |
|---|---|
block.break, block.place, player.interact_block |
the block id |
player.interact_item |
the item id |
entity.damage, entity.death |
the entity type |
client.screen_open |
the screen id |
config.reloaded |
the mod id |
cl.on("entity.death", function(event) ... end, { filter = "minecraft:creeper" })
When no mod subscribes to an event, firing it is a single load and branch. Unmodded Celestial pays nothing.
Events, mixins and behaviours
Events report that something happens and allow cancelling or the documented mutable fields. To change how the game computes something inside a function, use a mixin on one of the game’s targets (Mixins); for your own blocks and items, use their behaviour callbacks (Events, behaviours and mixins).
Planned: events defined by mods for other mods, value events (result fields that start as the vanilla value), and more vanilla events (see the planned section of Value event list).