Scripting Examples
Complete, working examples you can use as starting points. Copy an example into its own resource, keep client and server responsibilities separate, and rename every public event before extending it. Validate all client-provided values on the server before connecting an example to money, inventory, permissions, or persistence.
Vehicle lab: server-authoritative spawning
A complete, downloadable Lua resource for the most common request in FiveM development: spawning a vehicle without trusting the client.
Download fd-vehicle-lab 1.0.0 (Lua, no dependencies, requires OneSync). Its hash is recorded in the download manifest.
The client sends only a model name. The server checks an allow-list and an ACE
permission, applies a per-player cooldown, creates the vehicle with
CreateVehicleServerSetter at the player’s own position, moves it into the
player’s routing bucket and seats the player. Every failure path deletes the
vehicle: a sync or ownership timeout, a replacement request, a disconnect
during the spawn (server IDs can be reused immediately), playerDropped and
the resource stopping. Because the client never creates the entity, it also
works with sv_entityLockdown strict.
ensure fd-vehicle-lab
add_ace group.admin fdlab.vehicle allowconfig.lua
Config = {
-- ACE object a player needs: add_ace group.admin fdlab.vehicle allow
ace = 'fdlab.vehicle',
cooldownMs = 5000,
-- Upper bound for each wait on entity creation and ownership.
timeoutMs = 5000,
-- Allow-list of model name -> CreateVehicleServerSetter type.
models = {
adder = 'automobile',
bati = 'bike',
buzzard = 'heli',
},
}server.lua
-- Server-authoritative vehicle spawning. The client sends only a model name;
-- permission, model, position, routing bucket and cleanup are decided here.
local owned = {} -- [source] = vehicle entity this player spawned
local pending = {} -- [source] = token of the request being processed
local lastRequest = {} -- [source] = GetGameTimer() of the last accepted request
local function reply(src, ok, detail)
TriggerClientEvent('fd-vehicle-lab:result', src, ok, detail)
end
local function waitFor(check)
local deadline = GetGameTimer() + Config.timeoutMs
while not check() do
if GetGameTimer() >= deadline then return false end
Wait(0)
end
return true
end
local function deleteVehicle(vehicle)
if vehicle and DoesEntityExist(vehicle) then DeleteEntity(vehicle) end
end
local function deleteOwned(src)
deleteVehicle(owned[src])
owned[src] = nil
end
local function spawnFor(src, ped, modelName, vehicleType)
local coords = GetEntityCoords(ped)
local vehicle = CreateVehicleServerSetter(GetHashKey(modelName), vehicleType,
coords.x, coords.y, coords.z, GetEntityHeading(ped))
if vehicle == 0 then return nil end
SetEntityRoutingBucket(vehicle, GetPlayerRoutingBucket(src))
-- A non-empty plate means a client has created and synced the entity.
local synced = waitFor(function()
return DoesEntityExist(vehicle) and GetVehicleNumberPlateText(vehicle) ~= ''
end)
if synced then
SetPedIntoVehicle(ped, vehicle, -1)
-- Seating the driver hands ownership to the driver's client.
synced = waitFor(function() return NetworkGetEntityOwner(vehicle) == src end)
end
if not synced then
deleteVehicle(vehicle)
return nil
end
return vehicle
end
RegisterNetEvent('fd-vehicle-lab:request', function(modelName)
local src = source
local vehicleType = type(modelName) == 'string' and Config.models[modelName]
if not vehicleType then return reply(src, false, 'unknown model') end
if not IsPlayerAceAllowed(src, Config.ace) then return reply(src, false, 'not allowed') end
local now = GetGameTimer()
if pending[src] or (lastRequest[src] and now - lastRequest[src] < Config.cooldownMs) then
return reply(src, false, 'too many requests')
end
local ped = GetPlayerPed(src)
if ped == 0 then return end
lastRequest[src] = now
local token = {}
pending[src] = token
deleteOwned(src) -- one lab vehicle per player
local vehicle = spawnFor(src, ped, modelName, vehicleType)
-- The player left while we waited; the ID may already belong to someone else.
if pending[src] ~= token then
deleteVehicle(vehicle)
return
end
pending[src] = nil
if not vehicle then return reply(src, false, 'spawn failed') end
owned[src] = vehicle
reply(src, true, NetworkGetNetworkIdFromEntity(vehicle))
end)
AddEventHandler('playerDropped', function()
local src = source
pending[src] = nil
lastRequest[src] = nil
deleteOwned(src)
end)
AddEventHandler('onResourceStop', function(resourceName)
if resourceName ~= GetCurrentResourceName() then return end
for src in pairs(owned) do deleteOwned(src) end
end)What is tested, and what is not
lua tests/server_test.lua runs this exact server.lua against mocked
natives and a cooperative scheduler: rejection, ACE, cooldown, replacement,
both timeouts, a disconnect mid-spawn and both cleanup paths (11 cases, run in
CI with Lua 5.4). Mocks prove control flow, not engine behavior. Before relying
on it, test in a real client on your artifact: each model, a non-default
routing bucket, restart while seated, disconnect while seated, and
sv_entityLockdown strict. The plate and ownership waits follow the pattern
Qbox uses in qbx.spawnVehicle.
Example 1: Simple Notification Resource
fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
author 'Your Name'
description 'Simple notification resource'
version '1.0.0'
client_scripts {
'client.lua'
}
server_scripts {
'server.lua'
}server.lua
RegisterCommand('notify', function(source, args)
TriggerClientEvent('myresource:notify', source, {
message = table.concat(args, ' ')
})
end, false)client.lua
RegisterNetEvent('myresource:notify')
AddEventHandler('myresource:notify', function(data)
-- Show notification (using your preferred method)
print('Notification:', data.message)
end)Example 2: Simple Menu
client.lua
local menuOpen = false
RegisterCommand('menu', function()
menuOpen = not menuOpen
if menuOpen then
-- Open menu
print('Menu opened')
else
-- Close menu
print('Menu closed')
end
end, false)Example 3: Database Query
A staff-only lookup. The command is restricted, the query selects only the
column it prints, and the reply works from both the server console and in game.
It needs oxmysql, whose MySQL object exists only after
its library is loaded in the manifest.
fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
dependency 'oxmysql'
server_scripts {
'@oxmysql/lib/MySQL.lua',
'server.lua',
}server.lua
local function reply(source, message)
if source == 0 then
print(message) -- server console
else
TriggerClientEvent('chat:addMessage', source, { args = { 'User', message } })
end
end
RegisterCommand('getuser', function(source, args)
local userId = tonumber(args[1])
if not userId or userId % 1 ~= 0 then
return reply(source, 'Usage: /getuser <id>')
end
MySQL.scalar('SELECT name FROM users WHERE id = ?', { userId }, function(name)
reply(source, name and ('#%d: %s'):format(userId, name) or 'Not found')
end)
end, true) -- restricted: requires the command.getuser ACEGrant it with add_ace group.admin command.getuser allow; see
Permissions.