🔧 Custom Resource Development
Learn how to create custom QBCore resources from the ground up, following best practices and professional development patterns.
Getting Started
Custom resources extend QBCore’s functionality by adding new features, jobs, systems, or gameplay mechanics. This guide covers the complete development lifecycle.
Resource Types
QBCore supports several types of custom resources:
- Job Resources - Custom employment systems
- Activity Resources - Gameplay activities and mini-games
- Utility Resources - Helper systems and tools
- Integration Resources - Third-party system connections
Basic Resource Structure
Minimum Required Files
Every QBCore resource needs this basic structure:
your-resource/
├── fxmanifest.lua # Resource manifest
├── server/
│ └── main.lua # Server-side logic
├── client/
│ └── main.lua # Client-side logic
├── shared/
│ └── config.lua # Shared configuration
└── README.md # Documentation
Advanced Resource Structure
For complex resources, use this extended structure:
your-resource/
├── fxmanifest.lua
├── server/
│ ├── main.lua
│ ├── events.lua # Server events
│ ├── functions.lua # Server functions
│ ├── callbacks.lua # Server callbacks
│ └── commands.lua # Server commands
├── client/
│ ├── main.lua
│ ├── events.lua # Client events
│ ├── functions.lua # Client functions
│ ├── menu.lua # Menu systems
│ └── ui.lua # UI interactions
├── shared/
│ ├── config.lua # Configuration
│ ├── locale.lua # Localization
│ └── utils.lua # Shared utilities
├── html/ # UI files
│ ├── index.html
│ ├── style.css
│ └── script.js
├── locales/ # Language files
│ ├── en.lua
│ └── es.lua
└── sql/ # Database files
└── install.sql
Creating the Manifest
The fxmanifest.lua
file defines your resource:
fx_version 'cerulean'
game 'gta5'
author 'Your Name'
description 'Custom QBCore Resource'
version '1.0.0'
-- Dependencies
dependencies {
'qb-core',
'qb-target', -- If using target system
'qb-menu' -- If using menu system
}
-- Shared files
shared_scripts {
'shared/config.lua',
'shared/locale.lua'
}
-- Client files
client_scripts {
'client/main.lua',
'client/events.lua',
'client/functions.lua'
}
-- Server files
server_scripts {
'server/main.lua',
'server/events.lua',
'server/functions.lua'
}
-- UI files (if needed)
ui_page 'html/index.html'
files {
'html/index.html',
'html/style.css',
'html/script.js'
}
-- Exports
exports {
'GetResourceData',
'IsPlayerEligible'
}
server_exports {
'CreateCustomJob',
'ProcessPayment'
}
lua54 'yes'
Basic Implementation
1. Server-Side Setup
-- server/main.lua
local QBCore = exports['qb-core']:GetCoreObject()
-- Resource initialization
CreateThread(function()
print('^2[' .. GetCurrentResourceName() .. ']^7 Resource started successfully')
-- Initialize database tables if needed
InitializeDatabase()
-- Load existing data
LoadResourceData()
end)
-- Core functions
function InitializeDatabase()
-- Create tables if they don't exist
MySQL.query([[
CREATE TABLE IF NOT EXISTS custom_resource_data (
id INT AUTO_INCREMENT PRIMARY KEY,
citizenid VARCHAR(50) NOT NULL,
data LONGTEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
]])
end
function LoadResourceData()
-- Load any persistent data your resource needs
local result = MySQL.query.await('SELECT * FROM custom_resource_data')
for i = 1, #result do
-- Process loaded data
ProcessLoadedData(result[i])
end
end
-- Player joined handler
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function()
local src = source
local player = QBCore.Functions.GetPlayer(src)
if player then
-- Initialize player data for your resource
InitializePlayerData(src, player)
end
end)
function InitializePlayerData(source, player)
-- Setup player-specific data
TriggerClientEvent('your-resource:client:initialize', source, {
citizenid = player.PlayerData.citizenid,
permissions = GetPlayerPermissions(player),
settings = GetPlayerSettings(player.PlayerData.citizenid)
})
end
2. Client-Side Setup
-- client/main.lua
local QBCore = exports['qb-core']:GetCoreObject()
local PlayerData = QBCore.Functions.GetPlayerData()
-- Resource initialization
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function()
PlayerData = QBCore.Functions.GetPlayerData()
InitializeClientSide()
end)
RegisterNetEvent('QBCore:Client:OnPlayerUnload', function()
PlayerData = {}
CleanupClientSide()
end)
function InitializeClientSide()
-- Setup client-side functionality
CreateThread(function()
while PlayerData do
-- Main client loop
Wait(1000)
-- Check conditions, update UI, etc.
UpdateClientState()
end
end)
end
function CleanupClientSide()
-- Clean up client-side resources
-- Remove blips, NPCs, objects, etc.
end
-- Resource-specific initialization
RegisterNetEvent('your-resource:client:initialize', function(data)
print('Initializing custom resource for player:', data.citizenid)
-- Setup player-specific client data
SetupPlayerInterface(data)
CreateResourceBlips()
SpawnResourceNPCs()
end)
3. Configuration System
-- shared/config.lua
Config = {}
-- General Settings
Config.Debug = false
Config.Locale = 'en'
-- Feature Toggles
Config.EnableNotifications = true
Config.EnableBlips = true
Config.EnableNPCs = true
-- Locations
Config.Locations = {
main_office = {
coords = vector3(100.0, 200.0, 25.0),
heading = 90.0,
blip = {
sprite = 280,
color = 2,
scale = 0.8,
label = "Custom Resource Office"
}
}
}
-- Job Configuration
Config.Jobs = {
allowed_jobs = {"police", "ambulance", "mechanic"},
payment_range = {min = 500, max = 1500},
experience_multiplier = 1.0
}
-- Items and Rewards
Config.Items = {
required_items = {"id_card", "phone"},
reward_items = {
{item = "money", amount = {min = 100, max = 500}},
{item = "custom_item", amount = 1}
}
}
-- Timing Configuration
Config.Timers = {
cooldown_time = 300, -- 5 minutes
process_time = 60, -- 1 minute
reset_time = 86400 -- 24 hours
}
Event System Integration
Server Events
-- server/events.lua
-- Custom resource events
RegisterNetEvent('your-resource:server:processAction', function(actionType, data)
local src = source
local player = QBCore.Functions.GetPlayer(src)
if not player then return end
-- Validate the action
if not ValidateAction(player, actionType, data) then
TriggerClientEvent('QBCore:Notify', src, 'Action not allowed', 'error')
return
end
-- Process the action
local result = ProcessPlayerAction(player, actionType, data)
if result.success then
-- Update player data
UpdatePlayerProgress(player, result.progress)
-- Send success response
TriggerClientEvent('your-resource:client:actionComplete', src, result)
TriggerClientEvent('QBCore:Notify', src, result.message, 'success')
else
TriggerClientEvent('QBCore:Notify', src, result.error, 'error')
end
end)
-- Integration with other resources
RegisterNetEvent('qb-phone:server:sendMessage', function(phoneNumber, message)
-- Handle phone integration
if message:find("#customresource") then
ProcessPhoneCommand(source, message)
end
end)
function ValidateAction(player, actionType, data)
-- Check player permissions
if not HasRequiredJob(player) then
return false
end
-- Check cooldowns
if IsPlayerOnCooldown(player.PlayerData.citizenid, actionType) then
return false
end
-- Check required items
if not HasRequiredItems(player, actionType) then
return false
end
return true
end
Client Events
-- client/events.lua
-- Handle server responses
RegisterNetEvent('your-resource:client:actionComplete', function(result)
-- Update UI
UpdateProgressBar(result.progress)
-- Play effects
if result.effects then
PlayEffects(result.effects)
end
-- Update local data
UpdateLocalPlayerData(result.playerData)
end)
-- Integration events
RegisterNetEvent('your-resource:client:receivePhoneMessage', function(sender, message)
-- Handle phone notifications
TriggerEvent('qb-phone:client:addMessage', {
sender = sender,
message = message,
time = os.date('%H:%M')
})
end)
RegisterNetEvent('your-resource:client:showNotification', function(data)
-- Custom notification system
if Config.EnableNotifications then
ShowCustomNotification(data.title, data.message, data.type)
end
end)
Database Integration
Setting Up Tables
-- sql/install.sql
CREATE TABLE IF NOT EXISTS `custom_resource_players` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`citizenid` VARCHAR(50) NOT NULL,
`level` INT(11) DEFAULT 1,
`experience` INT(11) DEFAULT 0,
`settings` LONGTEXT DEFAULT '{}',
`statistics` LONGTEXT DEFAULT '{}',
`last_active` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `citizenid` (`citizenid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `custom_resource_actions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`citizenid` VARCHAR(50) NOT NULL,
`action_type` VARCHAR(50) NOT NULL,
`action_data` LONGTEXT DEFAULT '{}',
`timestamp` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `citizenid` (`citizenid`),
KEY `action_type` (`action_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Database Functions
-- server/functions.lua
function GetPlayerResourceData(citizenid)
local result = MySQL.query.await('SELECT * FROM custom_resource_players WHERE citizenid = ?', {citizenid})
if result[1] then
return {
level = result[1].level,
experience = result[1].experience,
settings = json.decode(result[1].settings),
statistics = json.decode(result[1].statistics)
}
end
return CreateDefaultPlayerData(citizenid)
end
function UpdatePlayerResourceData(citizenid, data)
MySQL.insert('INSERT INTO custom_resource_players (citizenid, level, experience, settings, statistics) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE level = VALUES(level), experience = VALUES(experience), settings = VALUES(settings), statistics = VALUES(statistics), last_active = CURRENT_TIMESTAMP', {
citizenid,
data.level,
data.experience,
json.encode(data.settings),
json.encode(data.statistics)
})
end
function LogPlayerAction(citizenid, actionType, actionData)
MySQL.insert('INSERT INTO custom_resource_actions (citizenid, action_type, action_data) VALUES (?, ?, ?)', {
citizenid,
actionType,
json.encode(actionData)
})
end
Testing Your Resource
Development Testing
-- Add debug commands for testing
if Config.Debug then
RegisterCommand('test-resource', function(source, args)
local player = QBCore.Functions.GetPlayer(source)
if player then
-- Test your resource functionality
TriggerEvent('your-resource:server:processAction', 'test', {
type = args[1] or 'default'
})
end
end, false)
RegisterCommand('reset-resource-data', function(source, args)
local player = QBCore.Functions.GetPlayer(source)
if player then
ResetPlayerData(player.PlayerData.citizenid)
TriggerClientEvent('QBCore:Notify', source, 'Resource data reset', 'info')
end
end, false)
end
Quality Assurance Checklist
- ✅ Performance Testing: Resource uses minimal server resources
- ✅ Error Handling: All functions have proper error handling
- ✅ Database Validation: All database operations are validated
- ✅ Client-Server Sync: Data synchronization works correctly
- ✅ Integration Testing: Works with other QBCore resources
- ✅ Security Validation: No exploitable endpoints
- ✅ Documentation: Code is well-documented
Best Practices
Code Organization
- Modular Structure: Split functionality into logical files
- Clear Naming: Use descriptive function and variable names
- Error Handling: Always handle potential errors gracefully
- Performance: Optimize loops and database queries
- Security: Validate all user inputs and server events
Resource Integration
- Event Naming: Use consistent event naming patterns
- Data Validation: Always validate data before processing
- Cleanup: Properly clean up resources on player disconnect
- Configuration: Make features configurable when possible
- Backwards Compatibility: Consider version compatibility
Remember to thoroughly test your custom resource in a development environment before deploying to production servers.
Common Patterns
Interaction Systems
-- Using qb-target for interactions
exports['qb-target']:AddBoxZone("custom-resource-interaction", vector3(100.0, 200.0, 25.0), 2.0, 2.0, {
name = "custom-resource-interaction",
heading = 90.0,
debugPoly = Config.Debug,
minZ = 24.0,
maxZ = 26.0,
}, {
options = {
{
type = "client",
event = "your-resource:client:openMenu",
icon = "fas fa-hand",
label = "Open Custom Menu",
canInteract = function()
return HasRequiredJob()
end,
},
},
distance = 2.5
})
Menu Systems
-- Using qb-menu for interfaces
RegisterNetEvent('your-resource:client:openMenu', function()
local menuOptions = {
{
header = "Custom Resource Menu",
isMenuHeader = true,
},
{
header = "Option 1",
txt = "Description of option 1",
params = {
event = "your-resource:client:option1",
args = {type = "option1"}
}
},
{
header = "Option 2",
txt = "Description of option 2",
params = {
event = "your-resource:client:option2",
args = {type = "option2"}
}
}
}
exports['qb-menu']:openMenu(menuOptions)
end)
This guide provides a solid foundation for creating professional QBCore custom resources. Continue with specific implementation details based on your resource’s requirements.