This guide shows how to implement a simple, server-authoritative racing script for QBCore-powered FiveM servers. It focuses on secure checkpoint sequencing, lap timing, and a minimal persistent best-time saving example. You’ll get a short implementation plan, assumptions, complete resource files (fxmanifest, client, server, config), installation steps, checks, and suggested next steps.
Overview
Racing scripts are a popular server activity, but naive client-side timers are easy to cheat. This guide builds a small QBCore resource where the server controls race state: players start, pass checkpoints in order, and the server computes lap and finish times. Clients only send checkpoint index events; the server validates order and stores best times. That design minimizes trust in the client while remaining performant and easy to extend.
Implementation plan (short)
- Define checkpoint positions and lap count in config.lua.
- Client shows markers and detects local proximity; when a checkpoint is passed, send the index to the server.
- Server tracks per-player race state (next checkpoint, lap start time). Server computes lap/finish times and validates ordering.
- On finish, save best time to your DB using your available MySQL library (example uses oxmysql; replace if needed).
Assumptions & dependencies
- Your server runs QBCore (modern export: exports['qb-core']:GetCoreObject()).
- A MySQL driver is available for persistence. Example uses exports['oxmysql']:execute. Replace with ghmattimysql, mysql-async, or your chosen wrapper if different.
- This example is minimal: integrate qb-target, ox_lib, UI menus, or server-side race scheduling to suit your server.
Files (complete, destination-labelled)
Place these files in a resource folder named qb-racing (or your chosen name). Folder structure:
- qb-racing/fxmanifest.lua
- qb-racing/config.lua
- qb-racing/client/main.lua
- qb-racing/server/main.lua
fxmanifest.lua
fx_version 'cerulean'
games { 'gta5' }
author 'YourName'
description 'QBCore Racing Script'
version '1.0.0'
dependency 'qb-core'
shared_script 'config.lua'
client_scripts { 'client/main.lua' }
server_scripts { 'server/main.lua' }
config.lua
Config = {}
-- Simple sample: three checkpoints forming a short circuit
Config.Checkpoints = {
{ x = 215.0, y = -810.0, z = 29.73, radius = 6.0 },
{ x = 255.0, y = -820.0, z = 29.73, radius = 6.0 },
{ x = 240.0, y = -790.0, z = 29.73, radius = 6.0 }
}
Config.Laps = 3
client/main.lua
local QBCore = exports['qb-core']:GetCoreObject()
local nextIndex = 1
local racing = false
local startTime = 0
-- Draw simple markers and check proximity
Citizen.CreateThread(function()
while true do
Citizen.Wait(500)
local ped = PlayerPedId()
local pos = GetEntityCoords(ped)
if not racing then
-- idle; no need to draw all markers every tick in production
else
local cp = Config.Checkpoints[nextIndex]
if cp then
DrawMarker(1, cp.x, cp.y, cp.z - 1.0, 0,0,0, 0,0,0, 2.0,2.0,1.0, 255,0,0,100, false, true, 2, nil, nil, false)
local dist = #(pos - vector3(cp.x, cp.y, cp.z))
if dist <= cp.radius then
-- pass locally and notify server
TriggerServerEvent('qb-race:checkpoint', nextIndex)
nextIndex = nextIndex + 1
if nextIndex > #Config.Checkpoints then
-- completed a lap locally; wrap
nextIndex = 1
end
Citizen.Wait(1000) -- simple debounce
end
end
end
end
end)
-- Commands for testing: start/stop
RegisterCommand('startrace', function()
racing = true
nextIndex = 1
TriggerServerEvent('qb-race:start')
QBCore.Functions.Notify('Race started!')
end)
RegisterCommand('stoprace', function()
racing = false
nextIndex = 1
TriggerServerEvent('qb-race:stop')
QBCore.Functions.Notify('Race stopped')
end)
server/main.lua
local QBCore = exports['qb-core']:GetCoreObject()
local raceState = {} -- keyed by source while in race
RegisterNetEvent('qb-race:start', function()
local src = source
local player = QBCore.Functions.GetPlayer(src)
if not player then return end
raceState[src] = {
nextIndex = 1,
lap = 1,
startTime = os.time()
}
TriggerClientEvent('QBCore:Notify', src, 'Server: race started')
end)
RegisterNetEvent('qb-race:stop', function()
local src = source
raceState[src] = nil
end)
RegisterNetEvent('qb-race:checkpoint', function(index)
local src = source
local state = raceState[src]
if not state then return end
-- validate order
if index ~= state.nextIndex then
-- out of order: ignore or punish
return
end
-- advance
state.nextIndex = state.nextIndex + 1
if state.nextIndex > #Config.Checkpoints then
-- completed a lap
local now = os.time()
local lapTime = now - state.startTime
state.lap = state.lap + 1
state.startTime = now
state.nextIndex = 1
-- if finished all laps
if state.lap > Config.Laps then
-- finished race
local player = QBCore.Functions.GetPlayer(src)
local citizenid = player.PlayerData.citizenid
-- Save best time example: replace with your DB method if needed
exports['oxmysql']:execute('INSERT INTO race_times (citizenid, time) VALUES (?, ?)',[citizenid, lapTime])
TriggerClientEvent('QBCore:Notify', src, ('Race finished! Final lap time: %s seconds'):format(lapTime))
raceState[src] = nil
return
else
TriggerClientEvent('QBCore:Notify', src, ('Lap complete! Lap time: %s seconds'):format(lapTime))
end
end
end)
Installation & deployment
- Create resource folder (qb-racing) and add the files above.
- Ensure fxmanifest.lua lists qb-core as dependency and your SQL wrapper is present on the server.
- Add your DB table (race_times) or change the save code to your existing schema. Do not store secrets in code.
- Add start/stop commands or a UI integration (qb-menu / qb-core exports) as you prefer.
- Start the resource and test with a client in a controlled environment.
If you want a workspace to plan versions and generate destination-labelled files faster, consider generating the project plan in the Stellar AI app: https://trystellarai.com/app — it helps structure file outputs and iterations. After you have a working prototype, you can import the plan again in the app to expand features: https://trystellarai.com/app.
Validation, security & best practices
- Server-authoritative checks: never accept times from the client. The server must compute lap and finish times using server-time and checkpoint order.
- Order enforcement: server must reject checkpoint events that are out of order. Optionally log or ban repeated cheating attempts.
- Database calls: sanitize input using parameterized queries (the example uses parameter placeholders). Do not log or store raw player tokens or secrets.
- Performance: avoid checking distances every frame for all players. The sample checks every 500ms. For many players, consider partitioning or using qb-target to reduce overhead.
- Persistence: record best times per citizenid, but consider rate limits and aggregation if races run frequently.
Checklist
| Task | Action |
|---|---|
| Config checkpoints | Define positions and radius in config.lua |
| Server validation | Ensure server computes times and enforces checkpoint order |
| DB integration | Replace example oxmysql call with your SQL library and test inserts |
| Client UI | Add in-game start UI / qb-menu if needed |
| Security test | Try sending out-of-order events from a client and verify rejection |
Next steps and tuning
After the basic server-authoritative loop works, you can add features: timed leaderboards, spectator mode, ghost replays, bet pools, or race lobbies. If you want help iterating your plan into full destination-labelled files or expanding to UI/menus, use the Stellar AI blog for inspiration: https://trystellarai.com/blog, or import your requirements into the app to produce concrete file outputs and revisions.
This guide aims to give a secure, minimal starting point. On production servers, test thoroughly with multiple players and review DB performance and anti-cheat logs before opening to the public.