Send packets

Build and send packets with write_*, with a writer, or from a table of named fields.

This guide shows the three ways to send, and how to start from a captured packet. For every function see Sending.

Before you start

The examples below send an Animate packet (a swing of the main hand). Its payload, as the decoder reads it, is action (byte, 1 is swing), runtime_id (unsigned varint 64), data (float), has_animation_source (bool) and hand (byte, 0 is the main hand). The runtime id comes from xiao.game.player(), which is readable only during a frame, so the sends run from a think callback.

1. Start from a capture: Copy as Lua New

The fastest way: open the packet log, pick a packet and use Copy as Lua. You get a script with named values on top and write_* calls below, see Copy as Lua. Edit the values, press Execute.

2. write_* on the current message

xiao.signal.add("think", "swing_once", function()
    local me = xiao.game.player()
    if me == nil then return end
    xiao.signal.remove("think", "swing_once")

    xiao.packet.start("Animate")
    xiao.packet.write_byte(1)
    xiao.packet.write_varuint64(me.id)
    xiao.packet.write_float(0)
    xiao.packet.write_bool(false)
    xiao.packet.write_byte(0)
    local ok, err = xiao.packet.send()
    if not ok then print(err) end
end)

xiao.packet.cancel() drops a message you started and do not want. A write_* or send without start raises packet.write_byte called without packet.start (or packet.send called without packet.start).

3. A writer object

A writer is independent and reusable, and its methods chain:

xiao.signal.add("think", "swing_three", function()
    local me = xiao.game.player()
    if me == nil then return end
    xiao.signal.remove("think", "swing_three")

    local w = xiao.packet.writer("Animate")
    w:write_byte(1):write_varuint64(me.id):write_float(0):write_bool(false):write_byte(0)
    for i = 1, 3 do
        local ok, err = w:send()
        if not ok then print(err) break end
    end
    print(w:size(), "bytes")
end)

4. From a fields table New

When you know the packet by its field names, skip the byte layout. Decode a payload, edit it, send it back. Here the payload is the MovePlayer saved to disk in Read packets:

local f = xiao.packet.fields("MovePlayer", xiao.fs.read("saved_move.bin"))
if f ~= nil and f._error == nil then
    f.position.y = f.position.y + 2
    local ok, err = xiao.packet.send_fields("MovePlayer", f)
    if not ok then print(err) end
end

The encoder fills what it can compute: has_* presence bytes, *_length and list counts. Enum values take the names of the game schema (action = "swing"); the old names still encode, see Renamed fields. Details and errors are in Encoding from fields.

The Animate of part 2, written with names instead of bytes:

xiao.signal.add("think", "swing_fields", function()
    local me = xiao.game.player()
    if me == nil then return end
    xiao.signal.remove("think", "swing_fields")

    local ok, err = xiao.packet.send_fields("Animate", {
        action = "swing",
        runtime_id = me.id,
        data = 0,
        hand = "mainhand",
    })
    if not ok then print(err) end
end)

Every field the packet reads must be given: without runtime_id, data or hand the call raises missing field runtime_id (then data, hand); only has_*, *_length and list counts are computed.

To get the bytes without sending, use xiao.packet.encode(packet, fields): it raises the same encoding errors and has no size limit.

To fill a writer from fields, use w:fields(fields). It replaces the payload and raises on an encoding error:

local f = xiao.packet.fields("MovePlayer", xiao.fs.read("saved_move.bin"))
local w = xiao.packet.writer("MovePlayer")
w:fields(f)
print(w:send())

5. The send queue New

A send takes one of 8 places and waits for the next packet the game sends on the same connection. Check the queue before a burst, and stop once the job is done:

local sent = 0
local f = xiao.packet.fields("MovePlayer", xiao.fs.read("saved_move.bin"))

xiao.signal.add("think", "burst", function()
    if f == nil or sent >= 20 then
        xiao.signal.remove("think", "burst")
        return
    end
    while sent < 20 and xiao.packet.pending() < xiao.packet.capacity() do
        local ok, err = xiao.packet.send_fields("MovePlayer", f)
        if not ok then
            print(err)
            break
        end
        sent = sent + 1
    end
end)

Sends leave in the order of the calls. A send not flushed within 2 seconds is dropped. Packet hooks never see the packets a script sends.

Limits to remember

Limit Value
payload of send, writer:send, send_raw, send_fields 512 bytes
pending sends 8 (xiao.packet.capacity())
wait for a flush 2 seconds, then the send is dropped
encode no size limit

All limits are in Limits.

Troubleshooting

Error Cause
no outgoing packet seen yet: join a world first the client has not sent a packet yet
send queue full, retry next frame 8 sends pending, retry next frame
network hook is not installed the client network hook is not active
missing field position.x the fields table lacks a value the packet needs: raised by send_fields and encode
field action: unknown value 'swng' enum name not known (typo)
packet payload exceeds 512 bytes too big to send: raised by write_* and writer:fields, returned by send_raw
packet payload exceeds 512 bytes (530 encoded) the same, returned by send_fields
packet 999 has no field decoder, use send_raw no decoder for this id: build the bytes yourself
packet id is not set a writer created without a packet: returned by w:send(), raised by w:fields(); set it with w:id(packet)

Related pages: Sending, Read packets, Modify packets.