Naar de inhoud
Scope, files and layout

Scope, files and layout

Keep FiveM Lua local by default: the visibility ladder from local function to net event, splitting client and server files, naming rules and manifest load order.

Deze docs zijn voorlopig in het Engels.

Last updated

Scope is the cheapest quality decision you will make. Something that is local cannot be called by accident, cannot be overwritten by another file, and is read out of a register instead of a hash table. Something registered as a net event can be called by any player on your server.

The rule is short: give every value and every function the smallest reach that still works, and widen it only when you have a reason.

The visibility ladder

Each rung is easier to call than the one above it, and each rung is one more thing you have to keep correct.

Rung Who can call it Use it when
local function This file only Default. Most helpers never need more.
Resource global (function Foo()) Any file in this resource, same side Two files in your resource need it.
exports('Foo', fn) Any other resource, same side You are deliberately offering an API.
AddEventHandler Anything in the same side that triggers the event Several handlers should react to one thing.
RegisterNetEvent The other side of the network, meaning players Data genuinely has to cross client and server.

Only the last rung is a security decision, and it is the one people reach for first. If a client never needs to trigger it, do not register it as a net event. See Events that stay cheap and safe.

Local is also faster

In Lua 5.4 a local variable lives in a register on the call stack. A global is a key in the _ENV table, so every read is a hash lookup:

Lua
-- Every use of `Config` here is a table lookup in _ENV
for i = 1, #Config.Shops do
    doSomething(Config.Shops[i])
end

-- One lookup, then registers
local shops = Config.Shops
for i = 1, #shops do
    doSomething(shops[i])
end

The difference per access is tiny. In a loop that runs thousands of times it stops being tiny, and the second version reads better anyway.

Note

Resources are isolated from each other, but files inside one resource are not. A global set in client/main.lua is visible in client/shops.lua on the same side, and a global set on the server never reaches the client. local at the top of a file is visible to that file only.

Split client and server

Put client code and server code in their own folders, and share only what genuinely belongs to both:

Text
my_resource/
├─ fxmanifest.lua
├─ shared/
│  └─ config.lua
├─ client/
│  ├─ main.lua
│  └─ shops.lua
└─ server/
   ├─ main.lua
   └─ money.lua
fxmanifest.lua
fx_version 'cerulean'
game 'gta5'

shared_scripts { 'shared/config.lua' }
client_scripts { 'client/main.lua', 'client/shops.lua' }
server_scripts { 'server/main.lua', 'server/money.lua' }

This stops the classic accident of a “shared” file that quietly ships server logic, prices or checks to every player’s PC. Anything in shared_scripts is downloaded by the client. Treat it as public.

Scripts load in the order the manifest lists them, so a file level local declared in client/main.lua does not exist while client/shops.lua is loading. If two files need the same helper, that is the signal to make it a resource global or move it into a module.

Hide helpers in their own file

When one real function needs five helpers that nobody else will ever call, the helpers do not belong next to the rest of your code. Put the group in its own file and expose one entry point:

server/pricing.lua
local function basePrice(item) ... end
local function jobDiscount(price, job) ... end
local function taxFor(price, zone) ... end

--- Final price for one item, for one player.
function GetItemPrice(item, job, zone)
    local price = basePrice(item)
    price = jobDiscount(price, job)
    return taxFor(price, zone)
end

Everything above GetItemPrice is unreachable from anywhere else, which is exactly what you want. The file is now the module boundary, and the rest of the resource only knows the one name.

Order inside a file

Two layouts work, and mixing them is what makes a file hard to read. Pick one per file:

  • By construct: file locals at the top, then local functions, then resource globals and exports, then event handlers and threads at the bottom.
  • By call order: each single use helper sits immediately above the function that calls it, so the file reads top to bottom like the code runs.

The first suits files with a lot of shared state, the second suits a file that is one feature from start to finish.

Naming

Resource folders: lowercase, words separated by underscores, no spaces. A dash is legal but it forces the bracket form everywhere the resource is called:

Lua
exports.my_resource:GetItemPrice(item, job, zone)   -- underscore, the dot form works
exports['my-resource']:GetItemPrice(item, job, zone) -- dash, bracket form required

Frameworks like qb-core use dashes for historical reasons and everyone lives with the brackets. For a new resource, use underscores and save yourself the noise. Prefix your own resources with something short (fm_, abc_) so they group together in the resources folder and never collide with a release you install later.

Files: lowercase, no spaces, dashes or underscores between words. Linux servers are case sensitive, so Client/Main.lua in the manifest and client/main.lua on disk will work on your Windows dev machine and fail in production. This is one of the most common “works locally” bugs in FiveM.

Next: Functions that stay small.