Skip to content

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.synced and entity.knockback events. Equipment (get and set every slot, drop chances), and the entity.spawn, player.interact_entity and entity.change_dimension events (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:entities channel (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 names vanilla_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), nil instead of not_found from nearest_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).