Convert an ESX script to QBCore (and back): function map and bridge

Port a FiveM script between ESX and QBCore: a table of the common calls (player, money, job, items, notifications, callbacks) and a bridge file that supports both.

You have a script written for ESX, and your server runs QBCore, or the other way round. Most of the script is plain Lua and natives that do not change. Only the framework calls do, and there are fewer than you expect. This article gives you a map of them, and then shows how to isolate them in one bridge file.

What stays and what changes

Natives, NUI, your own events, loops, targets and menus work on any framework. What changes is a short list:

  • how you get the framework object
  • how you get a player
  • money, jobs and items
  • notifications
  • usable items and callbacks
  • the database tables for players

Search the script for ESX. and xPlayer, and every hit is something to convert.

Function map

Task ESX Legacy QBCore
Get the object ESX = exports['es_extended']:getSharedObject() local QBCore = exports['qb-core']:GetCoreObject()
Get a player (server) ESX.GetPlayerFromId(src) QBCore.Functions.GetPlayer(src)
Player identifier xPlayer.identifier Player.PlayerData.citizenid
Cash balance xPlayer.getMoney() Player.PlayerData.money.cash
Add cash xPlayer.addMoney(n) Player.Functions.AddMoney('cash', n)
Remove cash xPlayer.removeMoney(n) Player.Functions.RemoveMoney('cash', n)
Bank add xPlayer.addAccountMoney('bank', n) Player.Functions.AddMoney('bank', n)
Bank remove xPlayer.removeAccountMoney('bank', n) Player.Functions.RemoveMoney('bank', n)
Job name xPlayer.job.name Player.PlayerData.job.name
Job grade xPlayer.job.grade Player.PlayerData.job.grade.level
Add item xPlayer.addInventoryItem(name, n) Player.Functions.AddItem(name, n)
Remove item xPlayer.removeInventoryItem(name, n) Player.Functions.RemoveItem(name, n)
Item count xPlayer.getInventoryItem(name).count Player.Functions.GetItemByName(name) (nil if none, then .amount)
Usable item ESX.RegisterUsableItem(name, function(src) end) QBCore.Functions.CreateUseableItem(name, function(src, item) end)
Notify (client) ESX.ShowNotification(msg) QBCore.Functions.Notify(msg, 'success')
Notify (server) xPlayer.showNotification(msg) TriggerClientEvent('QBCore:Notify', src, msg, 'success')
Player data (client) ESX.GetPlayerData() QBCore.Functions.GetPlayerData()
Player loaded event esx:playerLoaded QBCore:Client:OnPlayerLoaded
Job changed event esx:setJob QBCore:Client:OnJobUpdate
Server callback ESX.RegisterServerCallback QBCore.Functions.CreateCallback

Two details trip people up. Money types: ESX has money for cash and bank, QBCore uses 'cash' and 'bank' as the first argument. And the grade: ESX gives a plain number, QBCore a table, so job.grade.level.

Callbacks, which every second script uses, are compared in detail in server callbacks on ESX, QBCore and ox_lib.

The database

The player tables differ, so any SELECT or UPDATE on them needs a change:

ESX QBCore
Players users players
Player key identifier (a license: string) citizenid
Vehicles owned_vehicles player_vehicles

A script that stores its own data, with its own tables, usually keeps working, as long as it keys the rows on a value you can get from either framework. Using the license identifier for that works on both.

QBox

QBox is closer to QBCore than to ESX. Its core is qbx_core, it keeps a compatibility layer for exports['qb-core']:GetCoreObject(), and its inventory is ox_inventory. A script converted to QBCore usually runs on QBox with few changes, apart from inventory calls.

The bridge file approach

If the script must run on both, do not litter it with if ESX then ... else ... end. Put every framework call in one file, and have the rest of the script call that file.

lua
-- bridge.lua (server)
Bridge = {}

local framework
if GetResourceState('es_extended') == 'started' then
    framework = 'esx'
    ESX = exports['es_extended']:getSharedObject()
elseif GetResourceState('qb-core') == 'started' then
    framework = 'qb'
    QBCore = exports['qb-core']:GetCoreObject()
end

function Bridge.GetPlayer(src)
    if framework == 'esx' then return ESX.GetPlayerFromId(src) end
    return QBCore.Functions.GetPlayer(src)
end

function Bridge.AddCash(src, amount)
    local player = Bridge.GetPlayer(src)
    if not player then return false end

    if framework == 'esx' then
        player.addMoney(amount)
    else
        player.Functions.AddMoney('cash', amount)
    end
    return true
end

function Bridge.GetJob(src)
    local player = Bridge.GetPlayer(src)
    if not player then return nil end

    if framework == 'esx' then
        return player.job.name, player.job.grade
    end
    return player.PlayerData.job.name, player.PlayerData.job.grade.level
end

In the manifest, load it before the files that use it:

lua
server_scripts {
    'bridge.lua',
    'server.lua',
}

The script then reads like this, and never mentions a framework:

lua
RegisterNetEvent('my_script:pay', function()
    local src = source
    local job = Bridge.GetJob(src)
    if job == 'mechanic' then
        Bridge.AddCash(src, 100)
    end
end)

Add a client bridge for notifications and player data in the same way. When you add a new framework, such as QBox with qbx_core, you add a branch in the bridge, and the script stays as it is.

Remember that a bridge only works if the framework starts first. Put ensure es_extended or ensure qb-core above the script in server.cfg, and read ESX is nil if the object comes back nil.

Checklist

Symptom Fix
attempt to index a nil value (global 'ESX') on QBCore An ESX call is left; replace it from the map above
Money or job always nil You read PlayerData on ESX, or xPlayer.job on QBCore
Item count errors on QBCore GetItemByName returns nil when the player has none; check it first
Wrong table in a query users and identifier on ESX, players and citizenid on QBCore
Callback returns nothing Use the matching register and trigger calls for the framework
Needs both frameworks Move every framework call into a bridge file

Quick answers

Can I convert any ESX script to QBCore?

Scripts that only use the framework for players, money, jobs, items and notifications convert well. Scripts built around framework-specific resources, such as esx_society or qb-management, need those parts rewritten too.

Does the database change when I switch framework?

Yes. ESX stores players in users with an identifier, and QBCore in players with a citizenid. Queries and table names in the script must follow the framework you run.

Is a bridge file better than converting?

If you need the script on both frameworks, yes: you change one file, not the whole script. If you only run one framework, convert once and drop the other branch.

Scripts that skip this problem

Item Creator V2Create usable items with animations, props, effects and more β€” without writing code.View script β†’Shop CreatorBuild a shop in under a minute β€” owners, employees, vaults and robberies included.View script β†’Advanced BoostingTablet-driven vehicle boosting: contracts from class D to S+, crews and a live queue.View script β†’

Keep reading