Skip to content

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.break keeps 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).