Lynx Lua scripting

Guide

Sandbox limits

Every script runs in its own Lua state with the limits below. The limits are fixed in the runtime; a script cannot raise them.

Memory

Limit Value
Heap per script 8 MiB
Heap for all scripts together 32 MiB
Source file size 1 MiB
Scripts loaded at once 16

An allocation that would cross a heap limit fails inside Lua as a memory error. A source file larger than 1 MiB is refused at load. The seventeenth Load is refused with “too many scripts are loaded; unload one first”.

Instructions and time

The top level and each event callback have an instruction budget and a wall-clock budget. The runtime counts instructions with a Lua hook that fires every 100 instructions, so a budget is enforced to the nearest 100.

Stage Instructions Wall clock
Top level (load) 200 000 250 ms
Each event callback (paint, game events) 50 000 4 ms
The shutdown event 50 000 50 ms

The top level of the file gets the full load budget. When a budget runs out the callback stops with “instruction budget exceeded” or “time budget exceeded”.

What happens on an error

  • An error in the top level refuses the load. The Lua state is closed and the message shows in the tab.
  • An error in an event callback closes the script. It shows as error: in the tab and stops running until you load it again.
  • An error in the shutdown callback is logged; the script is unloaded regardless.

What is opened

The Lua standard libraries base, table, string, math and utf8 are opened. io, os, debug, package and coroutine are not.

What is removed

These globals are removed from every script:

print warn collectgarbage dofile load loadfile require rawget rawset rawequal rawlen setmetatable getmetatable

string.dump is also removed. There is no way to load code at run time and no bytecode is ever accepted; only text source compiles.

The require that the marketplace provides for pinned dependencies is a different function; see require and pinned dependencies.

Each script has its own globals

A script’s top-level assignments go into a per-script _ENV, not the shared globals. Two loaded scripts never see each other’s variables. Reads fall through to the standard globals, so string.format and math.floor work as usual.

Register your event callbacks from the top level with client.set_event_callback; each is an ordinary closure that captures the script’s own upvalues.