RedM resources run on Lua 5.4 when the manifest has lua54 'yes'. This page covers the language as you use it in resources, plus what Cfx.re adds. The complete language manual is at lua.org/manual/5.4.
Values and types#
Lua has eight types: nil, boolean, number, string, table, function, thread (coroutine) and userdata.
print(type(nil), type(true), type(10), type(1.5), type('x'), type({}), type(print))
-- nil boolean number number string table function- Only
nilandfalseare false.0and''are true. - Numbers are either integers or floats.
10 // 3is integer division (3),10 / 2is always a float (5.0),math.type(5)returns'integer'. - Strings are immutable byte sequences. Use
..to join and#sfor the length in bytes.
Variables and scope#
local count = 0 -- local to the block: always prefer this
globalVar = 1 -- global: shared by every file of the resource, avoid
local a, b = 1, 2 -- multiple assignment
a, b = b, a -- swap
local max <const> = 100 -- constant (5.4): assignment is an errorA local is visible until the end of the block (do ... end, function, loop body). Locals are faster than globals and cannot be overwritten by another script.
Operators#
| Kind | Operators |
|---|---|
| Arithmetic | + - * / // % ^ (^ is power, always float) |
| Comparison | == ~= < <= > >= (not equal is ~=, not !=) |
| Logic | and or not (a and b or c is the usual ternary) |
| String | .. concatenation, # length |
| Bitwise | & \| ~ << >> ~ (~ is xor when binary, not when unary) |
local label = player and player.name or 'unknown'
local half = total // 2Control flow#
if hp <= 0 then
die()
elseif hp < 30 then
limp()
else
walk()
end
for i = 1, 10 do end -- 1..10
for i = 10, 1, -1 do end -- counting down
for i, v in ipairs(list) do end -- array part, in order, stops at the first nil
for k, v in pairs(map) do end -- every key, order not guaranteed
while cond do end
repeat until condbreak leaves a loop. goto continue with a label ::continue:: at the end of the loop body is the idiom for "next iteration".
Functions#
local function add(a, b) return a + b end
local function stats(list)
return #list, list[1] -- several return values
end
local n, first = stats({4, 5, 6})
local function log(fmt, ...) -- variable arguments
print(string.format(fmt, ...))
end
local fn = function(x) return x * 2 end -- anonymous function / closureFunctions are values and closures keep the locals they use. select('#', ...) counts the arguments, table.pack(...) collects them, table.unpack(t) spreads a table.
Tables#
The table is the only data structure: it is the array, the map, the object and the set.
local list = { 'a', 'b', 'c' } -- list[1] == 'a' (Lua starts at 1)
local map = { name = 'Arthur', cash = 12.5 }
local set = { police = true, doctor = true } -- fast membership test: set[job]
list[#list + 1] = 'd' -- append
table.insert(list, 'e')
table.insert(list, 1, 'first') -- insert at position
table.remove(list, 1) -- remove at position
print(#list) -- length of the array part| Function | Use |
|---|---|
table.insert(t, [pos,] v) / table.remove(t [, pos]) |
Add or remove an element. |
table.concat(t, sep) |
Join strings, much faster than .. in a loop. |
table.sort(t [, cmp]) |
In place sort. The comparison must be a strict "less than". |
table.unpack(t, i, j) / table.pack(...) |
Spread or collect values. |
table.move(a, f, e, t [, a2]) |
Copy a range. |
next(t) |
nil when the table is empty, cheaper than counting. |
Gotchas: #t is only reliable on arrays without holes. Assigning nil removes a key. Tables are passed by reference: copy with a loop when you need an independent copy.
local function deepCopy(v)
if type(v) ~= 'table' then return v end
local out = {}
for k, x in pairs(v) do out[k] = deepCopy(x) end
return out
endStrings#
local s = 'Hello'
print(s:upper(), s:lower(), s:len(), s:sub(2, 4), s:rep(2, '-'))
print(('%s has $%.2f'):format('Arthur', 12.5))
print(('a,b,c'):find(',', 1, true)) -- plain search: start, end
for word in ('one two'):gmatch('%a+') do print(word) end
print(('x=1'):gsub('%d', '9')) -- replace, returns text and countstring.format specifiers: %s string, %d integer, %f / %.2f float, %x hex, %q quoted, %% a percent sign.
Patterns are not regular expressions:
| Class | Matches | Quantifier | Meaning | |
|---|---|---|---|---|
%a |
letter | * |
0 or more | |
%d |
digit | + |
1 or more | |
%s |
space | - |
0 or more, shortest | |
%w |
letter or digit | ? |
0 or 1 | |
%p |
punctuation | ^ / $ |
start / end of the string | |
. |
any character | ( ) |
capture |
Escape a magic character with %: %. is a literal dot.
Metatables#
A metatable changes how a table behaves. It is how classes are built.
local Vec = {}
Vec.__index = Vec
function Vec.new(x, y) return setmetatable({ x = x, y = y }, Vec) end
function Vec:len() return math.sqrt(self.x ^ 2 + self.y ^ 2) end
Vec.__add = function(a, b) return Vec.new(a.x + b.x, a.y + b.y) end
Vec.__tostring = function(v) return ('(%s, %s)'):format(v.x, v.y) end
local v = Vec.new(3, 4) + Vec.new(0, 0)
print(v:len(), tostring(v)) -- 5.0 (3, 4)Common metamethods: __index, __newindex, __call, __tostring, __len, __eq, __lt, __le, __concat, __close, and the arithmetic family (__add, __sub, __mul, __div, ...). obj:method(x) is shorthand for obj.method(obj, x).
Errors#
local ok, err = pcall(function() error('boom') end)
if not ok then print('failed:', err) end
local ok2, res = xpcall(risky, debug.traceback) -- keeps the stack trace
assert(type(x) == 'number', 'x must be a number')Wrap code that may fail because of outside input (JSON, database results) in pcall; do not hide your own bugs with it.
Math#
math.floor, math.ceil, math.abs, math.min, math.max, math.sqrt, math.sin, math.cos, math.pi, math.huge, math.random(m, n), math.randomseed(x), math.tointeger(x), math.fmod(a, b).
local function round(n, places)
local m = 10 ^ (places or 0)
return math.floor(n * m + 0.5) / m
endNever use math.random for anything that must be fair or secret (rewards that matter, tokens): do the roll on the server and check it.
Coroutines#
local co = coroutine.create(function(a)
local b = coroutine.yield(a + 1)
return b * 2
end)
print(coroutine.resume(co, 1)) -- true 2
print(coroutine.resume(co, 10)) -- true 20In RedM you rarely create coroutines yourself: CreateThread and Wait do it for you.
What Cfx.re adds#
| Feature | Example |
|---|---|
| Hash literal | `a_c_horse_arabian_white` gives the model hash |
| Vectors | vector3(1.0, 2.0, 3.0), vec3, vector2, vector4; + - *, #v length, .x .y .z |
json |
json.encode(t), json.decode(s) |
| Compound assignment | n += 1, s ..= 'x' (Cfx only) |
| Safe navigation | config?.blip?.sprite returns nil instead of an error |
| Threads | CreateThread(fn), Wait(ms), SetTimeout(ms, fn) |
| Promises | promise.new(), Citizen.Await(p) |
| Events | RegisterNetEvent, AddEventHandler, TriggerEvent, TriggerServerEvent, TriggerClientEvent |
| Exports | exports('Name', fn) and exports.resource:Name() |
| Msgpack | msgpack.pack, msgpack.unpack |
The Cfx extensions are not plain Lua: an editor or linter without Cfx support may flag them, and the code will not run outside the game.
Performance#
- Keep hot loops free of table creation and string concatenation.
- Cache
PlayerPedId(),GetEntityCoords(ped)and similar natives once per loop turn. - Use a table as a set for membership tests (
allowed[job]) instead of looping an array. - Build text with
table.concat, not..in a loop. - Sleep:
Wait(1000)while idle,Wait(0)only while something visible needs every frame. - Avoid
pairson huge tables in frame loops; keep a precomputed list.
Common mistakes#
| Mistake | Fix |
|---|---|
if x != 1 |
Use ~= |
list[0] |
Lua arrays start at 1 |
Using a global by accident (coords = ...) |
Write local coords |
string.sub with a 0 start |
Start at 1 |
Comparing a float to an integer with == for money |
Round to cents and compare integers |
| Waiting on a client value on the server | Ask the client with a callback and validate the answer |
Credits#
Lua is created by PUC-Rio and released under the MIT license. The authoritative text is the Lua 5.4 Reference Manual.