Limits, files and saved values
Every number a script can run into: files, memory, time, drawing, the menu, the tuning modules, what a call costs, the rules for file names, and how a script's control values are saved.
On this page
These are the numbers in the current build. A limit that says "clamped" changes your value without an error. A limit that says "error" raises one.
Files and sources
| limit | value |
|---|---|
| Size of one script | 256 KiB (262,144 bytes). Larger files are refused. |
| Encoding | Valid UTF-8. A leading byte order mark is skipped. A NUL byte is refused. A file that starts with byte 0x1B (a precompiled chunk) is refused. |
| Files shown on the page | 32, sorted by name without regard to case |
| Directory entries we look at | 4096 |
| Scripts loaded at once | 16 |
| File name | 5 to 64 characters including .lua |
Memory
| limit | value |
|---|---|
| One script | 8 MiB |
| All scripts together | 32 MiB |
Time
| call | instructions | wall clock |
|---|---|---|
| Main chunk | 5,000,000 | 100 ms |
events.on_paint | 100,000 | 1.5 ms |
ui.button callback | 100,000 | 1.5 ms |
events.on_unload | 20,000 | 0.5 ms |
| Game event handler, one call | 50,000 | 0.5 ms |
events.on_shot, one call | 50,000 | 0.5 ms |
An on_change function, one call | 100,000 | 1.5 ms |
One call of a surface on_draw | 50,000 | 0.5 ms |
| All scripts together, per frame | not counted | 3 ms |
Two strikes stop a script. Behaviour on overrun is described in How scripts run.
The thread that runs a script must have at least 256 KiB of stack left when we enter it. If it has less, the call is refused with not enough thread stack to run the script and counts as a strike.
Library caps
| function | cap |
|---|---|
| Pattern matching | 200,000 steps per call, pattern up to 256 bytes, subject up to 256 KiB |
string.rep, table.concat | result up to 64 KiB |
table.move | up to 2^18 elements |
table.sort | up to 65,536 elements |
| Lua stack | 60,000 slots |
| Nested C calls | 200 |
Drawing
| limit | value | on overflow |
|---|---|---|
| Commands per script and frame | 2000 | extra commands are dropped |
| Text per command | 128 bytes | cut at a character boundary |
| Text per script and frame | 16 KiB | extra text commands are dropped |
| Coordinates | -16384 to 16384 | clamped |
| Width and height | 0 or more | negative values become 0 |
thickness | 1 to 16 | clamped |
circle radius | 0 to 4096 | clamped |
Corner radius of rounded rectangles and gradients | 0 to 64 | clamped |
Points of draw.polyline | 2 to 64 | error |
Points of draw.polygon | 3 to 32 | error |
| Point numbers per script and frame | 2048 | the command is dropped |
| Open clips | 8 | extra draw.clip calls are dropped |
Drawing in the world
| limit | value | on overflow |
|---|---|---|
| Lines per script and frame | 256 | the line is skipped |
| Polylines per script and frame | 16 | the polyline is skipped |
Points of draw.polyline3d | 2 to 64 | error |
| Polyline point numbers per script and frame | 3072 | the polyline is skipped |
| Boxes per script and frame | 64 | the box is skipped |
| Circles per script and frame | 32 | the circle is skipped |
segments of draw.circle3d | 3 to 64, default 32 | clamped |
| Hitboxes per script and frame | 64 | the hitbox is skipped |
| Hitbox ids | 0 to 18 | nothing is drawn |
| Projected segments per script and frame | 1024 | the shape is skipped |
| World coordinates, sizes and radii | -1e6 to 1e6 | clamped |
| Distance in front of the camera | 16384 units | the shape is cut away |
| Cost of a call | 30 instructions plus 3 for each segment |
A line counts 1 segment, a polyline its points minus 1 (plus 1 when closed), a box 12, a circle its segments and a hitbox 16.
Controls
| limit | value |
|---|---|
| Interactive controls per script | 64 |
| Labels per script | 32 (they do not count against the 64) |
| Groups per script | 12, the default group included |
| Label of a control | 48 bytes |
| Name of a group | 32 bytes |
| Items of a combo or multicombo | 1 to 16 |
| One item | 32 bytes |
| Slider bounds and values | -1,000,000,000 to 1,000,000,000 |
| Queued presses of one button | 8 |
Interactive controls are the checkbox, slider, combo, multicombo, colour, keybind, button, input, list and fold. Display rows (label, separator, text, info and fold end) are counted as labels.
Menu layout
| limit | value |
|---|---|
| Order of a group | -100 to 100, rounded and kept within |
| Tooltip | 64 bytes |
| Text line, value of an info row, text of an input | 64 bytes |
Length limit of ui.input | 1 to 64, default 32 |
Visible rows of ui.list | 3 to 10, default 6 |
| Folds nested | 4 |
| Width of a control | 0.2 to 1 of the group, 0 for the rest of the row |
on_change calls per script and frame | 16, within 1.5 ms |
Tabs, style and surfaces
| limit | value |
|---|---|
| Tabs per script | 2 |
| Tabs in all running scripts together | 3 |
| Name of a tab | 12 bytes |
| Surfaces per script | 2 |
| Name of a surface, title of a floating surface | 32 bytes |
| Size of a surface | 32 to 900 wide, 32 to 700 high |
| Commands in the buffer of a surface | 1000 |
| Text in the buffer of a surface | 8 KiB |
| Point values in the buffer of a surface | 1024 |
| Wheel steps seen by a surface | -3 to 3 |
ui.style rounding | 0 to 6 |
ui.style row_gap | 2 to 8 |
ui.style header_spacing | 0 to 3 |
Game data
| limit | value |
|---|---|
Players in game.players() and entity.players() | 64 |
| Player name | 32 bytes |
| Velocity components | -32768 to 32768 |
| Property reads from the engine per script and frame | 512 |
Value of cvar.get | -1e12 to 1e12 |
Coordinates of bullet_impact | -1e6 to 1e6, otherwise the event is dropped |
Coordinates passed to game.to_screen | each within -1e6 and 1e6, otherwise it returns nil |
Events and shots
| limit | value |
|---|---|
| Event ring between the game and the client | 256 entries, the newest are dropped when it is full |
| Event queue the scripts read | 128 entries |
| Event calls per script and frame | 32, within 1.5 ms |
| Age at which an event is dropped | 5 seconds |
| Shot results kept | 64 |
| Shot calls per script and frame | 16, within 1.5 ms |
| Age at which a shot result is dropped | 5 seconds |
Resolver
| limit | value |
|---|---|
Accepted resolver.set_yaw calls per script and frame | 256 |
| Range of a hint | -60 to 60 degrees, clamped |
| Resolution of a hint | 0.01 degree |
| Life of a hint after the last call | 2 seconds |
Rage, settings, movement and visuals
| limit | value |
|---|---|
rage calls per script and frame | 128, accepted or not |
Life of a rage value after the last call | 500 ms |
| Hit chance asked for | 1 to 100, rounded and clamped |
| Minimum damage asked for | 1 to 110, rounded and clamped |
| Double precision chain cap | 1 to 3 |
| Settings held by one script | 64 |
Accepted settings.override calls per script and frame | 256 |
| Life of a settings override after the last call | 500 ms |
Ids returned by one settings.list call | 256 |
Accepted movement.intent calls per script and frame | 4 |
| Entries of an intent table | 16 |
ttl of an intent | 1 to 6 ticks, which is 16 to 96 ms |
forward and side | -1 to 1, clamped |
visuals calls per script and frame | 1024 |
| Players a script styles in one frame | 64 |
| Text lines and bars per player | 4 lines of 24 bytes, 2 bars |
Life of a visuals value | the frame it was set in and the next one |
Log
| limit | value |
|---|---|
| One line | 256 bytes |
| Rate | 20 lines per second, bursts of 20 |
| Lines kept by the page | 64, the last 14 are shown |
API costs
Every function we provide charges instructions to the running call, in addition to the instructions of your own code. This is why a loop that calls the API a lot ends earlier than a loop of the same length that does not. The costs are fixed.
| function | instructions |
|---|---|
time.*, screen.size, game.in_game | 10 |
log.info | 50 |
events.on_* (registering) | 20 |
game.local_player | 100 |
game.players | 100 + 180 for each player |
game.to_screen | 20 |
entity.get, entity.local_player | 30 |
entity.players | 100 + 60 for each player |
| Entity handle methods | 20 |
ent:eye_position | 60 |
ent:get | 40, plus 60 when the value is read from the engine |
cvar.get | 30 |
Every draw function except polyline and polygon | 20 |
draw.polyline, draw.polygon | 20, plus 1 for every number in the points table |
| Creating a control or a group | 50 (label 40, multicombo 60) |
get, set, has of a control | 10 (get of a multicombo: 10 plus 5 for every item) |
resolver.set_yaw, resolver.clear | 20 |
resolver.get_yaw | 10 |
rage.override_double_precision, rage.override_auto_stop | 30 |
The other rage functions | 20 |
movement.intent | 40 |
movement.release | 10 |
settings.get, settings.base, settings.info | 15 |
settings.list | 40, plus 2 for each id returned |
settings.override | 30 |
settings.clear | 20 |
settings.clear_all | 20, plus 2 for each setting given back |
visuals.player | 30 |
v:hide, v:color, v:glow | 25 |
v:bar | 30 |
v:text, v:chams | 40 |
v:reset | 20 |
draw.line3d, draw.polyline3d, draw.box3d, draw.circle3d, draw.hitbox | 30, plus 3 for each segment of the shape |
A full game.players() call costs 100 + 180 * 64 = 11,620 instructions, which is a little more than a tenth of the budget of on_paint. Call it once for each frame and keep the table.
File names
The page lists a file only if its name passes all of these rules. Anything else in the folder is ignored.
- It is a regular file, not a directory, a link, a junction or a device.
- It ends in
.lua. The ending may be in any case. - It is 5 to 64 characters long including the ending.
- It does not start with a dot.
- It has none of the characters
< > : " / \ | ? *and no control characters. - The part before
.luadoes not end in a dot or a space. - It is not a reserved device name:
con,prn,aux,nul,com0tocom9,lpt0tolpt9. Adding another extension does not help (con.x.luais refused).
The name of the script is the file name without .lua. It is the title of the script's group box, the prefix in the log, and the first part of every error message (name:12: message).
When we load a file we open it without following links. It must be a disk file with a single hard link, its real path must be directly inside the lua folder, and its size must be 256 KiB or less. We read it once, with one byte more than its size, so a file that grows while we read it is caught.
Saved values
Controls keep the value the user gave them. The client stores the values of one script in a small file:
Documents\Starlit\lua\cfg\<file name>.cfg
For hello.lua that is cfg\hello.lua.cfg. The folder cfg is created when the first value is written.
When it is written. After a value changed, at most once a second, and again when the script stops. A failed write is tried again after a second.
When it is read. Before the script starts. Saved values are applied as each control is created, so get() in the main chunk already returns the saved value and not the default you passed.
What is saved:
| control | saved |
|---|---|
| checkbox | the state |
| slider | the value |
| combo | the selected item |
| multicombo | the selected items |
| colour | the four channels |
| keybind | the key the user bound. The active state is not saved. |
| fold | whether the user left it open |
| list | the chosen item |
| label, button, separator, text, info, input | nothing |
How a saved value finds its control. A control is matched by its kind, the text of its label and its position among the controls with the same kind and label. If you rename a label, or change a checkbox into a slider, the control starts from its default. Two controls with the same kind and label are matched in the order you create them.
What the file contains. A header of 8 bytes (SLC1 and a count) and 16 bytes for each saved control: its kind, its position among equals, a hash of its label and 8 bytes of value. One more record keeps the user's show in pages switch for the script. At most 1048 bytes. Text from the script is never written into it, and files written by earlier builds still load.
What the client does with a bad file. It checks the header, the size and every record, and clamps every value to the bounds of its control. A file that does not pass is ignored and the script starts from its defaults. The file is written to a temporary name and renamed, so a half-written file is never read.
The client manages this file for you. To reset a script to its defaults, unload it and delete its .cfg file.
Error messages
Most mistakes raise a Lua error with a fixed text. The text never contains data from the script that raised it. The messages start with the script name and the line, for example hello:7: bad argument #4 to 'text' (number expected).
| message | cause |
|---|---|
bad argument #N to 'f' (number expected) | A value of the wrong type. The same form is used with string expected, function expected, boolean expected and table of points expected. |
bad argument #N to 'f' (finite number expected) | NaN or an infinite number where a finite one is needed. |
unknown font (ui, bold, small, medium, header, large) | The font name is not one of the six. |
unknown text style (normal, shadow, outline) | The style is not one of the three. |
unknown mode (hold, toggle, always) | The keybind mode is not one of the three. |
draw is only available inside on_paint | A draw function was called from another callback or from the main chunk. |
invalid handle | A method was called on a value that is not a handle of this script, or on a group object in place of a control and the reverse. |
handle is read-only | A field of a handle was assigned. |
table is read-only | A module table was assigned to. |
too many controls (64), too many labels (32), too many groups (12) | A cap on controls was reached. |
too many tabs (2), too many tabs in all scripts (3), too many surfaces (2) | A cap on tabs or surfaces was reached. |
that tab name is taken | The name belongs to one of Starlit's tabs or to a tab of another running script. |
give one of tab, page and floating | A surface was given more than one place. |
unknown page (scripts, rage, legit, visuals, misc), unknown column (auto, left, right) | A group or surface option has a value that does not exist. |
no open fold in this group, folds nest 4 deep at most | fold_end without a fold, or a fold nested too deep. |
this control has no change event, this control has no name, this control has no value | A handle method was called on a kind of control that does not have it. |
unknown field in the movement intent, invalid value in the movement intent, conflicting fields in the movement intent | The table passed to movement.intent is wrong. |
max must be greater than min | ui.slider got bounds in the wrong order. |
1 to 16 items expected, items must be strings, item index expected | A combo or multicombo got a bad item list or default. |
a button has no value | set was called on a button. |
not a multi-select control | has was called on something other than a multicombo. |
script budget exceeded | A time or instruction budget ran out. |
script memory limit reached | An allocation was refused. |
Messages that appear on a script's entry when it cannot start:
| message | cause |
|---|---|
file name is not allowed | The name does not pass the file name rules. |
file could not be opened, file could not be read | The file is locked, gone or unreadable. |
not a regular file inside the lua folder | The path is a link, a directory, a device, has several hard links or leads outside the folder. |
script is larger than 256 KiB | The file is too big. |
script contains a NUL byte, script is not valid UTF-8 | The text is not accepted. |
precompiled bytecode is not accepted | The file starts with byte 0x1B. |
too many scripts are loaded (16) | Sixteen scripts are already running. |
script state could not be prepared, script state could not be created, could not create the script heap, out of memory | The client could not allocate what the script needs. |