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"
TypeDescriptionFalsy?
nilAbsence of valueYes
booleantrue or falseOnly false
numberDouble-precision float (+ integers in 5.3+)No (even 0)
stringImmutable byte sequenceNo (even "")
tableAssociative array (only compound type)No
functionFirst-class closuresNo
userdataC data exposed to LuaNo
threadCoroutine handleNo
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
PatternMatches
%dDigit (0–9)
%aLetter (a–z, A–Z)
%wAlphanumeric (digit or letter)
%sWhitespace
%pPunctuation
%u / %lUppercase / 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?"
MetamethodTriggered by
__indexAccessing missing key (t.key)
__newindexSetting a missing key (t.key = v)
__callCalling table as function (t())
__tostringtostring(t)
__len#t
__add / __sub+ / -
__mul / __div* / /
__eq==
__lt / __le< / <=
__concat..
__gcGarbage 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.