Sending

Build a payload with write_* or from a fields table, and send it to the server.

Build a payload byte by byte, or from a table of named fields, and send it on the connection of the game. A walkthrough is in the send guide.

At a glance

Function Purpose
start(packet) begin the current message
write_*(...) append to the current message
send() send the current message
cancel() drop the current message
writer(packet?) independent writer object
send_raw(packet, bytes) send a ready payload
send_fields(packet, fields) New encode a table and send
encode(packet, fields) New encode a table, do not send
pending(), capacity() New send queue state

Two styles

gLua style, with a current message. A chat TextPacket from Steve:

xiao.packet.start("Text")
xiao.packet.write_bool(false)
xiao.packet.write_byte(1)
xiao.packet.write_byte(1)
xiao.packet.write_string("Steve")
xiao.packet.write_string("hello")
xiao.packet.write_string("")
xiao.packet.write_string("")
xiao.packet.write_bool(false)
local ok, err = xiao.packet.send()

Chainable and reusable. An arm swing of the local player:

local me = xiao.game.player()
if me ~= nil then
    local w = xiao.packet.writer("Animate")
    w:write_byte(1):write_varuint64(me.id):write_float(0):write_bool(false):write_byte(0)
    print(w:send())
    print(w:size(), #w:data())
end

No byte layout to remember, names come from the decoder:

local ok, err = xiao.packet.send_fields("MovePlayer", fields)
if not ok then print(err) end

Current message

xiao.packet.start(packet)

xiao.packet.start(packet: number | string): writer

Starts the current message and returns its writer. A previous message that was not sent is dropped.

Name Type Description
packet number or string packet id or name

Errors: an unknown name raises unknown packet '<name>', an id outside 0 to 1023 raises integer out of range.

xiao.packet.write_*(...)

xiao.packet.write_*(...): writer

Writes into the current message and returns its writer. Same functions and arguments as the writer methods.

Errors: raises an error without start (packet.write_byte called without packet.start), and the errors of the writer method.

xiao.packet.send()

xiao.packet.send(): boolean, string

Sends the current message and clears it.

Returns: true, or false, error. Errors: raises packet.send called without packet.start.

xiao.packet.cancel()

xiao.packet.cancel()

Drops the current message.


Writer

xiao.packet.writer(packet?)

xiao.packet.writer(packet?: number | string): writer

Returns a new independent writer. Without packet, set the id later with writer:id(packet).

Name Type Description
packet number or string optional packet id or name

xiao.packet.send_raw(packet, bytes)

xiao.packet.send_raw(packet: number | string, bytes: string): boolean, string

Sends a payload already built as a string.

Name Type Description
packet number or string packet id or name
bytes string payload, up to 512 bytes

Returns: true, or false, error (packet payload exceeds 512 bytes, or a send error).

local ok, err = xiao.packet.send_raw("Animate", saved_payload)

Writer methods

Each write_* returns the writer, so calls chain. A number outside the range of the type, or with a fraction, raises integer out of range.

Method Writes
write_bool(v) 1 byte, 0 or 1 (any Lua value, by truth)
write_byte(n) 1 byte, -128 to 255
write_short(n), write_ushort(n) 2 bytes
write_int(n), write_uint(n) 4 bytes
write_int64(n), write_uint64(n) 8 bytes
write_float(n) IEEE 754, 4 bytes
write_double(n) IEEE 754, 8 bytes
write_varint(n) zigzag varint, 32 bits
write_varint64(n) zigzag varint, 64 bits
write_varuint(n) unsigned varint, 32 bits
write_varuint64(n) unsigned varint, 64 bits
write_string(s) unsigned varint length, then the bytes
write_bytes(s) raw bytes, no length
write_vec2(x, y) 2 floats; also takes a table with x, y
write_vec3(x, y, z) 3 floats; also takes a table with x, y, z
write_blockpos(x, y, z) zigzag varint x, unsigned varint y (negative y wraps), zigzag varint z; also takes a table, raises block coordinates must be 32-bit integers

The ushort, uint and uint64 variants are aliases of the signed ones with the same size. Writing past 512 bytes raises packet payload exceeds 512 bytes.

Other writer methods

Method Returns Description
writer:id(packet?) number or nil gets or sets the packet id, nil while it is not set
writer:size() number payload length
writer:data() string payload string
writer:clear() writer resets the payload to empty
writer:send() boolean, string sends the packet; true or false, error. The writer can be sent again
writer:fields(fields) New writer replaces the payload with fields encoded for the writer id

writer:fields raises the encoding error, packet id is not set, or packet payload exceeds 512 bytes.

local w = xiao.packet.writer("MovePlayer")
w:fields(saved_fields)
for i = 1, 3 do w:send() end

Encoding from fields New

send_fields, encode, writer:fields and table results of hooks take a table of the same shape as msg:fields(). The encoder has no schema of its own: it replays the built-in decoder and writes each value the decoder reads, so every packet the decoder knows can be encoded, and only those.

xiao.packet.send_fields(packet, fields)

xiao.packet.send_fields(packet: number | string, fields: table): boolean, string

Encodes fields and sends the payload.

Name Type Description
packet number or string packet id or name
fields table same shape as msg:fields()

Returns: true, or false, error when the payload was encoded but could not be sent: packet payload exceeds 512 bytes (530 encoded), send queue full, retry next frame, or another send error. Errors: raises the encoding error, like encode: missing field runtime_id for a field the packet needs that is absent and cannot be computed (no value defaults to 0), field mode: unknown value 'fly', packet 999 has no field decoder, use send_raw. An unknown packet name or a fields that is not a table raises too.

local f = xiao.packet.fields("MovePlayer", saved_payload)
f.position.x = f.position.x + 2
f.mode = "teleport"
f.has_teleport = nil
local ok, err = xiao.packet.send_fields("MovePlayer", f)
if not ok then print(err) end

print(pcall(xiao.packet.send_fields, "Animate", { action = "swing" }))

The last line prints false missing field runtime_id: Animate also needs data and hand, only has_animation_source is computed.

xiao.packet.encode(packet, fields)

xiao.packet.encode(packet: number | string, fields: table): string

Returns the encoded payload without sending it, with no size limit.

Name Type Description
packet number or string packet id or name
fields table same shape as msg:fields()

Errors: raises the encoding error (missing field position.x).

local f = xiao.packet.fields(id, bytes)
assert(xiao.packet.encode(id, f) == bytes)

What the encoder accepts

Lua value Accepted for
number integers (range checked against the wire type), floats, angles in degrees
decimal string integers past 2^53 ("18446744073709551615")
name string or number enums (mode = "teleport" or mode = 2)
boolean, or 0 and 1 booleans and presence bytes
{ x, y, z } vec2, vec3, block positions
array lists (attribute[1])
byte string text, raw data (item.extra), UUID ("xxxxxxxx-xxxx-..." or 16 bytes)

Fields that can be computed are optional:

Field Value when absent
has_<name> true when the same table has <name> or a key starting with <name>_ (has_teleport from teleport_cause)
<name>_length length of the byte string <name>
count of a list number of entries of the matching list (links from link, metadata from meta, slots from slot), 0 without one

Encoding errors

encode, send_fields and writer:fields raise these errors. Only the fields of the table above are computed: every other value the packet reads must be given, nothing defaults to 0. Errors name the field with Lua indexes:

Error Cause
missing field position.x a value the packet needs is absent and cannot be computed
field mode: unknown value 'fly' enum name not known
field runtime_id: integer out of range value does not fit its wire type
field links = 2 but link has 3 entries count and list disagree
field teleport_cause is not part of the encoded packet (check has_* and counts) a value the packet does not read with these presence flags and counts (also catches typos)
packet 999 has no field decoder, use send_raw the decoder does not know this id
field input_flag: '<old>' is the old name of '<new>', give only one of them a list holds a value under its old and its new name, see Renamed fields
fields table has more than 65536 values table too big
fields table nested too deep more than 64 levels of tables
fields table keys must be names or list indexes a key that is neither a string nor an integer from 1 to 65536
unsupported field value of type function a value that is not a boolean, number, string or table

Renamed fields

Enum values renamed to the exact names of the game schema (build 1.26.6201.0). msg:fields(), the packet log text, Copy as Lua and Copy as receiver use the new name. The encoder (send_fields, encode, writer:fields, hook tables) accepts both and writes the same bytes; giving both names in the same list is an error.

Packet Field Old name New name Schema name
PlayerAuthInput input_flag auto_jump_water auto_jumping_in_water AutoJumpingInWater
PlayerAuthInput input_flag item_interaction perform_item_interaction PerformItemInteraction
PlayerAuthInput input_flag block_actions perform_block_actions PerformBlockActions
PlayerAuthInput input_flag item_stack_request perform_item_stack_request PerformItemStackRequest
PlayerAuthInput input_flag ack_server_data client_ack_server_data ClientAckServerData
PlayerAuthInput input_flag predicted_vehicle is_in_client_predicted_vehicle IsInClientPredictedVehicle
PlayerAuthInput input_flag block_break_delay block_breaking_delay_enabled BlockBreakingDelayEnabled
PlayerAuthInput input_flag camera_relative is_camera_relative_movement_enabled IsCameraRelativeMovementEnabled
PlayerAuthInput input_flag rotation_move_direction is_rot_controlled_by_move_direction IsRotControlledByMoveDirection
PlayerAuthInput input_flag hotbar_touch_only is_hotbar_only_touch IsHotbarOnlyTouch
PlayerAuthInput input_mode gamepad game_pad GamePad
Animate action swing_arm swing Swing

The input_flag names are the xiao.cmd button names: both read the same table. A list with both names fails with field input_flag: 'block_break_delay' is the old name of 'block_breaking_delay_enabled', give only one of them.

local f = xiao.packet.fields("PlayerAuthInput", payload)
for index, name in ipairs(f.input_flag) do
    if name == "block_breaking_delay_enabled" then f.input_flag[index] = "block_break_delay" end
end
assert(xiao.packet.encode("PlayerAuthInput", f) == payload)

Rules

  • Values the decoder only computes are ignored: item.nbt, item.can_place_on, meta[1].name, empty_slots.
  • NBT is written from <name>_raw (nbt_raw, meta[1].value_raw, the network NBT bytes that fields() returns next to the text). Changing the NBT text without changing _raw raises an error; an empty text with no _raw writes an empty NBT.
  • Bytes the decoder leaves uninterpreted are in _rest (prefix-only decoders, or bytes after the last field); they are written back after the fields. Other keys starting with _ (_error) are ignored.
  • encode(id, fields(id, bytes)) == bytes holds for every complete decode: tested on all 6279 packets of the real exports (34 over 512 bytes).
  • Cost: the encoder decodes the packet once, plus once for each presence flag, count or length that differs from zero (tens of microseconds for movement packets, 60 to 85 ms for a 4.7 KB inventory).

Send queue New

A send does not leave immediately. It takes one of 8 places and waits for the next packet the game sends on the same connection, which carries it out just before its own packet. A send still waiting after 2 seconds is dropped instead of sent.

xiao.packet.pending()

xiao.packet.pending(): number

Returns: the number of send places in use (queued and not yet flushed or dropped).

xiao.packet.capacity()

xiao.packet.capacity(): number

Returns: the number of send places: 8.

if xiao.packet.pending() < xiao.packet.capacity() then
    xiao.packet.send_fields("MovePlayer", fields)
end

Complete example

Capture one outgoing MovePlayer, then send a copy one block higher, retrying while the queue is full:

local saved = nil

xiao.packet.receive("MovePlayer", "lift", function(msg)
    if saved == nil and msg.outgoing then
        local f = msg:fields()
        if f ~= nil and f._error == nil then
            f.position.y = f.position.y + 1
            saved = f
        end
    end
end)

xiao.signal.add("think", "lift_send", function()
    if saved == nil or xiao.packet.pending() >= xiao.packet.capacity() then return end
    local ok, err = xiao.packet.send_fields("MovePlayer", saved)
    if not ok then print("send failed:", err) end
    saved = nil
    xiao.packet.remove("MovePlayer", "lift")
    xiao.signal.remove("think", "lift_send")
end)