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.
-- 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
endIn the manifest, load it before the files that use it:
server_scripts {
'bridge.lua',
'server.lua',
}The script then reads like this, and never mentions a framework:
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 β