Luau Language Notes
On this page
Violet scripts run on Luau, Roblox's fork of Lua. Luau is based on Lua 5.1 with selected features from later versions and its own extensions. If you've written Lua before — especially targeting 5.4 — there are a few differences to be aware of.
What you gain over Lua 5.1
continueinside loops- Compound assignments —
x += 1,s ..= "foo", etc. - String interpolation —
`Hello, ${name}!` - Generalized iteration —
for k, v in t doworks on tables without callingpairs(respects__iter) - Optional type annotations —
local x: number = 1(ignored at runtime, useful if you run the Luau analyzer) buffertype — native mutable byte arrays, seebufferlibrary.in_packet:decode_bufferreturns one.- Sandbox-safe by default — scripts can't escape into
os.execute,io.open,debug.getfenv, etc. (None of those are exposed.)
What's missing vs Lua 5.2+/5.4
Scripts ported from newer Lua will need adjustments for:
| Lua 5.2+ feature | Luau equivalent |
|---|---|
Native bitwise operators |, &, ~, <<, >> | Use the bit32 library |
string.pack / string.unpack | Encode manually, or use buffer + in_packet / out_packet helpers |
utf8 library | Not available |
<const> / <close> variable attributes | Not supported; plain local only |
Integer subtype (math.type, //) | Luau numbers are doubles (// works as integer division) |
goto / ::label:: | Luau supports continue instead for the common use case |
Globals and hook chaining
All loaded plugins share a single global table. Top-level assignments in one script are visible to others.
Hooks are chained, not replaced. Every plugin's on_tick (and every other lifecycle hook) is stored independently when the plugin loads, so all of them fire in load order every tick. Two plugins defining on_tick means both run — not that the second overwrites the first.
Block-style hooks follow first-wins semantics:
on_packet_recv/on_packet_send— first plugin to returnfalsedrops the packet; later plugins don't see iton_dialog— first plugin to return a non-nil, non-truevalue intercepts the dialog; later plugins don't see it
For private helpers, use local at file scope — they stay scoped to the file:
local function helper()
-- only this file can see `helper`
end
function on_tick() -- still a global, becomes the script's hook
helper()
endOr put shared utilities under scripts/libs/ and pull them in with require("my_lib") — module returns are cached, one copy per require path.
Error handling
Script errors propagate through pcall / xpcall normally. Uncaught errors from a hook are logged as [LUA ERROR] in <hook>: <message> and the hook is skipped for that tick — other plugins continue running.
When calling C-side bindings, failures surface as Lua errors (catchable with pcall). Calling a binding with the wrong argument type or on an invalid object raises an error; guarding with is_valid() on objects you've held across ticks is a good habit.
Performance
- Luau's interpreter is ~2–3× faster than vanilla Lua 5.1 on typical hot loops.
- Native codegen (enabled by default) compiles loaded chunks to machine code for x64 — no action required in your scripts.
bufferoperations avoid the overhead of Lua table allocations for byte-level work; preferbufferover tables when processing packet payloads.
64-bit packet integers
in_packet:decode_8 returns a Luau integer (a distinct int64 type, not a regular number). This preserves the full 64-bit pattern of fields like object IDs, timestamps, and hashes — a regular number (double) would silently round bits 54-64.
The integer type does not implicitly coerce against number for comparison or arithmetic:
local id = packet:decode_8()
if id > 0 then end -- error: attempt to compare integer with number
if id == 0 then end -- always false (different types compare unequal)Work with int64 values explicitly via the integer library and integer.new(...) for literals:
local id = packet:decode_8()
if id > integer.new(0) then end -- ok
if id == integer.new(0) then end -- ok
-- Drop to a regular number if you don't need the high bits:
local n = tonumber(id) -- precision lost past 2^53All other numeric bindings (player:get_exp(), player:get_meso(), decode_1/2/4, etc.) return regular number values and can be mixed freely with plain numeric literals.