Skip to content

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.