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). A few APIs return the separate integer type |
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 integers
A few values are identifiers that need all 64 bits, so they come back as a Luau integer: a separate 64-bit type, not a regular number. A number is exact only up to ±9,007,199,254,740,991 (2^53 − 1).
- Equip
sn,essence_snandcash_item_snfrom the inventory functions - Locker
cash_snfromcore.cash_shop.locker.get_items() packet:decode_8()- A whole number beyond ±2^53 read back from
core.settings.storage
Everything else returns a regular number, including meso, EXP, timestamps, decode_1/2/4 and core.get_setting.
These work on an integer:
==and~=against another integer, including literals written with anisuffix such as0i..andtostring- Arithmetic.
+,-,*,//and%give anintegerwhen the other side is an integer or a whole number, wrapping around past the 64-bit range, sosn + 1stays exact. With a fractional number, and for/and^, the result is anumber. <,>,<=and>=between two integers
These don't:
- Comparing with a plain
number:sn < 5raises an error, andsn == 0is alwaysfalse. Compare with0iinstead. math.*,string.repandforloop limits. Convert first withinteger.tonumber.tonumber(sn), which returnsnil. Useinteger.tonumber(sn), which is exact up to 2^53.- Table keys:
t[5]andt[5i]are different keys.
for _, item in ipairs(core.inventory.get_equip_items()) do
core.log("Serial: " .. item.sn) -- ok
if item.essence_sn == 0i then end -- ok: the equip has no essence
if item.essence_sn == 0 then end -- always false: an integer never equals a number
endThe integer library works on integers and also accepts whole numbers, so integer.gt(10, item.sn) and integer.tonumber(5) both work.