Lua doesn't really have a widely adopted central coding standard, especially when it comes to FiveM development. A lot of the coding standard around FiveM can be drastically improved, and we hope this will help people! This repository outlines the coding standards we follow at Versa to ensure all contributed Lua code remains clean, consistent, and maintainable across all of our projects.
If you have suggestions or improvements, feel free to open a Pull Request β weβd love to see your ideas!
- 1.0 - Lua Syntax
- 2.0 - FiveM Development Practices
- 3.0 - Documentation & Commenting
- 4.0 - File Structure
- 5.0 - Error Handling
- When defining a constant, use
UPPER_SNAKE_CASE. We also make use of the new Lua 5.4 keyword.- Must define Lua 5.4 runtime in the
fxmanifest.luain order to use it
- Must define Lua 5.4 runtime in the
local SERVER_MAX_PLAYERS <const> = 100- However, local variables assigned from runtime data (e.g., property on an object) that do not change inside a function should use
lowerCamelCaseand can be marked with the keyword if it will not be reassigned.- Example: 5.0 Error Handling ->
hasMoney()
- Example: 5.0 Error Handling ->
local spendLimit <const> = Account.spendLimit- While Luaβs standard library and most popular community codebases use
snake_case, we prefer to uselowerCamelCase/UpperCamelCaseas we believe there is better consistency and readability. . - Do not use Hungarian Notation when defining variables
- Keep functions short (some say even 20-25 lines). Separate longer functions into smaller helper functions.
- Use
lowerCamelCasefor all local variables and local functions.
local someLocalVariable = {}
local function someLocalFunction()
-- ... code ...
end- Use
UpperCamelCasefor all global variables and global functions.
SomeGlobalVariable = {}
function SomeGlobalVariable()
-- ... code ...
end- Use spaces around operators for better readability.
- Do not put any spaces inside parentheses for function calls or grouped expressions...
-- Don't do this:
print(firstName..' '..lastName)
local sum=a+b
local result = ( a + b ) * c
local numbers = {1,2,3}
-- Do this:
print(firstName .. ' ' .. lastName)
local sum = a + b
local result = (a + b) * c
local numbers = {1, 2, 3}- Use
ipairswhen you want to iterate until the first nil and the data is structured as a clean array. - Use
for i = 1, #tablewhen performance is critical or when you know the array may containnilvalues but#tablestill gives you a safe bound. - Always avoid modifying the table you're looping over during iteration.
- Nope. Separate code onto multiple lines.
- When Registering an event, define the handler function directly in
RegisterNetEvent - Naming events should always follow the same pattern -
resource:server/client:eventKey
-- Don't do this
RegisterNetEvent('panel:server:sendLog')
AddEventHandler('panel:server:sendLog', function(message)
print('Log Message: ' .. message)
end)
-- Do this
RegisterNetEvent('panel:server:sendLog', function(message)
print('Log Message: ' .. message)
end)- Inspired from Project Error's FiveM Lua Style, we agree with what they say: "Where possible, avoid using the Citizen prefix for the Citizen table of functions"
- The following Citizen functions are globally aliased and can be called without the prefix:
SetTimeout,Wait,CreateThread,RconPrint(alias for Citizen.Trace). For all other Citizen table functions, continue to use the fullCitizen.prefix as they are not aliased globally.
-- Don't do this
Citizen.CreateThread(function()
Citizen.Wait(1000) -- 1 second wait with ugly Citizen prefix
-- ... code ...
end)
-- Do this
CreateThread(function()
Wait(1000) -- 1 second wait
-- ... code ...
end)- When documenting a function, you always use 3 dashes (
---) to describe what the function does, and then for each@paramyou make a new line, and finally the@returnto describe what the function returns. - Note: You don't need to document every single local function β especially small, well-named helper functions that are only used within a single file.
- Use your judgment: if the function has non-obvious logic, multiple parameters, or will be used in multiple places, it's worth documenting.
--- Add money to a player's bank account
-- @param player int The online player ID
-- @param account int The account ID to add money to
-- @param amount int The amount of money to add
-- @param message string The transaction message (reason for adding money)
-- @return table or (nil, string)
-- Returns a table of the account object on success, or nil and an error message on failure
function AddMoneyToAccount(player, account, amount, message)
-- ... code ...
end- Use comments to explain why something is done, not what the code is doingβgood code will be self-explanatory. Essentially, make sure they are concise and relevant
- Use single-line comments (
--) for short explanations and multi-line comments (--[[ ... ]]) for longer descriptions or disabling code blocks temporarily. - Use
TODO:andFIXME:tags to indicate missing features/problems in existing code.
-- TODO: implement method
local function something()
-- FIXME: check conditions
end- Resources should be lowercase and split by underscores. All client files must have a
cl_prefix in the filename & all server files must have asv_prefix in the file name. All filenames must be fully lowercase. - A config folder is used to separate by environment (client, server, shared).
- Obviously, you do not want to store sensitive data in the client or shared config as the client can access it.
- We have a resource template on our GitHub (fxmanifest data included)
βββ fxmanifest.lua
βββ client
β βββ cl_main.lua
βββ server
β βββ sv_main.lua
βββ shared
β βββ sh_utils.lua
βββ config
βββ sh_config.lua -- shared config
βββ sv_config.lua -- server-only config
βββ cl_config.lua -- client-only config (if needed)
- Functions that can fail for expected reasons should return
niland a (string) error message - One of the main reasons we (Versa) use this is because we use a lot of APIs. Also, returning errors from helper functions keeps main code cleaner as you won't need to plaster hard-coded error messages all over it (main codebase).
-- a really simple way to show how to handle an error using a helper function
-- does all validation in helper function so the main payForFood() is clean
local function hasMoney(amountNeeded)
local bankAccount <const> = Account.money
local spendLimit <const> = Account.spendLimit
local spentToday <const> = Account.spentToday
if amountNeeded > bankAccount then
return nil, 'Missing $' .. (amountNeeded - bankAccount)
end
if spentToday >= spendLimit then
return nil, 'You are over your spend limit'
end
return true
end
-- Calling code example (would be in main code)
local function payForFood(amount)
local result, err = hasMoney(amount) -- trigger our helper function
if not result then
ShowNotification(err) -- The helper function handles all error messages, so this stays clean
return -- dont run any mmore of the code
end
getMyFood()
end- In other cases (e.g. not using an API) you could also just show the notification in the helper function then just return inside
payForFood()
local result = hasMoney(amount) -- trigger our helper function
if not result then return end -- dont run any more code