xiao.cmd New

Read and edit the player input of every tick, on the client before the simulation (createmove) and in the PlayerAuthInputPacket sent to the server (authinput).

The Bedrock version of the Source engine usercmd / CreateMove: a callback receives a cmd object once per game tick and can read or change the view angles, the movement and the input buttons of the local player.

At a glance

Function Returns Purpose
hook(event, identity, callback) nothing install a createmove or authinput callback
unhook(event, identity) boolean remove it
current() cmd \| nil the cmd being processed, nil outside a callback
buttons table every input button: name to index
button_name(index) string \| nil name of a button index
available(event) boolean true when the layer is installed in this game build
stats() table counters and timings of both layers

Bedrock has two places where this input exists, so there are two events with the same cmd object:

Event Where What a change does
createmove on the client, right after the game read the keyboard, before the tick is simulated the client simulates your input and builds the packet from it: client and server agree
authinput in the PlayerAuthInputPacket the client sends to the server (20 per second) only the server sees it: the client view and its own simulation do not change

The overview explains the two layers, when to use which, the order of one tick and the time budget.

The cmd object:

Method createmove authinput Page
valid(), layer(), command_number() yes yes below
get_buttons, set_buttons, has_button, set_button, remove_button jump_down, sprint_down, sneak_down (and up, down, left, right read only) all 66 buttons buttons
get_view_angles, set_view_angles, get_head_yaw, set_head_yaw, lock_angles yes, moves the camera yes, packet only angles
get_move, set_move, get/set_forward_move, get/set_left_move, get/set_up_move, rotate_movement yes yes movement
get_analog_move, set_analog_move error yes movement
get_position() yes yes below
native_aim() always false true when the native aim already aimed this packet overview
get_mouse_delta, set_mouse_delta error error below

Page map

Page Content
overview the two layers, a tick step by step, threads, budget, priority with the native aim
buttons the 66 input buttons with their index and schema name
angles pitch, yaw, head yaw, no roll, lock_angles (silent angles)
movement forward, left, up, analog vector, rotate_movement
examples complete scripts

A cmd callback runs outside the render thread, with the same restrictions as a packet hook: see Execution contexts.


Functions

xiao.cmd.hook(event, identity, callback)

xiao.cmd.hook(event: "createmove" | "authinput", identity: any, callback: function(cmd: cmd))

Installs a callback. Callbacks of one event run in the order they were added, all on the same cmd: the second one sees the changes of the first. A new callback takes effect at the next frame.

Name Type Description
event string "createmove" (client) or "authinput" (packet)
identity any key of the callback; the same key replaces the callback and keeps its place
callback function receives the cmd; its return value is ignored

Returns: nothing.

Errors:

Message Cause
bad argument #1 to 'hook' ('createmove' or 'authinput' expected) another event name
bad argument #2 to 'hook' (identity expected) nil identity
bad argument #3 to 'hook' (function expected) the callback is not a function
too many cmd hooks (32) 32 cmd callbacks in total, both events together
xiao.cmd.hook("createmove", "sprint", function(cmd)
    if cmd:get_forward_move() > 0 then cmd:set_button("sprint_down") end
end)

xiao.cmd.unhook(event, identity): boolean

xiao.cmd.unhook(event: "createmove" | "authinput", identity: any): boolean

Removes a callback. When the last callback of an event is removed the layer costs nothing again.

Name Type Description
event string "createmove" or "authinput"
identity any key given to hook

Returns: true if the callback existed, false otherwise.

Errors: bad argument #1 to 'unhook' ('createmove' or 'authinput' expected).

xiao.cmd.unhook("createmove", "sprint")

xiao.cmd.current(): cmd | nil

xiao.cmd.current(): cmd | nil

Returns: the cmd of the running callback, nil anywhere else. Useful in a helper function that does not receive the cmd as an argument.

local function jump()
    local cmd = xiao.cmd.current()
    if cmd then cmd:set_button("jump_down") end
end

xiao.cmd.buttons: table

xiao.cmd.buttons: table<string, number>

Every input button of the game build, name to index (xiao.cmd.buttons.start_sprinting == 25). The full table is in buttons. A button can be given by name or by index to every button method.

print(xiao.cmd.buttons.jump_down)

xiao.cmd.button_name(index): string | nil

xiao.cmd.button_name(index: number): string | nil
Name Type Description
index number integer from 0 to 127

Returns: the name of a button index (0 to 65), nil for an index the schema does not name (66 to 127).

Errors: bad argument #1 to 'button_name' (integer out of range) for a value that is not an integer from 0 to 127.

print(xiao.cmd.button_name(3))

