FixFX

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
{
  "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

  1. Separate Concerns: Keep client, server, and shared code separate
  2. Use Callbacks: For data requests between client and server
  3. Validate Everything: Never trust client input
  4. Handle Errors: Use pcall for database operations
  5. Performance: Avoid unnecessary loops and timers

Security

  1. Server Validation: Always validate on the server
  2. Permission Checks: Verify job/permissions before actions
  3. Rate Limiting: Prevent spam/abuse
  4. Secure Events: Use source validation

Performance

  1. Efficient Queries: Use proper database indexes
  2. Caching: Cache frequently accessed data
  3. Resource Cleanup: Clean up on resource stop
  4. Minimal UI: Keep NUI lightweight

This comprehensive development guide provides everything needed to create professional QBCore resources following best practices and framework conventions.