Entities
Status: Partly implemented (API 1.20). Entity handles with validity, type, UUID, level, position, teleport and health; custom entity types with a vanilla fallback, spawning and removing, motion, rotation, runtime size and pose (API 1.8); glTF models and layered animation (API 1.9); queries, tags, attributes, effects, synced data, hurting with damage types, mod damage types, knockback control, natural spawn rules and passengers (API 1.10); the
entity.damage,entity.death,entity.tick,entity.anim_event,entity.syncedandentity.knockbackevents. Equipment (get and set every slot, drop chances), and theentity.spawn,player.interact_entityandentity.change_dimensionevents (API 1.20); AI: Entity AI (goals, brains, pathfinding) (API 1.12). Custom renderers beyond glTF models are Planned. Guides: Custom Entities, Entity Models and Animation, Entity Gameplay.
Function table: entity. Players are entities too: Players.
Functions
bool cl_entity_is_valid(cl_entity e); // false once removed (stale handle)
bool cl_entity_is_player(cl_entity e);
int32_t cl_entity_id(cl_entity e); // the network id
cl_uuid cl_entity_uuid(cl_entity e); // TICK
cl_result cl_entity_type(cl_entity e, char* buf, size_t cap, size_t* out_len); // "minecraft:zombie"
cl_level cl_entity_level(cl_entity e);
cl_result cl_entity_position(cl_entity e, cl_vec3* out);
cl_result cl_entity_teleport(cl_entity e, cl_vec3 pos); // server, within its level
float cl_living_health(cl_entity e); // 0 for non-living entities
cl_result cl_living_set_health(cl_entity e, float health); // server
| C | C++ (clight::Entity) |
Lua |
|---|---|---|
cl_entity_is_valid(e) |
e.is_valid() |
e:is_valid() |
cl_entity_type(e, ...) |
e.type() → Expected<std::string> |
e:type() |
cl_entity_position(e, &v) |
e.position() → Expected<cl_vec3>; e.pos() → cl_vec3 |
e.pos (a cl.vec) |
cl_entity_uuid(e) |
e.uuid() |
e.uuid (string) |
cl_entity_level(e) |
e.level() → Level |
e.level / e.world |
cl_entity_teleport(e, v) |
e.teleport(v) |
e:teleport(cl.vec(x, y, z)) |
cl_living_health(e) / set_health |
e.health() / e.set_health(h) |
e.health / e:set_health(h) |
All of these run on the tick (TICK); writes are server only.
API 1.8: custom types, spawning, motion, size
cl_result cl_entity_type_register(const cl_entity_type_desc* desc, cl_holder* out); // lifecycle.register
cl_result cl_entity_type_fallback(cl_str id, char* buf, size_t cap, size_t* out_len); // the vanilla type others see
cl_result cl_level_spawn(cl_level l, cl_str type, cl_vec3 pos, const cl_spawn_opts* opts, cl_entity* out); // server
cl_result cl_entity_remove(cl_entity e, cl_remove_reason reason); // DISCARDED / KILLED, server, not players
cl_result cl_entity_velocity(cl_entity e, cl_vec3* out); // blocks per tick
cl_result cl_entity_set_velocity(cl_entity e, cl_vec3 v); // server; players get SetEntityMotion
cl_result cl_entity_rotation(cl_entity e, cl_rotation* out); // body yaw, pitch (degrees)
cl_result cl_entity_set_rotation(cl_entity e, float yaw, float pitch);
cl_result cl_entity_head_rotation(cl_entity e, cl_rotation* out); // head yaw, pitch
cl_result cl_entity_set_head_rotation(cl_entity e, float yaw, float pitch);
bool cl_entity_on_ground(cl_entity e);
cl_result cl_entity_eye_position(cl_entity e, cl_vec3* out);
cl_result cl_entity_look_vector(cl_entity e, cl_vec3* out); // Entity#getViewVector
cl_result cl_entity_dimensions(cl_entity e, cl_dimensions* out); // width, height, eye_height
cl_result cl_entity_set_dimensions(cl_entity e, float width, float height, float eye_height); // server, not players
cl_result cl_entity_reset_dimensions(cl_entity e);
cl_pose cl_entity_pose(cl_entity e);
cl_result cl_entity_set_pose(cl_entity e, cl_pose pose); // server, not players
bool cl_entity_is_custom(cl_entity e);
cl_entity_type_desc: id, kind (CL_ENTITY_KIND_MOB / MISC / PROJECTILE), vanilla_fallback (defaults minecraft:zombie / minecraft:item_display / minecraft:snowball; a mob needs a mob fallback), category, width, height, eye_height (0: 0.85 x height), max_health, attributes ("id=value, ..." or a JSON object; the fallback’s attributes are the base), gravity (< 0: the kind’s default), tracking_range, update_interval (0: the fallback’s), flags (CL_ENTITY_FIRE_IMMUNE, NOT_SUMMONABLE, NO_GRAVITY, SILENT, PERSISTENT).
cl_spawn_opts: yaw, pitch, nbt (SNBT merged like /summon), velocity, finalize (default true; skipped with a tag).
What goes over the network: a custom entity is sent with its fallback’s network type, so vanilla clients see the fallback. The server adds a clight:entities custom payload (entity id, type id, simulated size) for mod types and for entities with a runtime size; vanilla clients ignore it, Celestial clients use it for entity_type, entity_is_custom, picking and hitboxes. Vanilla entities without a runtime size never cause one.
| C | C++ (clight::Entity / clight::Level) |
Lua |
|---|---|---|
cl_entity_type_register(&d, &h) |
clight::registry::entity_type_register(&d) |
cl.entities.register{...} |
cl_level_spawn(l, type, pos, &o, &e) |
level.spawn(type, pos, &o) |
world:spawn(type, pos, { yaw =, pitch =, nbt =, velocity = }) |
cl_entity_velocity / set_velocity |
e.velocity() / e.set_velocity(v) |
e:velocity() / e:set_velocity(v) or (x, y, z) |
cl_entity_rotation / set_rotation |
e.rotation() / e.set_rotation(yaw, pitch) |
e:rotation() → {yaw=, pitch=} |
cl_entity_dimensions / set_dimensions |
e.dimensions() / e.set_dimensions(w, h, eye) |
e:dimensions() / e:set_dimensions(w, h[, eye]) |
cl_entity_pose / set_pose |
e.pose() / e.set_pose(CL_POSE_CROUCHING) |
e:pose() / e:set_pose("crouching") |
cl_entity_remove(e, CL_REMOVE_KILLED) |
e.remove(CL_REMOVE_KILLED) |
e:remove("killed") |
Feature names for cl.has: entity.custom_types, entity.spawn, entity.motion, entity.dimensions, entity.pose, entity.tick (true); entity.models, entity.animation and the other API 1.9 names below.
API 1.9: animation
cl_result cl_entity_anim_play(cl_entity e, cl_str clip, const cl_anim_play_opts* opts); // server: synced; client: local
cl_result cl_entity_anim_layer(cl_entity e, int32_t layer, const cl_anim_layer_opts* opts);
cl_result cl_entity_anim_stop(cl_entity e, int32_t layer, float fade); // layer -1: every layer
cl_result cl_entity_anim_clip(cl_entity e, int32_t layer, char* buf, size_t cap, size_t* out_len);
cl_result cl_entity_anim_state(cl_entity e, int32_t layer, cl_anim_layer_state* out);
cl_result cl_entity_set_morph(cl_entity e, cl_str name, float weight); // weight < 0: back to the clips
cl_result cl_entity_anim_look_at(cl_entity e, const cl_look_at_opts* opts);
cl_result cl_entity_bone_transform(cl_entity e, cl_str bone, cl_bone_transform* out); // world pos + axes + scale
cl_result cl_entity_anim_root_motion(cl_entity e, cl_vec3* out); // last tick, blocks
cl_result cl_entity_model(cl_entity e, cl_model* out);
They work on every entity whose type has a model (Rendering (blocks, entities, items)); others give CL_E_NOT_FOUND. cl_anim_play_opts: loop (-1 the clip’s own, 0, 1), fade (0.2 s), speed, layer (0…7), weight, additive, mask (bones with their children, "!bone" leaves one out, "*" all), start, root_motion, hold. cl_anim_layer_opts: weight, fade, additive (-1 keep), mask, speed (< 0 keep). cl_look_at_opts: bones (the chain, parent first), target or target_entity (its eyes), speed (degrees per second), max_yaw, max_pitch, weight, enabled.
On the server every change goes to the Celestial clients with CLight that track the entity, and to players who start tracking it later, as one clight:anim custom payload per changed entity and tick (clip names, times relative to now, masks and chains by bone name); vanilla clients ignore it. A client-side call changes only that client’s copy until the server sends a new state.
| C | C++ (clight::Entity) |
Lua |
|---|---|---|
cl_entity_anim_play(e, clip, &o) |
e.anim_play("walk", &o) |
e:anim_play("walk", { layer =, loop =, fade =, speed =, weight =, additive =, mask = { "head" }, root_motion = }) |
cl_entity_anim_layer(e, 1, &o) |
e.anim_layer(1, &o) |
e:anim_layer(1, { weight = 0, fade = 0.5 }) |
cl_entity_anim_stop(e, -1, 0.2f) |
e.anim_stop(-1, 0.2f) |
e:anim_stop() (every layer, 0.2 s) or e:anim_stop(layer, fade) |
cl_entity_set_morph(e, name, w) |
e.set_morph("inhale", 0.5f) |
e:set_morph("inhale", 0.5) |
cl_entity_anim_look_at(e, &o) |
e.anim_look_at(&o) |
e:anim_look_at({ "neck", "head" }, target, speed, { yaw =, pitch = }), e:anim_look_stop() |
cl_entity_bone_transform(e, bone, &t) |
e.bone_transform("jaw") |
e:bone_transform("jaw") → { pos, forward, up, right, scale } |
Feature names: entity.models, entity.animation, entity.anim_layers, entity.anim_events, entity.root_motion, entity.look_at, entity.bone_transform, entity.anim_sync, model.gltf, model.skinning, model.morphs, model.pbr (true); API 1.15: item.models, block.models, block_entity.models, model.lod, model.alpha_mode, model.pbr_shaderpacks (true, Guide: Item and Block Models).
API 1.10: queries, attributes, effects, synced data, combat, spawning, riding
// queries (both sides; on clients the client's copies, without tags / type tags / vanilla categories)
cl_result cl_level_entities_in_box(cl_level, cl_vec3 min, cl_vec3 max, const cl_entity_query*, cl_entity* out, size_t cap, size_t* count);
cl_result cl_level_nearest_player(cl_level, cl_vec3 pos, double range, cl_entity* out); // range <= 0: any; CL_E_NOT_FOUND
cl_result cl_level_entity_by_uuid(cl_level, cl_uuid, cl_entity* out);
cl_result cl_entity_by_uuid(cl_uuid, cl_entity* out); // server: every level
cl_result cl_entity_category(cl_entity, char* buf, size_t cap, size_t* len); // "monster" ... "misc", "player"
bool cl_entity_has_tag(cl_entity, cl_str); cl_result cl_entity_add_tag(...); cl_result cl_entity_remove_tag(...);
// attributes (server)
cl_result cl_entity_attribute(cl_entity, cl_str id, double* out); // value; CL_E_NOT_FOUND / CL_E_UNSUPPORTED
cl_result cl_entity_attribute_base(cl_entity, cl_str id, double* out);
cl_result cl_entity_set_attribute_base(cl_entity, cl_str id, double v);
cl_result cl_entity_add_attribute_modifier(cl_entity, cl_str attr, cl_str modifier, double amount, cl_attribute_op op);
cl_result cl_entity_remove_attribute_modifier(cl_entity, cl_str attr, cl_str modifier);
bool cl_entity_has_attribute_modifier(cl_entity, cl_str attr, cl_str modifier);
// effects (server)
bool cl_entity_add_effect(cl_entity, cl_str effect, int32_t ticks, const cl_effect_opts*); // -1 ticks: infinite
bool cl_entity_remove_effect(cl_entity, cl_str effect);
bool cl_entity_has_effect(cl_entity, cl_str effect);
cl_result cl_entity_effect(cl_entity, cl_str effect, cl_effect_info* out);
int32_t cl_entity_clear_effects(cl_entity);
// synced data (set: server; read: both sides)
cl_result cl_entity_set_synced_bool / _number / _string(cl_entity, cl_str key, value);
cl_result cl_entity_clear_synced(cl_entity, cl_str key);
cl_synced_type cl_entity_synced_type(cl_entity, cl_str key);
bool / double cl_entity_synced_bool / _number(cl_entity, cl_str key);
cl_result cl_entity_synced_string(cl_entity, cl_str key, char* buf, size_t cap, size_t* len);
// combat (server)
bool cl_entity_hurt(cl_entity target, float amount, cl_str damage_type, const cl_hurt_opts*);
cl_result cl_entity_knockback(cl_entity, double strength, double dx, double dz);
cl_result cl_damage_type_register(const cl_damage_type_desc*, cl_holder* out); // registry module
cl_result cl_spawn_rule_register(const cl_spawn_rule_desc*); // registry module, server
// riding (server)
cl_result cl_entity_set_passenger(cl_entity vehicle, cl_entity passenger, const cl_ride_opts*); // force, bone, offset
cl_result cl_entity_dismount(cl_entity); cl_result cl_entity_eject_passengers(cl_entity);
cl_result cl_entity_vehicle(cl_entity, cl_entity* out);
cl_result cl_entity_passengers(cl_entity, cl_entity* out, size_t cap, size_t* count);
- Synced data goes only to clients that registered the
clight:entitieschannel (Celestial clients with CLight do on joining); vanilla clients get nothing. At most 64 keys per entity and 1024-byte strings; saved with the entity (players: per session). - Mod damage types: death messages
death.attack.<message_id>(attacker as%2$s) or.player(kill credit only), with the descriptor’s English text as the translation fallback; the damage event packet namesvanilla_fallback. - Spawn rules join the biome spawn lists like data pack entries; vanilla caps, packs and distances apply. Register before the server starts.
- Lua sugar: query flags as booleans (
nearest = true),level:entities_near(pos, radius, query),nilinstead ofnot_foundfromnearest_player,by_uuid,vehicle,effect;e:set_synced(key, v)/e:synced(key);e:ride(vehicle, opts);cl.damage_types.register{},cl.registry.spawn_rule{}; bare keys and modifier ids get the mod namespace.
Events
| Event | Payload | Notes |
|---|---|---|
entity.damage |
entity, level, source (damage type id), attacker, amount (mut) |
Cancellable. Filter: entity type. |
entity.death |
entity, level, source, attacker |
Cancellable. Filter: entity type. |
player.attack_entity |
player, target |
Cancellable |
entity.tick |
entity, level, type (entity type id), client |
After the entity’s tick, on the server and on clients (client true). Filter: entity type; types nobody listens to cost nothing. |
entity.anim_event |
entity, level, type, name, clip, layer, time, client |
A playing clip passed an event marker (API 1.9). On the server and on Celestial clients that animate the entity. Filter: entity type. |
entity.synced |
entity, level, key, type |
A Celestial client got synced data of an entity (API 1.10). Client only. Filter: the key. |
entity.knockback |
entity, level, type, strength (mut), dx (mut), dz (mut) |
Before a living entity’s knockback (hits, entity_hurt, entity_knockback; not explosions). Cancellable. Filter: entity type. |
-- creepers take double damage
cl.on("entity.damage", function(event)
event.amount = event.amount * 2
end, { filter = "minecraft:creeper" })
Equipment (API 1.20)
cl_item_stack cl_entity_equipment(cl_entity e, cl_equip_slot slot); // owned; empty for an empty slot
cl_result cl_entity_set_equipment(cl_entity e, cl_equip_slot slot, cl_item_stack stack); // server; 0 / an empty stack clears
cl_result cl_entity_set_drop_chance(cl_entity e, cl_equip_slot slot, float chance); // server, mobs (Mob#setDropChance)
Slots in EquipmentSlot order: CL_EQUIP_MAINHAND, OFFHAND, FEET, LEGS, CHEST, HEAD, BODY (horse / wolf / llama armour, the happy ghast harness), SADDLE. Any living entity or player; a player’s slots are its inventory (the selected hotbar slot, the off hand, the armour slots). A mob shows a change to every client (SetEquipment) and gets Mob#setItemSlot’s side effects. On a client the getter returns what the server sent (item and count) or, for the local player, its inventory. Drop chances: 0 never drops, 1 drops (damaged like vanilla), above 1 always drops undamaged. Lua: e:equipment("head"), e:set_equipment("head", "minecraft:diamond_helmet") (nil clears), e:set_drop_chance("head", 1).
New events (API 1.20)
| Event | Payload | When |
|---|---|---|
entity.spawn |
entity, level, loaded |
An entity joined a level on the server (spawned, summoned, bred, or loaded with its chunk: loaded), once it is placed and before it is first sent to players. Cancellable: the entity is removed again. Not players (player.join / player.respawn). Filter: entity type. |
player.interact_entity |
player, target, level, hand |
A player uses (right-clicks) an entity, before the entity’s own interaction (Player#interactOn). Cancel or set_result anything but pass: no vanilla interaction. Filter: the target’s type. |
entity.change_dimension |
entity, from, to |
An entity arrived in another dimension (portals, /tp, /execute in). Level filter: to. |
cl.on("entity.spawn", function(e) e:cancel() end, { filter = "minecraft:phantom" }) -- no phantoms
cl.on("player.interact_entity", function(e)
e.player:send_message("You poked a villager")
end, { filter = "minecraft:villager" })
Planned
Custom entity renderers beyond glTF models (Rendering (blocks, entities, items)), more events (mount, tame, breed).