xiao.packet

What a Bedrock packet is, how it is encoded on the wire, and where xiao.packet plugs into the game.

Read, write, send, intercept and block game packets from Lua, in the spirit of the gLua net library. Everything lives at xiao.packet.

This page explains what you are manipulating. The other pages are references, and the guides walk through typical tasks.

What is a Bedrock packet

Everything a Bedrock client and server say to each other is a game packet. On the wire they travel in layers:

Layer Role
RakNet reliability, ordering and fragmentation over UDP
Batch several game packets grouped in one datagram payload
Compression the batch is compressed
Encryption after the login handshake the batch is encrypted
Game packet a header followed by the payload

xiao.packet works at the last layer, on the game network thread of the client: you see decrypted, decompressed game packets, and you never deal with RakNet, batching or encryption.

Header and payload

A game packet is a header and a payload:

  • The header is an unsigned varint that holds the packet id (0 to 1023) and the sub-client ids (split screen).
  • The payload is everything after the header. It is what Lua sees in msg.payload and what you build with a writer.

The client adds the header on send, so you only ever write the payload.

Wire encoding

Payloads use the Bedrock binary format:

Type Encoding Lua side
integers and floats little endian, fixed size read_short, read_int, read_int64, read_float, read_double
unsigned varint 7 bits per byte, low bits first read_varuint, read_varuint64
signed varint zigzag then varint read_varint, read_varint64
string unsigned varint length, then the bytes read_string
vec2, vec3 2 or 3 floats read_vec2, read_vec3
block position zigzag x, unsigned y, zigzag z read_blockpos
boolean 1 byte read_bool

Each reader has its writer twin (read_x and write_x).

Names and ids

A packet is named by its id or by its name. "TextPacket", "Text" and "text" all resolve to the same id: case does not matter and the Packet suffix is optional. An unknown name raises unknown packet '<name>' and an id outside 0 to 1023 raises integer out of range; only xiao.packet.id(name) returns nil instead. xiao.packet.name(id) and msg.name give the full name ("TextPacket").

print(xiao.packet.id("MovePlayer"))
print(xiao.packet.name(9))

Where the client plugs in

You want to Use Page
watch packets without touching them receive, reader, fields Receiving
send your own start, writer, send_fields Sending
change or cancel the packets the game sends or receives hook Hooks
change the player input (angles, movement, buttons) xiao.cmd.hook xiao.cmd
drop an id entirely block Blocking
turn a captured packet into code Copy as Lua Packet log

Receivers run on the render thread, once per frame, after the fact. Hooks run synchronously on the network thread and decide what the game sees, with the restrictions of their execution context. Sends are queued and flushed by the network thread, just before the next packet the game sends.

Page map

Page Content
Sending start, write_*, send, writer, send_fields, encode, the send queue
Receiving receive, the packet object, read_*, fields, decode
Hooks New modify or cancel packets in flight, budget, hook_stats
Blocking block, unblock, unblock_all, is_blocked
Packet log New Copy as Lua and Copy as receiver
Limits every numeric limit

Quick look

Print every MovePlayer the game receives, decoded by field name:

xiao.packet.receive("MovePlayer", "where", function(msg)
    local f = msg:fields()
    if f ~= nil and f._error == nil then
        print(msg.outgoing and "sent" or "received", f.position.x, f.position.y, f.position.z, f.mode)
    end
end)