Player control
Status: Implemented (API 1.11). The camera, stackable movement modifiers (speed, jump, step height, no sprinting, no jumping, camera sway), hiding player model parts and putting glTF models on them, animation layers on the vanilla player model, disabled hands and locked slots, forced input, food, saturation, exhaustion, air and per-player natural regeneration. Abilities, experience and respawn points are Planned (Players).
This guide builds an injury mod: a broken leg slows the player down and stops sprinting, a lost arm hides the arm and disables the hand, a peg leg replaces the leg with a model, the player limps with an animation layer, can’t drop a cursed item, and doesn’t heal naturally while bleeding. Everything is server-authoritative and synced, so no player rubber-bands.
Reference: Players, function tables entity, camera, render. Models: Guide: Entity Models and Animation. The world: Guide: World Access.
Who sees what
Gameplay runs on the server. What players see runs on the clients:
| Feature | Vanilla clients | Celestial clients with CLight |
|---|---|---|
| Speed, jump and step height modifiers | yes (vanilla attribute modifiers, their own movement applies them) | yes |
| No sprinting | sprinting gives no extra speed | can’t start sprinting |
| No jumping | yes (jump strength 0) | yes |
| Camera sway | no | yes |
| Hidden parts, part models, animation layers | the normal player | yes, for every player they see |
| Disabled hands, locked slots | the server refuses (the screen snaps back) | the client doesn’t even try |
| Forced input | the server’s copy of crouch (held) and sprint (released): pose, interactions | the movement keys themselves |
Vanilla clients never get a packet they don’t know; their movement stays theirs, so nothing desyncs.
1. View and camera
Players are entities, so player:eye_position(), player:look_vector() and player:rotation() work on both sides. On a client the camera can be somewhere else (third person, spectating):
local cam = cl.camera.get() -- { pos, forward, up, yaw, pitch, fov, first_person, detached }
if cl.camera.is_first_person() then draw_scope_overlay() end
local listener = cl.camera.position()
cl.camera.* is client-only (cl.has("camera") is false on a dedicated server).
2. Movement modifiers
player:add_movement_modifier{ id = "broken_leg", speed_mult = 0.6, jump_mult = 0.5, can_sprint = false, sway = 2 }
player:add_movement_modifier{ id = "mud", speed_mult = 0.8, step_height = -0.1 }
local m = player:movement() -- the combined effect: { speed_mult = 0.48, jump_mult = 0.5, can_sprint = false, ... }
player:remove_movement_modifier("mud")
| Field | Default | Meaning |
|---|---|---|
id |
(required) | the modifier’s id; bare ids get your mod’s namespace; adding the same id again replaces it |
speed_mult |
1 | walking speed (minecraft:movement_speed, multiplied) |
jump_mult |
1 | jump height (minecraft:jump_strength) |
step_height |
0 | blocks added to minecraft:step_height (0.6 by default) |
can_sprint |
true | false: no sprinting |
can_jump |
true | false: no jumping |
sway |
0 | degrees of slow camera sway (0…45) |
Multipliers multiply, step heights and sways add, any false wins. Speed, jump and step height are vanilla attribute modifiers (ADD_MULTIPLIED_TOTAL / ADD_VALUE), so every client’s own movement prediction applies them and the server agrees: no rubber-banding. Modifiers last until removed or the player leaves; they survive death and are not saved (add them again in player.join).
3. Model parts
The parts are the vanilla player model’s: head, hat, body, jacket, left_arm, left_sleeve, right_arm, right_sleeve, left_leg, left_pants, right_leg, right_pants, cape.
player:set_part_visible("left_arm", false) -- the arm, its sleeve, its armour and what the hand holds; first person too
player:set_part_model("right_leg", "my_mod:models/peg_leg.glb", { scale = 1, offset = { 0, -0.1, 0 } })
player:set_part_model("right_leg", nil) -- the vanilla leg again
player:part_visible("left_arm") -- false
A part model is drawn at the part’s pivot (shoulder, hip, neck, waist), follows the part’s animation and replaces the part unless keep_part = true (armour pieces, attachments). Model space: +Y up, +Z forward of the player, 1 unit = 1 block. rotation is degrees around x, y, z; clip loops a clip of the model. On the server these reach every Celestial client with CLight that sees the player (and the player’s own client); on a client they change only that client’s view, until the server sends a change. Load the model on the client at setup (cl.model.load(path)) so the first sight does not load a file.
4. Animation layers on the player
Give the player model clips from a glTF file whose nodes are named like the parts (head, body, left_arm, right_arm, left_leg, right_leg; children like left_sleeve or cape too). The file’s rest pose is the vanilla standing pose; +Y up, +Z forward.
cl.on("lifecycle.register", function()
cl.render.player_animations("my_mod:models/player/injuries.glb") -- both sides
end)
player:anim_play("limp", { layer = 1 }) -- replaces the legs' walk swing
player:anim_play("shiver", { layer = 2, additive = true, weight = 0.5 }) -- on top of everything
player:anim_stop(1, 0.3)
All the entity animation functions work on players: anim_play, anim_layer, anim_stop, anim_state, anim_clip. A non-additive layer replaces the vanilla animation of the parts its clip moves (by the layer weight); additive layers add their change on top of the vanilla walk, head look and arm swing. The server’s state goes to every Celestial client with CLight that sees the player. Several mods can register files; a clip is looked up in every file (registration order).
5. Hands and slots
player:set_hand_disabled("off_hand", true) -- "main_hand" / "off_hand"
player:lock_slot(40) -- the off hand slot can't be moved, dropped, swapped or shift-clicked
player:lock_slot(0, true, true) -- hotbar slot 0: locked, and it can't be selected either
player:unlock_slot(0)
A disabled main hand can’t attack, mine or use; a disabled off hand isn’t used and F can’t swap into it. A locked slot keeps its item: the player can’t move, drop (Q), swap (F, number keys) or shift-click it, and with select_lock a hotbar slot can’t be selected. Your mod still changes locked slots with player:inventory_set. The server refuses what vanilla clients try; Celestial clients with CLight don’t try.
6. Forced input
player:set_input_override{ crouch = true, sprint = false } -- true: held down, false: held up, nil: the player's own
player:input_override() -- { crouch = true, sprint = false }
player:clear_input_override()
The keys are forward, backward, left, right, jump, crouch and sprint. From the server they reach Celestial clients with CLight, whose movement then works as if the keys were pressed (the server accepts the movement as usual). For vanilla clients the server keeps its own copy of crouch held and sprint released (what others see, interactions, sneaking on blocks); their movement stays theirs. On a client, cl.entity.client_player():set_input_override{...} changes only that client’s input (cutscenes, auto-walk).
7. Food, air and regeneration
player:set_food(6) -- 0..20 (FoodData#foodLevel)
player:set_saturation(0) -- 0..20
player:exhaust(2.5) -- Player#causeFoodExhaustion (not in creative / spectator)
cl.log("%d food, %.1f saturation, %.2f exhaustion", player:food(), player:saturation(), player:exhaustion())
player:set_air(60) -- ticks of air (300 full, -20 drowning); every living entity has it
player:set_natural_regen(false) -- no healing from food for this player (the game rule still applies to everyone)
8. C and C++
cl_movement_modifier m = CL_MOVEMENT_MODIFIER_INIT;
m.id = CL_STR("broken_leg");
m.speed_mult = 0.6f;
m.can_sprint = false;
cl_player_add_movement_modifier(player, &m);
cl_player_set_part_visible(player, CL_PART_LEFT_ARM, false);
cl_player_lock_slot(player, 40, true, false);
cl_input_override in = CL_INPUT_OVERRIDE_INIT; /* every key -1 */
in.crouch = 1;
cl_player_set_input_override(player, &in);
clight::Entity p(player);
p.add_movement_modifier(&m);
auto state = p.movement(); // Expected<cl_movement_state>
p.set_natural_regen(false);
cl.has("player.movement_modifiers"), "player.model_parts", "player.part_models", "player.animations", "player.slot_locks" and "player.input_override" tell a mod these exist.