If you've ever opened a config.lua and changed a price, a job name, or a spawn coordinate, you've already touched Lua — you just didn't know the rules. This lesson hands you those rules. By the end you'll look at a script and see structure instead of a wall of symbols. That single shift is what separates a config-editor from a developer.
You already write Lua (you just call it "config")
When you set Config.MaxPlayers = 48, that line is Lua. The framework loads your file and runs it like any other code. The only difference between you and a "real developer" is that they know what else those symbols can do. So we're not starting from zero — we're naming things you've already seen.
Build one habit now: keep the official Lua 5.4 reference manual (lua.org, MIT-licensed) open in a tab. We won't paste it here — we'll explain it in plain English and point you back to the source of truth. That tab is also your defense against bad AI output, which we'll get to at the end.
Variables and types
A variable is a name pointing at a value. You make one with =:
local speed = 60
Lua has a handful of core types you'll use constantly:
- number —
60,19.99. Lua 5.4 added true integers, but you rarely have to think about it. - string — text in quotes,
"police". Join strings with..(two dots). - boolean —
trueorfalse. The entire engine of decisions. - nil — "nothing here." A variable you never set is
nil. This is the #1 source of beginner errors, so learn to recognize the word.
You don't declare types like in some languages — a variable can hold a number now and a string later. That's freedom, and a footgun.
Tables: the one structure that runs everything
Lua has exactly one container type — the table — and it does two jobs.
As an array (a numbered list):
local jobs = { "police", "ems", "mechanic" }
Lua arrays start at 1, not 0. So jobs[1] is "police". Write that on your hand.
As a dictionary (named slots, "key → value"):
local player = { name = "Vera", cash = 250, online = true }
Here player.cash reads 250. Every Config you've ever edited is just a big nested table. That's the whole secret — once tables click, config files stop being magic.
Functions: name a job, reuse it
A function wraps a chunk of work behind a name so you can run it whenever you like, with different inputs. Read this top to bottom — it's the lesson in one screen:
-- A tiny price book for a roleplay shop.
-- This is a "dictionary" table: each key (item name) maps to a value (price).
local prices = {
water = 5,
sandwich = 12,
lockpick = 75,
}
-- An "array" table: a numbered list of what a player is buying.
-- Lua arrays start at index 1, not 0.
local cart = { "water", "water", "sandwich" }
-- A function turns a job into a name you can reuse.
-- It takes the cart and the price book, and returns one number.
local function totalCost(items, priceBook)
local sum = 0
-- ipairs walks an array in order: i is the index, name is the value.
for i, name in ipairs(items) do
local price = priceBook[name]
if price == nil then
-- A missing item shouldn't crash the till; warn and skip it.
print("No price set for: " .. name)
else
sum = sum + price
end
end
return sum
end
print("Total: $" .. totalCost(cart, prices)) -- Total: $22
Control flow: if, for, while
Code runs top to bottom until you tell it to branch or repeat.
- if / else — do something only when a condition is true.
- for — repeat a known number of times, or walk a table (
ipairsfor arrays,pairsfor dictionaries). - while — repeat until a condition flips false. Powerful and dangerous: forget to change the condition and it loops forever.
You just saw if and for in the example above. You've already read a real loop.
local vs global: the scope rule that saves you
Notice every variable so far began with local. That word limits where the name exists — usually to the file or block it lives in. Drop it, and the variable becomes global: visible to every script on the server.
Always write local unless you have a strong reason not to. Globals from two different scripts can quietly overwrite each other, and you'll lose an afternoon to a bug that lives in neither file. local is a one-word insurance policy.
This same idea — what can see what — becomes critical soon. In FiveM, code runs on the client (each player's PC) or the server. The client can be lied to. So "never trust the client" is really a scope rule: money, items, and jobs get validated server-side, and the client only asks. Modern scripts do that asking with lib.callback from ox_lib, not the old event hacks. You'll write one in a later module — for now, just know scope is a security idea, not only a tidiness one.
A note on building with AI
AI will write Lua for you, and it's a real accelerator — if you can read the output. AI confidently invents natives and exports that don't exist, blends QBCore, Qbox, and ESX into a stew that runs on none of them, and reaches for deprecated patterns it learned from old forum posts. Your defenses are simple: pin one framework (we use Qbox/QBCore + ox), verify every native and export against the real reference, and paste the code into the PlayDeck sandbox to see if it actually runs. Reading Lua is what makes you the editor instead of the victim.
Practice
Open the PlayDeck browser sandbox — no FiveM, no server, just Lua. Paste the shop example above, then:
- Run it. Confirm you get
Total: $22. - Add a lockpick to the cart: change the
cartline to{ "water", "water", "sandwich", "lockpick" }. Before you run it, predict the new total in your head. Then run it and check. - Break it on purpose. Type a fake item like
"banana"into the cart and run it. Read what prints — and notice the till didn't crash. That's thenilcheck earning its keep.
If your prediction matched the output, you just debugged in your head. That's the job.
Recap
- A config is code — you've been writing Lua all along.
- Variables name values; the types you'll use are number, string, boolean, and
nil. - Tables do everything: arrays (start at 1) and dictionaries (key → value).
- Functions name reusable work; if / for / while branch and repeat.
localkeeps variables contained — default to it, and remember scope is also security: validate server-side, ask withlib.callback.- AI is a tool, not an oracle — pin one framework, verify against the real reference, and prove it in the sandbox.