Running scripts
This page describes how the client runs your code: the editor, the console, the lifecycle of the VM, what happens in one frame and the time limits that protect the game.
Execute and Clear VM
-
Open a script in the Lua tab and press Execute. The editor text is executed as is, it does not need to be saved first.
-
There is one VM. Each Execute runs the code inside it: globals, listeners, receivers and signals of earlier executions stay alive and keep running. An execution that fails prints its error and leaves the VM running.
-
Clear VM calls
on_unloadand theunloadsignal, then destroys the VM: every global, listener, receiver, signal, image and packet block of every execution is gone. The next Execute starts a fresh VM. -
New Clear VM also drops every pending timer and spawned coroutine without calling them, and writes every workspace table, after
on_unloadand theunloadsignal. -
Scripts run in a sandbox: the only disk access is
xiao.fsandxiao.workspace, limited toC:\xiaohook.
Editor
-
New The clipboard is read off the render thread: a paste lands at the current caret as soon as Windows hands the text over, and a slow clipboard no longer freezes the game.
-
While the editor has the focus, key presses do not reach the game.
Console
Execute opens the console window. It floats over the game while the menu is open:
- drag its header to move it, drag its edges to resize it
- Clear empties it, the cross closes it
- it opens again on the next Execute
print(...) writes to the console, arguments are separated by tabs. Errors are shown in red with the file name, the
line and a stack traceback. The console keeps the last 512 lines and cuts a line at 4096 bytes.
Lifecycle callbacks
on_frame is a single global: the last execution that defines it wins. An error in on_frame removes it
(on_frame removed: ... in the console). An error in a paint listener, a packet receiver or a signal callback fired by
the client only removes that callback.
The signals think and unload (see xiao.signal) are the preferred way to hook these
events, because several executions can each add their own callback.
local elapsed = 0
xiao.signal.add("think", "timer", function(dt)
elapsed = elapsed + dt
end)
xiao.signal.add("unload", "report", function()
print(("ran for %.1f seconds"):format(elapsed))
end)
Order of a frame
New Every frame runs the same steps in the same order.
-
packet receivers, for every packet captured since the last frame
-
the
world_changesignal, when the world state changed -
the
entity_addandentity_removesignals, in arrival order (at most 256) -
the
chatsignal (at most 64) -
the
damagesignal (at most 64 hits, then the local hurt) -
New the timers and spawned coroutines that are due, by due time then creation order
-
on_frame(dt) -
the
thinksignal -
the paint listeners
Steps 2 to 9 run in the same 100 ms budget. Game signals are fired only while a VM is running and never for what happened before it started, see Events.
Packet hooks and cmd hooks are not part of the frame: they
run at the moment the packet passes or the tick is simulated, see
Execution contexts
Code runs in one of five contexts. Hooks run outside the frame, so most of the API is refused or inert there.
-
The error reads
xiao.config is not available inside a packet hook or a cmd hook, withxiao.workspace,xiao.notify,xiao.timer.spawn,xiao.game.set_stateanddebug.sethookin place ofxiao.config. -
A hook that needs one of the refused functions calls
xiao.timer.after(0, fn)and does the work infn, which runs on the render thread at the next frame. -
Outside the frame (main chunk,
on_unload, hooks) everyxiao.gamefunction returns empty results:players()andentities()give{},player()givesnil,block()givesnil, "unavailable". Read what you need inthinkand keep it in an upvalue. -
Hooks have a 2 ms budget (see below).
xiao.fsandrequirecount in it.
Time limits
The limit also applies to loops already compiled by the JIT. A timeout cannot be caught with pcall or xpcall, and
no script can turn it off: debug.sethook cannot install a hook, jit.attach and jit.profile are removed, and the
2 ms budget is set again at the start of every hook. Only a __gc finalizer runs outside the limits, see
debug.
Keep frame work small: do heavy computation in chunks over several frames, and cache what does not change every frame.
Modules with require
require("name") looks for name.lua and name/init.lua in C:\xiaohook\lua only. Dots are directory separators:
a.b means a/b.lua. package.path and package.cpath are ignored and native modules cannot be loaded.
local util = require("lib.util")
This loads C:\xiaohook\lua\lib\util.lua once and returns its value; later calls return the cached value. require
also finds the modules LuaJIT preloads (table.new, table.clear, string.buffer, jit.util; jit.profile is removed). A module that
requires itself, directly or through another module, fails with loop or previous error loading module.
Example: a script that cleans up after itself
local paint = xiao.paint
local white = paint.color.rgb(255, 255, 255)
local frames = 0
paint.add("counter", function(width, height)
frames = frames + 1
paint.text.left(paint.point.vector2(20, 20), "frames: " .. frames, white)
end)
xiao.signal.add("unload", "counter", function()
print("drew " .. frames .. " frames")
end)
Press Execute to start the counter and Clear VM to stop it: the unload callback prints the total in the console.