Each enabled script runs in its own sandbox on its own worker thread - scripts never run on
the engine or render thread.
Callback
Cadence
Runs on
Typical use
on_tick()
every engine tick
script worker
reading state, aim, abilities, input
on_draw()
every render frame
script worker
draw.* overlay primitives
Dispatch is non-blocking: if the previous frame's call is still running, the next frame is
skipped rather than queued. draw.* calls do not draw immediately - they are recorded and
replayed by the render thread inside the scene, so everything you draw appears one frame
later and is always scene-safe.
Each on_tick / on_draw call has a 50 ms wall-clock budget. A script that overruns
(infinite loop, runaway recursion) or raises a Lua error faults its sandbox: it is
disabled permanently for the session, the error shows on the script card, and its pending
taps are dropped. A bad script can never stall the engine, the render thread, or other
scripts.
Globals and script-local tables persist across ticks for the life of the sandbox
(activation to hero swap / disable / fault):
lua
local track = {} -- persists across tickslocal cd = {}
functionon_tick()ifnot util.cooldown(cd, 250) thenreturnend-- every 250 mslocal e, d = util.sticky_enemy(track, { fov = 150, bone = 'head' })
-- ...end
os.clock() is available for timing (seconds, float). Entity/config tables from the host
are rebuilt or rebound on activation - keep identity in e.id and config keys in locals
only as caches.
The runtime is a hard MoonSharp sandbox. Available: math, string, table, bit32,
os.clock/date/time/difftime, pcall/xpcall/error, metatables, plus the SDK modules.
Explicitly not available: io, os.exit/execute/getenv/remove, debug,
load/loadfile/dofile/require, coroutines, and any CLR bridge. There is no filesystem or
network access from scripts.
Per-script content limits (silently enforced on declaration): 8 menu cards; per card
12 sliders, 8 toggles, 4 keybinds, 4 dropdowns, 2 pixel shapes; slider range +/-1000;
draw commands capped per frame (8192); key.tap explicit duration capped at 120 ticks.
Scripts active for the same hero tick independently. Their fire/key output is merged once
per tick by the shared arbiter: any script's hold/tap asserts the action, and any
script's block wins over all of them (see Input & aim). Aim output is
last-call-wins per tick across all scripts, issued as one clamped step.
game.ult_threats (and the util.ult_threat_on_me / util.ult_threat_on_mate wrappers)
only surface a threat after a randomized 150-300 ms delay, applied once in
the host for every script. Do not add your own extra delay on top.