Modify packets New
A hook decides what the game sends or receives. This guide covers the patterns and the rules that keep the game smooth. The full reference is Hooks.
The rule to remember
Hooks run on the game network thread and the game waits for them. You get 1 ms to obtain the VM and 2 ms in total per packet. A slow hook is stopped, the packet passes unchanged, and after 3 overruns in a row the hook is removed. A hook that raises an error, or returns something other than nil, false, a string or a table, is removed at once.
At most 64 hooks exist at once. A blocked packet (xiao.packet.block) never reaches a hook.
1. Cancel a packet
Return false:
xiao.packet.hook("Animate", "no_swing", function(msg)
return false
end)
Cancel only some packets of an id by testing first:
xiao.packet.hook("Text", "mute", function(msg)
if msg.outgoing then return nil end
local f = msg:fields()
if f ~= nil and f.source == "spammer" then return false end
end)
source exists only for the chat, whisper and announcement text types. xiao.packet.unhook("Text", "mute") removes the hook.
2. Change values with fields
Return the edited msg:fields() table and the client encodes it:
xiao.packet.hook("MovePlayer", "nudge", function(msg)
if not msg.outgoing then return nil end
local f = msg:fields()
if f == nil or f._error ~= nil then return nil end
f.position.y = f.position.y + 0.5
return f
end)
This is cheap for small packets (about 30 us for MovePlayer). Enum values use the names of the game schema ("swing", "teleport"); the old names still encode, see Renamed fields. A table that does not encode removes the hook with the encoding error.
3. Change bytes for big packets
A table pays the encoder. For a large packet, such as an inventory of 4.7 KB, that is 60 to 85 ms, far over the budget. Work on the bytes instead and return a string:
xiao.packet.hook("InventoryContent", "bytes", function(msg)
local p = msg.payload
return string.char(0) .. p:sub(2)
end)
The edit above only illustrates the shape: it overwrites the first byte. Use your own byte edit. Incoming replacements cannot be longer than the buffer the game received, and payloads over 64 KiB are not hooked.
4. Watch your budget
xiao.signal.add("think", "hook_report", function()
local s = xiao.packet.hook_stats()
if s.overruns > 0 or s.removed > 0 then
print("hooks in trouble", s.overruns, s.removed, ("%.0f us max"):format(s.max_us))
xiao.signal.remove("think", "hook_report")
end
end)
The counters restart at zero on Clear VM. All of them are listed with xiao.packet.hook_stats() in
Hooks.
What not to do inside a hook
-
no drawing:
xiao.paintdraws nothing -
no
xiao.gamereads: they return empty results -
no
xiao.config,xiao.workspaceorxiao.notifycalls, and noxiao.timer.spawn: they raise... is not available inside a packet hook or a cmd hook -
no long loops, big
string.rep, orxiao.fscalls: they work but eat the 2 ms
What works: print, the xiao.packet functions (a send from a hook goes to the send queue), xiao.json,
xiao.signal, xiao.timer.after, every and cancel (the callback runs later on the render thread), and pure Lua.
The full table per context is in Execution contexts.
If you need game state in a hook, store it from think into an upvalue and read the upvalue:
local my_health = 20
xiao.signal.add("think", "cache", function()
local me = xiao.game.player()
if me and me.health then my_health = me.health end
end)
xiao.packet.hook("Animate", "low_hp_no_swing", function(msg)
if my_health < 6 then return false end
end)
To show something on screen from a hook, do the reverse: set an upvalue in the hook and draw it from a paint listener.
Movement and input: use xiao.cmd
To change the view angles, the movement or the input buttons of the local player, a packet hook on PlayerAuthInput is
the hard way. xiao.cmd gives a cmd object once per tick with getters and setters for all
of them, under the same 2 ms rules, and on two layers: createmove (what the client simulates) and authinput (what
the server receives). A packet hook on PlayerAuthInput sees the payload after the authinput callbacks changed it.
xiao.cmd.hook("createmove", "auto_sprint", function(cmd)
if cmd:get_forward_move() > 0.5 and not cmd:has_button("sneak_down") then
cmd:set_button("sprint_down")
end
end)
See the cmd overview and the examples.
Good practice
- return
nilas early as possible, testmsg.outgoingfirst when only one direction matters - one hook per packet and per job, with a stable identity
- check
fields()is notniland has no_errorbefore touching it - hooks do not see packets sent by
xiao.packet.send - Clear VM stops every hook
Related pages: Hooks, Hook limits, Read packets, Send packets, xiao.cmd.