xiao.cmd.available(event): boolean

xiao.cmd.available(event: "createmove" | "authinput"): boolean
Name Type Description
event string "createmove" or "authinput"

Returns: true when the layer is installed. createmove needs the client tick hook of the pathfinder (ClientInputUpdateSystem), authinput needs the network capture. Updated once per frame. A layer that is not available never calls its callbacks; the other one keeps working.

Errors: bad argument #1 to 'available' ('createmove' or 'authinput' expected).

if not xiao.cmd.available("createmove") then print("createmove is not installed") end

xiao.cmd.stats(): table

xiao.cmd.stats(): table

Returns: the counters since the VM started.

Field Description
createmove, authinput one table per layer, fields below
.calls ticks or packets seen while a callback was installed (or lock_angles angles were pending, for authinput)
.busy skipped because the VM was busy (render thread running the script)
.overruns over the 2 ms budget: every change of that tick is dropped
.errors callbacks removed after an error
.modified ticks or packets actually changed
.removed callbacks removed (errors and overruns)
.malformed authinput only: packets that did not parse, passed unchanged
.avg_us, .max_us time per tick or packet, waiting included
createmove.matched, createmove.mismatched packets whose tick equals, or not, the command_number() announced by the oldest pending createmove (a mismatch resynchronizes)
createmove.lead_us average time from a createmove to the packet that carries it: well under one tick (50 ms) when the packet is built in the same tick
silent_applied, silent_expired angles of lock_angles written into a packet, or dropped after 100 ms
silent_matched angles of lock_angles written into the packet of their own tick (a packet older than that tick never receives them)
apply_failures createmove changes the game memory refused (entity changed between read and write)
state_client New ticks whose client input was changed by xiao.game.set_state
state_server New PlayerAuthInputPacket changed by xiao.game.set_state (held states, start_* and stop_*)
budget_us, wait_us 2000 and 2000 (authinput waits 1000 like packet hooks)
local s = xiao.cmd.stats()
print(s.createmove.calls, s.authinput.avg_us)

Methods shared by both layers

cmd:valid(): boolean

cmd:valid(): boolean

Returns: true only inside the callback that received the cmd. Every other method raises cmd is only valid inside its createmove or authinput callback once the callback returned, so do not keep a cmd for later: copy the numbers you need. tostring(cmd) gives xiao.cmd(createmove, 1234), or xiao.cmd(expired).

cmd:layer(): string

cmd:layer(): "createmove" | "authinput"

Returns: the event of the callback that received the cmd.

cmd:command_number(): number

cmd:command_number(): number | nil

Returns: the Client Tick of the PlayerAuthInputPacket of this tick (the tick the server uses to order inputs). In authinput it is read in the packet. In createmove it is the tick of the packet that will leave with this input: the previous packet tick plus one (one packet per tick, checked on 854 consecutive real packets), so a createmove and the authinput of the same tick give the same number. nil in createmove before the first packet of the session was seen. stats().createmove.matched counts the packets that carried the announced number.

cmd:native_aim(): boolean

cmd:native_aim(): boolean

Returns: true in authinput when the client aim (aura, aim assist on the server packet) already rewrote the angles of this packet, always false in createmove. See priority.

cmd:get_position(): number, number, number

cmd:get_position(): number, number, number

Returns: x, y, z, at eye height like xiao.game.local_player(). In authinput, the position written in the packet (read only). In createmove, the position of the player before the tick is simulated.

xiao.cmd.hook("authinput", "where", function(cmd)
    local x, y, z = cmd:get_position()
    if y < 0 then print("below zero", x, z) end
end)

cmd:get_mouse_delta(), cmd:set_mouse_delta(dx, dy)

Errors: always: mouse_delta is not available: Bedrock applies mouse look per frame (CameraInjectInputLookDeltaSystem), not in the 20 Hz input tick; use set_view_angles. A delta set in a tick would only move the camera frames later and never in this tick. Use set_view_angles instead.


Complete example

Walk forward and jump while a toggle is on, and print the cost of the layer every 5 seconds. More scripts are in examples.

local walking = true
local elapsed = 0

xiao.cmd.hook("createmove", "walker", function(cmd)
    if not walking then return end
    cmd:set_move(1, 0, 1)
    cmd:set_button("sprint_down")
end)

xiao.signal.add("think", "walker_cost", function(dt)
    elapsed = elapsed + dt
    if elapsed < 5 then return end
    elapsed = 0
    local s = xiao.cmd.stats().createmove
    print(("walker: %d ticks, %.1f us average, %d busy"):format(s.calls, s.avg_us, s.busy))
end)