Read packets

Capture packets, decode them into named fields and react to them, step by step.

This guide goes from "print every packet" to decoding a packet into a table. For the full list of functions see Receiving.

1. See what passes

Register a receiver on "*" and count by name:

local counts = {}
xiao.packet.receive("*", "count", function(msg)
    local key = msg.name or msg.id
    counts[key] = (counts[key] or 0) + 1
end)

xiao.signal.add("unload", "count", function()
    for key, n in pairs(counts) do print(key, n) end
end)

The packet object has id, name (absent for an id without a name), outgoing, blocked, complete, size and payload. msg.outgoing tells you the direction: true for packets the client sends, false for those it receives.

2. Read a packet by hand

Pick a packet by name and read its payload in order with read_*:

xiao.packet.receive("Text", "chat_log", function(msg)
    local needs_translation = msg:read_bool()
    local body = msg:read_byte()
    local text_type = msg:read_byte()
    if body == 1 then
        local source = msg:read_string()
        print(text_type, needs_translation, source, msg:read_string())
    end
end)

The Text payload starts with needs_translation (bool), body (byte) and type (byte); body == 1 means the text has an author, so source comes before message. You need to know the layout. The packet log shows it, and Copy as receiver writes this code for you.

3. Decode into named fields New

msg:fields() decodes the payload with the decoder of the packet log and returns a table with the same names:

xiao.packet.receive("MovePlayer", "where", function(msg)
    local f = msg:fields()
    if f == nil then return end
    if f._error ~= nil then
        print("partial decode:", f._error)
        return
    end
    print(f.runtime_id, f.position.x, f.position.y, f.position.z, f.mode)
end)

Always handle the two special cases:

Case Meaning
fields() returns nil the decoder does not know this packet
f._error ~= nil the payload is cut or invalid, you get the fields read before the problem

Lists are arrays starting at 1, vectors are { x, y, z }, enums are name strings with the names of the game schema (Animate action is "swing", see Renamed fields). The full shape is in Receiving.

4. Decode saved bytes New

xiao.packet.fields(packet, bytes) works on any string, handy with a payload saved to disk. Save one first from a receiver, then decode it later:

xiao.packet.receive("MovePlayer", "save_one", function(msg)
    if msg.outgoing then
        xiao.fs.write("saved_move.bin", msg.payload)
        xiao.packet.remove("MovePlayer", "save_one")
    end
end)
local bytes = xiao.fs.read("saved_move.bin")
local f = xiao.packet.fields("MovePlayer", bytes)
if f ~= nil and f._error == nil then print(f.position.x) end

xiao.packet.reader(bytes, packet) gives a packet object over a string when you want read_* instead.

5. Keep your own state

Receivers are plain functions, so keep state in upvalues. Here, the last position the server forced on you:

local last_teleport = nil

xiao.packet.receive("MovePlayer", "teleports", function(msg)
    if msg.outgoing then return end
    local f = msg:fields()
    if f ~= nil and f._error == nil and f.mode == "teleport" then
        last_teleport = f.position
    end
end)

xiao.signal.add("think", "show_teleport", function()
    if last_teleport ~= nil then
        print("last teleport", last_teleport.x, last_teleport.y, last_teleport.z)
        last_teleport = nil
    end
end)

Tips

  • Use a stable identity per receiver: executing the script again replaces it instead of stacking. xiao.packet.remove(packet, identity) takes it out.

  • A receiver that raises an error is removed. Guard against nil fields.

  • Receivers run during the frame, so they can read xiao.game and draw with xiao.paint; a hook cannot.

  • If xiao.packet.dropped() grows, the receivers are too slow or too many packets match: filter by name rather than "*". The limits are in Limits.

Related pages: Receiving, Packet log, Send packets, Modify packets.