Sending
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.
Sends never raise
send, writer:send, send_raw and send_fields return false and a message instead of raising (only send() without start raises): no outgoing packet seen yet: join a world first, send queue full, retry next frame (8 pending sends), network hook is not installed, packet id is not set, packet payload exceeds 512 bytes. Always check the result. An encoding error of send_fields (missing field runtime_id) is a mistake in the script, not a send failure: it raises, like encode.
At a glance
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.
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
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).
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.
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.
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
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.
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 encodemissing 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.
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
Fields that can be computed are optional:
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:
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.
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 thatfields()returns next to the text). Changing the NBT text without changing_rawraises an error; an empty text with no_rawwrites 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)) == bytesholds 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.
Sends are flushed in the order of the send calls: each send gets a sequence number and the flush sorts by it, even when places were freed out of order. The packet goes to the connection, with the reliability, of the last outgoing packet of that id on the current connection, or else of the last outgoing packet. Sent packets do not reach hooks, but receivers see them as outgoing.
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)