Skip to Content
ScriptingExamples
Open in ChatGPTOpen in Claude

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 allow

config.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 ACE

Grant it with add_ace group.admin command.getuser allow; see Permissions.