Development
Complete guide to developing resources for QBCore framework.
This guide covers everything you need to know about developing custom resources for QBCore framework.
Development Environment Setup
Prerequisites
- Code Editor: VS Code with Lua extensions recommended
- Git: For version control
- Database Tool: HeidiSQL, phpMyAdmin, or similar
- QBCore Server: Running development server
Recommended VS Code Extensions
{
"recommendations": [
"sumneko.lua",
"actboy168.lua-debug",
"keyring.lua",
"koihik.vscode-lua-format",
"trixnz.vscode-lua"
]
}
Development Server Setup
Create a separate development server configuration:
# server-dev.cfg
set sv_hostname "QBCore Development Server"
set sv_maxclients 4
sv_licenseKey "your_license_key"
# Developer permissions
add_ace group.admin command allow
add_ace group.admin resource allow
add_principal identifier.steam:your_steam_id group.admin
# Fast restart for development
sv_scriptHookAllowed 1
set developer_mode true
Resource Structure
Standard QBCore Resource Structure
qb-resourcename/
├── client/
│ ├── main.lua
│ ├── events.lua
│ └── utils.lua
├── server/
│ ├── main.lua
│ ├── events.lua
│ └── callbacks.lua
├── shared/
│ ├── config.lua
│ ├── items.lua
│ └── locale.lua
├── html/ # For NUI resources
│ ├── index.html
│ ├── style.css
│ └── script.js
├── database/ # Database files
│ └── qb-resourcename.sql
├── locales/ # Translation files
│ ├── en.lua
│ ├── es.lua
│ └── fr.lua
├── fxmanifest.lua
└── README.md
fxmanifest.lua Template
fx_version 'cerulean'
game 'gta5'
author 'Your Name <[email protected]>'
description 'QBCore Resource Description'
version '1.0.0'
repository 'https://github.com/yourusername/qb-resourcename'
shared_scripts {
'@qb-core/shared/locale.lua',
'locales/en.lua',
'shared/*.lua'
}
client_scripts {
'client/*.lua'
}
server_scripts {
'@oxmysql/lib/MySQL.lua',
'server/*.lua'
}
ui_page 'html/index.html' -- For NUI resources
files {
'html/index.html',
'html/style.css',
'html/script.js'
}
lua54 'yes'
dependencies {
'qb-core',
'oxmysql'
}
provide 'qb-resourcename' -- Optional: for resource replacement
Core Integration
Getting QBCore Object
-- Client-side
local QBCore = exports['qb-core']:GetCoreObject()
-- Server-side
local QBCore = exports['qb-core']:GetCoreObject()
-- Alternative method (deprecated but still works)
QBCore = nil
CreateThread(function()
while QBCore == nil do
TriggerEvent('QBCore:GetObject', function(obj) QBCore = obj end)
Wait(200)
end
end)
Player Data Management
Getting Player Data
-- Server-side
local Player = QBCore.Functions.GetPlayer(source)
if Player then
local playerData = Player.PlayerData
local citizenId = Player.PlayerData.citizenid
local job = Player.PlayerData.job
local money = Player.PlayerData.money
end
-- Client-side
local PlayerData = QBCore.Functions.GetPlayerData()
if PlayerData then
local job = PlayerData.job
local money = PlayerData.money
end
Player Events
-- Client-side: Listen for player data updates
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function()
PlayerData = QBCore.Functions.GetPlayerData()
-- Initialize your resource after player loads
end)
RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo)
PlayerData.job = JobInfo
-- Handle job updates
end)
RegisterNetEvent('QBCore:Client:OnMoneyChange', function(moneyType, amount, operation)
-- Handle money changes
end)
-- Server-side: Player management
AddEventHandler('QBCore:Server:OnPlayerLoaded', function(Player)
-- Handle player loading
end)
AddEventHandler('playerDropped', function()
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if Player then
-- Handle player disconnect
end
end)
Database Integration
Using oxmysql
-- SELECT query
MySQL.Async.fetchAll('SELECT * FROM players WHERE job = ?', {jobName}, function(result)
if result[1] then
-- Handle results
end
end)
-- SELECT single row
MySQL.Async.fetchSingle('SELECT * FROM players WHERE citizenid = ?', {citizenId}, function(result)
if result then
-- Handle single result
end
end)
-- INSERT query
MySQL.Async.execute('INSERT INTO my_table (citizenid, data) VALUES (?, ?)', {
citizenId,
json.encode(data)
}, function(affectedRows)
if affectedRows > 0 then
-- Success
end
end)
-- UPDATE query
MySQL.Async.execute('UPDATE players SET money = ? WHERE citizenid = ?', {
json.encode(money),
citizenId
}, function(affectedRows)
-- Handle update
end)
Modern oxmysql (Promise-based)
-- Using promises (recommended)
local result = MySQL.query.await('SELECT * FROM players WHERE job = ?', {jobName})
if result[1] then
-- Handle results
end
-- With error handling
local success, result = pcall(MySQL.query.await, 'SELECT * FROM players WHERE citizenid = ?', {citizenId})
if success and result[1] then
-- Handle success
else
print('Database query failed')
end
Item System
Creating Custom Items
Add items to qb-core/shared/items.lua
:
QBShared.Items = {
-- Existing items...
['my_custom_item'] = {
name = 'my_custom_item',
label = 'My Custom Item',
weight = 500,
type = 'item',
image = 'my_custom_item.png',
unique = false,
useable = true,
shouldClose = true,
combinable = nil,
description = 'This is my custom item description'
},
['weapon_custom'] = {
name = 'weapon_custom',
label = 'Custom Weapon',
weight = 2000,
type = 'weapon',
ammotype = 'AMMO_PISTOL',
image = 'weapon_custom.png',
unique = true,
useable = false,
description = 'A custom weapon'
}
}
Item Usage Events
-- Server-side: Register useable item
QBCore.Functions.CreateUseableItem('my_custom_item', function(source, item)
local Player = QBCore.Functions.GetPlayer(source)
if Player then
-- Item usage logic
TriggerClientEvent('qb-myresource:client:useItem', source, item)
end
end)
-- Client-side: Handle item usage
RegisterNetEvent('qb-myresource:client:useItem', function(item)
-- Client-side item effects
QBCore.Functions.Notify('You used ' .. item.label, 'success')
end)
Inventory Management
-- Server-side inventory functions
local Player = QBCore.Functions.GetPlayer(source)
-- Add item
Player.Functions.AddItem('my_item', 1, false, {quality = 100})
-- Remove item
Player.Functions.RemoveItem('my_item', 1)
-- Get item
local item = Player.Functions.GetItemByName('my_item')
if item then
print('Player has ' .. item.amount .. ' of ' .. item.label)
end
-- Check if player has item
local hasItem = Player.Functions.GetItemByName('my_item') ~= nil
Job System
Creating Custom Jobs
Add jobs to qb-core/shared/jobs.lua
:
QBShared.Jobs = {
-- Existing jobs...
['mechanic'] = {
label = 'Mechanic',
defaultDuty = true,
offDutyPay = false,
grades = {
['0'] = {
name = 'Trainee',
payment = 50
},
['1'] = {
name = 'Mechanic',
payment = 75
},
['2'] = {
name = 'Expert Mechanic',
payment = 100
},
['3'] = {
name = 'Shop Supervisor',
payment = 125
},
['4'] = {
name = 'Shop Owner',
isboss = true,
payment = 150
},
},
}
}
Job Management Functions
-- Server-side: Job management
local Player = QBCore.Functions.GetPlayer(source)
-- Set player job
Player.Functions.SetJob('mechanic', 2)
-- Check job permissions
if Player.PlayerData.job.name == 'police' and Player.PlayerData.job.grade.level >= 3 then
-- Allow police captain+ actions
end
-- Check if player is boss
if Player.PlayerData.job.isboss then
-- Boss-only actions
end
-- Client-side: Job checking
local PlayerData = QBCore.Functions.GetPlayerData()
if PlayerData.job.name == 'mechanic' then
-- Mechanic-specific functionality
end
Vehicle System
Vehicle Management
-- Server-side: Vehicle functions
local Player = QBCore.Functions.GetPlayer(source)
-- Get player vehicles
local vehicles = MySQL.query.await('SELECT * FROM player_vehicles WHERE citizenid = ?', {Player.PlayerData.citizenid})
-- Add vehicle to player
local vehicleData = {
citizenid = Player.PlayerData.citizenid,
vehicle = 'adder',
hash = GetHashKey('adder'),
mods = json.encode({}),
plate = 'ABC123',
garage = 'pillboxgarage',
fuel = 100,
engine = 1000.0,
body = 1000.0
}
MySQL.insert.await('INSERT INTO player_vehicles (citizenid, vehicle, hash, mods, plate, garage, fuel, engine, body) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', {
vehicleData.citizenid,
vehicleData.vehicle,
vehicleData.hash,
vehicleData.mods,
vehicleData.plate,
vehicleData.garage,
vehicleData.fuel,
vehicleData.engine,
vehicleData.body
})
Vehicle Keys Integration
-- Give keys to player
TriggerEvent('qb-vehiclekeys:server:GiveVehicleKeys', source, plate)
-- Remove keys from player
TriggerEvent('qb-vehiclekeys:server:RemoveVehicleKeys', source, plate)
-- Client-side: Check if player has keys
local hasKeys = exports['qb-vehiclekeys']:HasKeys(plate)
UI Development (NUI)
Basic NUI Setup
<!-- html/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QBCore Resource UI</title>
<link rel="stylesheet" href="style.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div id="container" style="display: none;">
<div id="header">
<h1>My QBCore Resource</h1>
<button id="close-btn">×</button>
</div>
<div id="content">
<!-- Your UI content here -->
</div>
</div>
<script src="script.js"></script>
</body>
</html>
/* html/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: transparent;
color: white;
}
#container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600px;
height: 400px;
background: rgba(0, 0, 0, 0.9);
border-radius: 10px;
border: 1px solid #333;
}
#header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #333;
}
#close-btn {
background: #ff4757;
color: white;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
cursor: pointer;
font-size: 16px;
}
#content {
padding: 20px;
}
// html/script.js
$(document).ready(function() {
// Hide UI on ESC key
document.onkeyup = function(data) {
if (data.which == 27) {
closeUI();
}
}
// Close button click
$('#close-btn').click(function() {
closeUI();
});
// Listen for messages from Lua
window.addEventListener('message', function(event) {
const data = event.data;
switch(data.action) {
case 'open':
openUI(data.data);
break;
case 'close':
closeUI();
break;
case 'updateData':
updateUI(data.data);
break;
}
});
});
function openUI(data) {
$('#container').fadeIn(300);
$.post('https://qb-myresource/uiLoaded', JSON.stringify({}));
}
function closeUI() {
$('#container').fadeOut(300);
$.post('https://qb-myresource/closeUI', JSON.stringify({}));
}
function updateUI(data) {
// Update UI with new data
}
Lua NUI Integration
-- Client-side NUI management
local isUIOpen = false
-- Open UI
function OpenUI(data)
if isUIOpen then return end
isUIOpen = true
SetNuiFocus(true, true)
SendNUIMessage({
action = 'open',
data = data
})
end
-- Close UI
function CloseUI()
if not isUIOpen then return end
isUIOpen = false
SetNuiFocus(false, false)
SendNUIMessage({
action = 'close'
})
end
-- NUI Callbacks
RegisterNUICallback('uiLoaded', function(data, cb)
-- UI has loaded
cb('ok')
end)
RegisterNUICallback('closeUI', function(data, cb)
CloseUI()
cb('ok')
end)
-- Export functions for other resources
exports('OpenUI', OpenUI)
exports('CloseUI', CloseUI)
Event System
Custom Events
-- Server-side events
RegisterNetEvent('qb-myresource:server:doSomething', function(data)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
-- Validate data
if not data or not data.value then
return
end
-- Process request
local success = processRequest(Player, data)
-- Send response
TriggerClientEvent('qb-myresource:client:requestResult', src, success)
end)
-- Client-side events
RegisterNetEvent('qb-myresource:client:requestResult', function(success)
if success then
QBCore.Functions.Notify('Request successful!', 'success')
else
QBCore.Functions.Notify('Request failed!', 'error')
end
end)
Callbacks
-- Server-side callback
QBCore.Functions.CreateCallback('qb-myresource:server:getData', function(source, cb, playerId)
local Player = QBCore.Functions.GetPlayer(playerId)
if Player then
cb(Player.PlayerData)
else
cb(false)
end
end)
-- Client-side callback usage
QBCore.Functions.TriggerCallback('qb-myresource:server:getData', function(playerData)
if playerData then
-- Use player data
end
end, GetPlayerServerId(PlayerId()))
Commands
Creating Commands
-- Server-side command
QBCore.Commands.Add('mycommand', 'Command description', {{name = 'target', help = 'Target player ID'}}, true, function(source, args)
local Player = QBCore.Functions.GetPlayer(source)
if not Player then return end
local targetId = tonumber(args[1])
if not targetId then
TriggerClientEvent('QBCore:Notify', source, 'Invalid player ID', 'error')
return
end
local TargetPlayer = QBCore.Functions.GetPlayer(targetId)
if not TargetPlayer then
TriggerClientEvent('QBCore:Notify', source, 'Player not found', 'error')
return
end
-- Command logic here
TriggerClientEvent('QBCore:Notify', source, 'Command executed successfully', 'success')
end, 'admin') -- Permission level
-- Client-side command
RegisterCommand('clientcommand', function(source, args, rawCommand)
local PlayerData = QBCore.Functions.GetPlayerData()
if PlayerData.job.name == 'police' then
-- Police-only client command
end
end, false)
Configuration Management
config.lua Template
Config = {}
-- General settings
Config.Debug = false
Config.Locale = GetConvar('qb_locale', 'en')
-- Feature toggles
Config.EnableFeatureA = true
Config.EnableFeatureB = false
-- Timing settings
Config.Cooldowns = {
action1 = 5000, -- 5 seconds
action2 = 30000, -- 30 seconds
}
-- Job permissions
Config.JobPermissions = {
mechanic = {
repair = 0, -- Grade 0+
advanced = 2, -- Grade 2+
boss = 4 -- Grade 4+
},
police = {
arrest = 0,
impound = 1,
commander = 3
}
}
-- Locations
Config.Locations = {
mechanic_shop = {
coords = vector3(123.45, 678.90, 12.34),
heading = 90.0,
radius = 2.0,
blip = {
sprite = 446,
color = 2,
scale = 0.8,
name = "Mechanic Shop"
}
}
}
-- Items and pricing
Config.Items = {
repair_kit = {
item = 'repair_kit',
price = 500,
requiredJob = 'mechanic'
}
}
-- Notifications
Config.Notifications = {
success_repair = 'Vehicle repaired successfully',
insufficient_funds = 'You don\'t have enough money',
wrong_job = 'You don\'t have the required job'
}
Testing & Debugging
Debug Functions
-- Debug utility
local function DebugPrint(...)
if Config.Debug then
print('^3[QB-MYRESOURCE]^7', ...)
end
end
-- Server-side debugging
local function LogAction(player, action, data)
if Config.Debug then
print(string.format('^3[QB-MYRESOURCE]^7 Player: %s (%s) | Action: %s | Data: %s',
player.PlayerData.name,
player.PlayerData.citizenid,
action,
json.encode(data)
))
end
end
-- Client-side debugging
local function DrawDebugText(text, x, y)
if Config.Debug then
SetTextFont(0)
SetTextProportional(1)
SetTextScale(0.0, 0.35)
SetTextDropshadow(0, 0, 0, 0, 255)
SetTextEdge(1, 0, 0, 0, 255)
SetTextDropShadow()
SetTextOutline()
SetTextEntry("STRING")
AddTextComponentString(text)
DrawText(x, y)
end
end
Testing Checklist
- Resource starts without errors
- Database connections work
- Player events trigger correctly
- Items can be used/given/removed
- UI opens/closes properly
- Commands execute with proper permissions
- No console errors or warnings
- Memory usage is reasonable
- Performance is acceptable
Best Practices
Code Organization
- Separate Concerns: Keep client, server, and shared code separate
- Use Callbacks: For data requests between client and server
- Validate Everything: Never trust client input
- Handle Errors: Use pcall for database operations
- Performance: Avoid unnecessary loops and timers
Security
- Server Validation: Always validate on the server
- Permission Checks: Verify job/permissions before actions
- Rate Limiting: Prevent spam/abuse
- Secure Events: Use source validation
Performance
- Efficient Queries: Use proper database indexes
- Caching: Cache frequently accessed data
- Resource Cleanup: Clean up on resource stop
- Minimal UI: Keep NUI lightweight
This comprehensive development guide provides everything needed to create professional QBCore resources following best practices and framework conventions.