Read packets
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.
Receivers run once per frame, after the fact, before the client signals of that frame. They cannot change the packet. To change it, see Modify packets.
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:
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
identityper 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
nilfields. -
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.