Lua Quick Reference
Variables, tables, strings, functions, metatables, OOP, coroutines and common FiveM/Roblox patterns.
Variables & Types
-- Variables are global by default; use local for scoping
local name = "Alex"
local age = 30
local active = true
local nothing = nil -- nil is the absence of value
-- Multiple assignment
local x, y, z = 1, 2, 3
local a, b = 10, 20
a, b = b, a -- swap values
-- Lua has 8 basic types
type("hello") -- "string"
type(42) -- "number"
type(3.14) -- "number" (integers and floats share the type in Lua 5.3+)
type(true) -- "boolean"
type(nil) -- "nil"
type({}) -- "table"
type(print) -- "function"
type(io.stdin) -- "userdata"
-- Number operations
local sum = 10 + 3 -- 13
local diff = 10 - 3 -- 7
local product = 10 * 3 -- 30
local quotient = 10 / 3 -- 3.3333...
local int_div = 10 // 3 -- 3 (floor division, Lua 5.3+)
local modulo = 10 % 3 -- 1
local power = 2 ^ 10 -- 1024
-- String to number conversion
tonumber("42") -- 42
tonumber("3.14") -- 3.14
tonumber("0xff") -- 255
tonumber("abc") -- nil
-- Number to string
tostring(42) -- "42"
tostring(true) -- "true"
-- Boolean logic — only nil and false are falsy; 0 and "" are truthy
local truthy = 0 and "yes" -- "yes" (0 is truthy in Lua!)
local falsy = nil and "yes" -- nil
local fallback = nil or "default" -- "default"
| Type | Description | Falsy? |
|---|---|---|
nil | Absence of value | Yes |
boolean | true or false | Only false |
number | Double-precision float (+ integers in 5.3+) | No (even 0) |
string | Immutable byte sequence | No (even "") |
table | Associative array (only compound type) | No |
function | First-class closures | No |
userdata | C data exposed to Lua | No |
thread | Coroutine handle | No |
Unlike JavaScript and Python, 0 and "" (empty string) are truthy in Lua. Only
nil and false evaluate as false.Tables
-- Tables are Lua's only compound data structure
-- They serve as arrays, dictionaries, objects, and modules
-- Array-style (1-indexed!)
local fruits = {"apple", "banana", "cherry"}
print(fruits[1]) -- "apple" (NOT fruits[0])
print(#fruits) -- 3 (length operator)
-- Dictionary-style
local user = {
name = "Alex",
email = "[email protected]",
age = 30
}
print(user.name) -- "Alex"
print(user["email"]) -- "[email protected]"
-- Mixed keys
local config = {
"first", -- [1] = "first"
"second", -- [2] = "second"
host = "localhost",
port = 3000,
[42] = "answer", -- numeric key
["special-key"] = true -- key with special characters
}
-- Modifying tables
user.role = "admin" -- add/update field
user.age = nil -- remove field (set to nil)
-- Array operations
table.insert(fruits, "date") -- append to end
table.insert(fruits, 2, "avocado") -- insert at position 2
table.remove(fruits, 1) -- remove at position 1
table.sort(fruits) -- in-place alphabetical sort
table.sort(fruits, function(a, b) return a > b end) -- custom sort
-- Iteration
-- ipairs: sequential integer keys (1, 2, 3, ...)
for i, v in ipairs(fruits) do
print(i, v)
end
-- pairs: all keys (unordered)
for k, v in pairs(user) do
print(k, v)
end
-- Table length (#) only counts sequential integer keys from 1
local t = {10, 20, nil, 40}
print(#t) -- may be 2 or 4 (undefined for tables with holes)
-- Shallow copy
local function shallowCopy(t)
local copy = {}
for k, v in pairs(t) do
copy[k] = v
end
return copy
end
-- Check if table is empty
local function isEmpty(t)
return next(t) == nil
end
-- Concatenate array-like table to string
local csv = table.concat({"a", "b", "c"}, ",") -- "a,b,c"
-- Unpack (table to multiple values)
local a, b, c = table.unpack({10, 20, 30})
Strings
-- String creation
local single = 'hello'
local double = "world"
local multiline = [[
This is a
multi-line string.
No escape processing.
]]
-- Concatenation
local full = "Hello" .. " " .. "World" -- "Hello World"
local greeting = "Count: " .. tostring(42) -- "Count: 42"
-- String length
print(#"hello") -- 5
print(string.len("hello")) -- 5
-- Common string functions
string.upper("hello") -- "HELLO"
string.lower("HELLO") -- "hello"
string.rep("ab", 3) -- "ababab"
string.reverse("hello") -- "olleh"
string.sub("hello world", 1, 5) -- "hello"
string.sub("hello world", 7) -- "world"
string.sub("hello", -3) -- "llo"
-- String search
string.find("hello world", "world") -- 7, 11 (start, end positions)
string.find("hello world", "xyz") -- nil
-- Pattern matching (Lua patterns, not full regex)
string.match("age: 30", "%d+") -- "30"
string.match("2026-03-07", "(%d+)-(%d+)-(%d+)") -- "2026", "03", "07"
-- Global match (iterator)
for word in string.gmatch("hello world lua", "%w+") do
print(word) -- "hello", "world", "lua"
end
-- Replace
string.gsub("hello world", "world", "Lua") -- "hello Lua", 1
string.gsub("aaa", "a", "b", 2) -- "bba", 2 (limit replacements)
-- Format (printf-style)
string.format("Name: %s, Age: %d", "Alex", 30)
string.format("Pi: %.2f", 3.14159) -- "Pi: 3.14"
string.format("%04d", 42) -- "0042"
string.format("Hex: 0x%x", 255) -- "Hex: 0xff"
-- Byte / char conversion
string.byte("A") -- 65
string.char(65) -- "A"
string.byte("hello", 1, 3) -- 104, 101, 108
| Pattern | Matches |
|---|---|
%d | Digit (0–9) |
%a | Letter (a–z, A–Z) |
%w | Alphanumeric (digit or letter) |
%s | Whitespace |
%p | Punctuation |
%u / %l | Uppercase / lowercase letter |
. | Any character |
+ | One or more |
* | Zero or more |
- | Zero or more (lazy) |
? | Zero or one |
Functions
-- Basic function
local function greet(name)
return "Hello, " .. name .. "!"
end
-- Alternative syntax (anonymous function assigned to variable)
local add = function(a, b)
return a + b
end
-- Multiple return values
local function minmax(t)
local min, max = t[1], t[1]
for _, v in ipairs(t) do
if v < min then min = v end
if v > max then max = v end
end
return min, max
end
local lo, hi = minmax({3, 1, 4, 1, 5, 9})
-- lo = 1, hi = 9
-- Variadic functions
local function sum(...)
local args = {...}
local total = 0
for _, v in ipairs(args) do
total = total + v
end
return total
end
print(sum(1, 2, 3, 4)) -- 10
print(select("#", ...)) -- number of arguments (including nil)
-- Default arguments (idiomatic pattern)
local function createUser(name, role)
role = role or "user"
return { name = name, role = role }
end
-- Functions as first-class values
local function apply(fn, a, b)
return fn(a, b)
end
print(apply(add, 10, 20)) -- 30
-- Closures
local function counter(start)
local count = start or 0
return function()
count = count + 1
return count
end
end
local next = counter(0)
print(next()) -- 1
print(next()) -- 2
print(next()) -- 3
-- Method syntax (colon calls pass self implicitly)
local obj = { name = "Alex" }
function obj:greet()
return "Hi, I'm " .. self.name
end
-- obj:greet() is equivalent to obj.greet(obj)
-- Tail call optimization — Lua optimizes recursive tail calls
local function factorial(n, acc)
acc = acc or 1
if n <= 1 then return acc end
return factorial(n - 1, n * acc) -- tail call
end
Control Flow
-- if / elseif / else
local score = 85
if score >= 90 then
print("A")
elseif score >= 80 then
print("B")
elseif score >= 70 then
print("C")
else
print("F")
end
-- Ternary-style (idiomatic Lua)
local status = (age >= 18) and "adult" or "minor"
-- while loop
local i = 1
while i <= 5 do
print(i)
i = i + 1
end
-- repeat...until (like do...while — body runs at least once)
local input
repeat
input = getInput()
until input ~= nil
-- Numeric for (start, stop, step)
for i = 1, 10 do -- 1 to 10 inclusive
print(i)
end
for i = 10, 1, -1 do -- 10 down to 1
print(i)
end
for i = 0, 1, 0.2 do -- float step
print(i)
end
-- Generic for (iterators)
for i, v in ipairs(myArray) do
print(i, v)
end
for k, v in pairs(myTable) do
print(k, v)
end
-- break — exit a loop early
for i = 1, 100 do
if i > 10 then break end
print(i)
end
-- goto (Lua 5.2+) — jump to a label (use sparingly)
for i = 1, 10 do
if i == 3 then goto continue end
print(i)
::continue::
end
-- No switch/case — use if/elseif or table dispatch
local actions = {
start = function() print("Starting...") end,
stop = function() print("Stopping...") end,
reset = function() print("Resetting...") end,
}
local cmd = "start"
local action = actions[cmd]
if action then action() end
Modules
-- Module pattern — return a table of public functions
-- utils.lua
local M = {}
function M.add(a, b)
return a + b
end
function M.multiply(a, b)
return a * b
end
local function privateHelper()
-- not exposed (local function)
end
return M
-- Importing a module
local utils = require("utils")
print(utils.add(2, 3)) -- 5
-- require caches modules — subsequent calls return the cached table
-- To force reload (development only):
package.loaded["utils"] = nil
local utils = require("utils")
-- Module search path
-- package.path — Lua file search path (semicolon-separated patterns)
-- Default: "./?.lua;./?/init.lua;/usr/local/share/lua/5.4/?.lua"
-- package.cpath — C library search path
-- OOP-style module
-- player.lua
local Player = {}
Player.__index = Player
function Player.new(name, health)
local self = setmetatable({}, Player)
self.name = name
self.health = health or 100
return self
end
function Player:takeDamage(amount)
self.health = math.max(0, self.health - amount)
end
function Player:isAlive()
return self.health > 0
end
return Player
-- Usage
local Player = require("player")
local p1 = Player.new("Alex", 100)
p1:takeDamage(30)
print(p1.health) -- 70
Metatables & OOP
-- Metatables control table behavior through metamethods
-- __index — called when accessing a missing key
local defaults = { color = "blue", size = 10 }
local obj = setmetatable({}, { __index = defaults })
print(obj.color) -- "blue" (falls back to defaults)
print(obj.size) -- 10
-- __newindex — called when setting a missing key
local readonly = setmetatable({}, {
__newindex = function(t, k, v)
error("attempt to modify a read-only table")
end
})
-- __tostring — custom string representation
local vec = setmetatable({ x = 3, y = 4 }, {
__tostring = function(self)
return string.format("(%g, %g)", self.x, self.y)
end
})
print(tostring(vec)) -- "(3, 4)"
-- Operator overloading
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({ x = x, y = y }, Vector)
end
function Vector.__add(a, b)
return Vector.new(a.x + b.x, a.y + b.y)
end
function Vector.__mul(a, scalar)
if type(a) == "number" then a, scalar = scalar, a end
return Vector.new(a.x * scalar, a.y * scalar)
end
function Vector.__eq(a, b)
return a.x == b.x and a.y == b.y
end
function Vector:length()
return math.sqrt(self.x ^ 2 + self.y ^ 2)
end
function Vector:__tostring()
return string.format("Vec(%g, %g)", self.x, self.y)
end
local v1 = Vector.new(3, 4)
local v2 = Vector.new(1, 2)
local v3 = v1 + v2 -- Vec(4, 6)
print(v1:length()) -- 5
-- Inheritance via metatables
local Entity = {}
Entity.__index = Entity
function Entity.new(name, x, y)
return setmetatable({ name = name, x = x, y = y }, Entity)
end
function Entity:move(dx, dy)
self.x = self.x + dx
self.y = self.y + dy
end
function Entity:getPosition()
return self.x, self.y
end
-- NPC inherits from Entity
local NPC = setmetatable({}, { __index = Entity })
NPC.__index = NPC
function NPC.new(name, x, y, dialog)
local self = Entity.new(name, x, y)
setmetatable(self, NPC)
self.dialog = dialog
return self
end
function NPC:speak()
return self.name .. ": " .. self.dialog
end
local guard = NPC.new("Guard", 10, 20, "Halt! Who goes there?")
guard:move(5, 0) -- inherited from Entity
print(guard:speak()) -- "Guard: Halt! Who goes there?"
| Metamethod | Triggered by |
|---|---|
__index | Accessing missing key (t.key) |
__newindex | Setting a missing key (t.key = v) |
__call | Calling table as function (t()) |
__tostring | tostring(t) |
__len | #t |
__add / __sub | + / - |
__mul / __div | * / / |
__eq | == |
__lt / __le | < / <= |
__concat | .. |
__gc | Garbage collection (Lua 5.2+) |
File I/O
-- Read entire file
local f = io.open("data.txt", "r")
if f then
local content = f:read("*a") -- read all
f:close()
print(content)
end
-- Read line by line
local f = io.open("data.txt", "r")
if f then
for line in f:lines() do
print(line)
end
f:close()
end
-- Shorthand: iterate lines of a file
for line in io.lines("data.txt") do
print(line)
end
-- Write to file
local f = io.open("output.txt", "w")
if f then
f:write("Hello, World!\n")
f:write("Second line\n")
f:close()
end
-- Append to file
local f = io.open("log.txt", "a")
if f then
f:write(os.date("%Y-%m-%d %H:%M:%S") .. " — Event occurred\n")
f:close()
end
-- Read modes
f:read("*a") -- read entire file
f:read("*l") -- read one line (default)
f:read("*n") -- read a number
f:read(10) -- read 10 bytes
-- Binary file
local f = io.open("data.bin", "rb")
if f then
local bytes = f:read("*a")
f:close()
end
-- File existence check
local function fileExists(path)
local f = io.open(path, "r")
if f then
f:close()
return true
end
return false
end
-- Safe file read with error handling
local function readFile(path)
local f, err = io.open(path, "r")
if not f then
return nil, err
end
local content = f:read("*a")
f:close()
return content
end
local data, err = readFile("config.txt")
if not data then
print("Error: " .. err)
end
Coroutines
-- Coroutines enable cooperative multitasking
-- They can pause (yield) and resume execution
-- Basic coroutine
local function producer()
local i = 0
while true do
i = i + 1
coroutine.yield(i)
end
end
local co = coroutine.create(producer)
print(coroutine.resume(co)) -- true, 1
print(coroutine.resume(co)) -- true, 2
print(coroutine.resume(co)) -- true, 3
-- Coroutine status
coroutine.status(co) -- "suspended", "running", "dead", "normal"
-- wrap — returns a function that resumes the coroutine
local gen = coroutine.wrap(producer)
print(gen()) -- 1
print(gen()) -- 2
print(gen()) -- 3
-- Passing values via yield/resume
local function accumulator()
local total = 0
while true do
local value = coroutine.yield(total)
total = total + value
end
end
local co = coroutine.create(accumulator)
coroutine.resume(co) -- initialize
print(coroutine.resume(co, 10)) -- true, 10
print(coroutine.resume(co, 20)) -- true, 30
print(coroutine.resume(co, 5)) -- true, 35
-- Iterator using coroutine
local function permutations(arr)
local function permute(a, n)
if n == 0 then
coroutine.yield(a)
else
for i = 1, n do
a[i], a[n] = a[n], a[i]
permute(a, n - 1)
a[i], a[n] = a[n], a[i]
end
end
end
return coroutine.wrap(function()
permute(arr, #arr)
end)
end
for perm in permutations({1, 2, 3}) do
print(table.concat(perm, ", "))
end
-- Timer/scheduler pattern (common in game development)
local tasks = {}
local function delay(seconds)
local start = os.clock()
while os.clock() - start < seconds do
coroutine.yield()
end
end
local function runTask(fn)
local co = coroutine.create(fn)
table.insert(tasks, co)
end
local function updateTasks()
for i = #tasks, 1, -1 do
local co = tasks[i]
if coroutine.status(co) == "dead" then
table.remove(tasks, i)
else
coroutine.resume(co)
end
end
end
Common Patterns (FiveM / Roblox Context)
FiveM (CitizenFX)
-- FiveM uses Lua with the CitizenFX runtime
-- Citizen.CreateThread wraps coroutines with automatic resumption
-- Game tick loop
Citizen.CreateThread(function()
while true do
Citizen.Wait(0) -- every frame (~16ms at 60 FPS)
local ped = PlayerPedId()
local coords = GetEntityCoords(ped)
-- per-frame logic here
end
end)
-- Periodic task (less CPU-intensive)
Citizen.CreateThread(function()
while true do
Citizen.Wait(5000) -- every 5 seconds
checkPlayerStatus()
end
end)
-- Server events
RegisterNetEvent('myResource:playerLoaded')
AddEventHandler('myResource:playerLoaded', function(playerData)
local source = source
print(("Player %s loaded (ID: %d)"):format(playerData.name, source))
end)
-- Trigger events
TriggerServerEvent('myResource:requestData', playerId)
TriggerClientEvent('myResource:updateUI', targetPlayer, data)
-- NUI callbacks (client-side UI communication)
RegisterNUICallback('closeMenu', function(data, cb)
SetNuiFocus(false, false)
cb({ ok = true })
end)
-- Server callback pattern (common community pattern)
ESX = exports['es_extended']:getSharedObject()
ESX.RegisterServerCallback('myResource:getData', function(source, cb)
local xPlayer = ESX.GetPlayerFromId(source)
local result = MySQL.query.await('SELECT * FROM users WHERE id = ?', { xPlayer.identifier })
cb(result)
end)
-- Command registration
RegisterCommand('heal', function(source, args, raw)
local ped = PlayerPedId()
SetEntityHealth(ped, GetEntityMaxHealth(ped))
end, false)
Roblox (Luau)
-- Roblox uses Luau (typed Lua fork)
-- Services are accessed via game:GetService
local Players = game:GetService("Players")
local RS = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
-- Player joined event
Players.PlayerAdded:Connect(function(player)
print(player.Name .. " joined the game")
player.CharacterAdded:Connect(function(character)
local humanoid = character:WaitForChild("Humanoid")
humanoid.MaxHealth = 200
humanoid.Health = 200
end)
end)
-- RemoteEvent communication (client ↔ server)
-- In ReplicatedStorage: RemoteEvent named "DamageEvent"
local damageEvent = RS:WaitForChild("DamageEvent")
-- Server script
damageEvent.OnServerEvent:Connect(function(player, targetId, damage)
local target = Players:GetPlayerByUserId(targetId)
if target and target.Character then
local humanoid = target.Character:FindFirstChild("Humanoid")
if humanoid then
humanoid:TakeDamage(damage)
end
end
end)
-- Client script
damageEvent:FireServer(targetUserId, 25)
-- Render loop (client only)
RunService.RenderStepped:Connect(function(deltaTime)
-- runs every frame before rendering
end)
-- Heartbeat (server + client, after physics)
RunService.Heartbeat:Connect(function(deltaTime)
-- runs every frame after physics simulation
end)
-- Luau type annotations
type Vector = { x: number, y: number, z: number }
local function distance(a: Vector, b: Vector): number
return math.sqrt((a.x-b.x)^2 + (a.y-b.y)^2 + (a.z-b.z)^2)
end
-- Module script pattern (Roblox modules)
local CombatModule = {}
function CombatModule.calculateDamage(baseDamage: number, armor: number): number
local reduction = math.clamp(armor / 100, 0, 0.75)
return math.floor(baseDamage * (1 - reduction))
end
return CombatModule
FiveM uses standard Lua 5.4 with CitizenFX extensions (
Citizen.CreateThread, Citizen.Wait, native game functions). Roblox uses Luau, a Lua 5.1 fork with type annotations, continue, generalized iteration, and performance optimizations.