Convert Old ESX Scripts
How to convert ESX 1.1, 1.2 and v1-final scripts to ESX Legacy, from getSharedObject and fxmanifest to mysql-async, xPlayer changes and events.
To convert an ESX 1.1, 1.2 or v1-final script to ESX Legacy, replace the esx:getSharedObject event with the @es_extended/imports.lua import, move __resource.lua to an fxmanifest.lua with lua54 'yes', switch mysql-async calls to oxmysql, and move every money, item and permission decision to the server. Most xPlayer and ESX.* names are unchanged, so the rest is usually small fixes.
Overview
| Area | Old ESX | ESX Legacy |
|---|---|---|
| Shared object | TriggerEvent('esx:getSharedObject', cb) | @es_extended/imports.lua or exports['es_extended']:getSharedObject() |
| Manifest | __resource.lua | fxmanifest.lua with fx_version 'cerulean' and lua54 'yes' |
| Database | mysql-async (MySQL.Async.fetchAll) | oxmysql (MySQL.query) |
| Player loops | ESX.GetPlayers() plus ESX.GetPlayerFromId | ESX.GetExtendedPlayers() |
| Base resources | essentialmode, es_admin2, mapmanager | not allowed; ESX stops them |
| Server | OneSync optional | OneSync required |
Step-by-step conversion
Replace __resource.lua with fxmanifest.lua
Old manifest:
resource_manifest_version '44febabe-d386-4d18-afbe-5e627f4af937'
client_scripts {
'@es_extended/locale.lua',
'client/main.lua',
}
server_scripts {
'@mysql-async/lib/MySQL.lua',
'server/main.lua',
}
dependency 'es_extended'New manifest:
fx_version 'cerulean'
game 'gta5'
lua54 'yes'
shared_scripts {
'@es_extended/imports.lua',
'@es_extended/locale.lua',
'config.lua',
'locales/*.lua',
}
server_scripts {
'@oxmysql/lib/MySQL.lua',
'server/main.lua',
}
client_script 'client/main.lua'
dependencies {
'oxmysql',
'es_extended',
}lua54 'yes' turns on Lua 5.4. All ESX Legacy core resources use it, and Legacy code samples rely on Lua 5.4 features such as <const> locals.
Remove the esx:getSharedObject pattern
Old client code often looked like this:
ESX = nil
Citizen.CreateThread(function()
while ESX == nil do
TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
Citizen.Wait(0)
end
while ESX.GetPlayerData().job == nil do
Citizen.Wait(10)
end
PlayerData = ESX.GetPlayerData()
end)With @es_extended/imports.lua in the manifest, delete that block. The import sets the global ESX, keeps ESX.PlayerData updated and sets ESX.PlayerLoaded:
CreateThread(function()
while not ESX.PlayerLoaded do
Wait(200)
end
print('Job:', ESX.PlayerData.job.name)
end)On the server, also delete ESX = nil and the TriggerEvent('esx:getSharedObject', ...) line. If you cannot add the import, use the export directly:
ESX = exports['es_extended']:getSharedObject()es_extended still answers the esx:getSharedObject event for backwards compatibility, so unconverted scripts do not break right away. Treat it as deprecated and convert when you touch a script.
Convert mysql-async to oxmysql
Replace the include in the manifest (done above) and rename the calls:
MySQL.Async.fetchAll('SELECT * FROM owned_vehicles WHERE owner = @owner', {
['@owner'] = xPlayer.identifier
}, function(result)
cb(result)
end)local vehicles = MySQL.query.await(
'SELECT `plate`, `vehicle`, `stored` FROM `owned_vehicles` WHERE `owner` = ?',
{ xPlayer.getIdentifier() }
)
cb(vehicles)oxmysql still accepts MySQL.Async.* and MySQL.Sync.* as aliases, so you can migrate file by file. The full mapping table is in Using oxmysql.
Replace player loops
local xPlayers = ESX.GetPlayers()
for i = 1, #xPlayers do
local xPlayer = ESX.GetPlayerFromId(xPlayers[i])
if xPlayer.job.name == 'police' then
TriggerClientEvent('myalert:notify', xPlayers[i], 'Robbery in progress')
end
endlocal cops = ESX.GetExtendedPlayers('job', 'police')
for i = 1, #cops do
cops[i].triggerEvent('myalert:notify', 'Robbery in progress')
endESX.GetExtendedPlayers(key, value) filters loaded players on the server. With 'job' it compares xPlayer.job.name. To count players in a job, ESX.GetNumPlayers('job', 'police') uses a cached counter.
Check xPlayer calls
The common methods are unchanged: getMoney, addMoney, removeMoney, getAccount, addAccountMoney, removeAccountMoney, getInventoryItem, addInventoryItem, removeInventoryItem, getJob, setJob, getGroup, setGroup, showNotification, triggerEvent. Points to check:
- Return values:
removeAccountMoneyandremoveMoneyreturnfalsewhen the balance would go negative, andtrueon success. Use the return value instead of reading the balance first. - Invalid amounts throw:
addAccountMoneywith0, a negative number orNaNraises a Lua error in the default inventory mode. Validate amounts before you call it. - Identifiers: use
xPlayer.getIdentifier(). ESX Legacy stores identifiers without the type prefix (for example the license hash withoutlicense:), and with acharN:prefix when multicharacter is used. If an old database stores prefixed identifiers, those rows will not match. - New helpers:
xPlayer.hasItem(name),xPlayer.canCarryItem(name, count),xPlayer.getMeta/setMetaandxPlayer.isAdmin()exist in Legacy and replace many hand-written checks.
See the full list in the xPlayer reference.
Update event handlers
The core events keep their names. Check the arguments:
| Event | Side | Arguments |
|---|---|---|
esx:playerLoaded | Server | playerId, xPlayer, isNew |
esx:playerLoaded | Client | xPlayer (player data table) |
esx:setJob | Server | playerId, job, lastJob |
esx:setJob | Client | job, lastJob |
esx:playerDropped | Server | playerId, reason |
RegisterNetEvent('esx:setJob', function(job, lastJob)
if job.name == 'police' then
-- start police features
end
end)The import already keeps ESX.PlayerData.job current, so you only need the handler to react to changes.
Move trust to the server
Many old scripts did the important part on the client and then told the server what happened:
TriggerServerEvent('myjob:pay', 500)RegisterServerEvent('myjob:pay')
AddEventHandler('myjob:pay', function(amount)
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.addMoney(amount)
end)Any cheat menu can trigger that event with any amount. In the converted version the client only reports that it finished, and the server decides:
local PAYOUT <const> = 500
local DELIVERY_POINT <const> = vector3(-424.5, -1683.4, 19.0)
local lastPaid = {}
RegisterNetEvent('myjob:finishDelivery', function()
local src = source
local xPlayer = ESX.GetPlayerFromId(src)
if not xPlayer or xPlayer.getJob().name ~= 'trucker' then return end
if #(GetEntityCoords(GetPlayerPed(src)) - DELIVERY_POINT) > 10.0 then return end
local now = os.time()
if lastPaid[src] and now - lastPaid[src] < 60 then return end
lastPaid[src] = now
xPlayer.addMoney(PAYOUT, 'Delivery payout')
end)
AddEventHandler('playerDropped', function()
lastPaid[source] = nil
end)Server-side position checks work because ESX Legacy requires OneSync. For request/response logic, use callbacks.
Clean up deprecated natives and helpers
Citizen.CreateThreadandCitizen.Waitstill work, butCreateThreadandWaitare the short forms.GetPlayerPed(-1)on the client becomesPlayerPedId().RegisterServerEventplusAddEventHandlercan become oneRegisterNetEvent(name, handler)call.ESX.UI.Menustill exists, but newer core resources use esx_context. Keep the old menus if they work, and switch when you rewrite the UI.
Test on a clean ESX Legacy server
Start the converted script on a test server and watch the server console and F8 for errors. Test every path that changes money, items or jobs, including invalid input.
Resources ESX Legacy refuses to run
es_extended stops these resources if they start, and prints an error: essentialmode, es_admin2, basic-gamemode, mapmanager, fivem-map-skater, fivem-map-hipster, qb-core and default_spawnpoint. Old ESX servers often still have essentialmode and es_admin2. Remove them, and replace their admin features with the ESX commands described in Admin permissions.
es_extended also requires FiveM server artifact 10188 or newer and prints an error every minute on older builds.
Common issues
attempt to index a nil value (global 'ESX'): the import is missing fromshared_scripts, or the resource starts beforees_extended.attempt to index a nil value (global 'MySQL'):@oxmysql/lib/MySQL.luais missing fromserver_scripts.- Player data is
nilright after join: wait forESX.PlayerLoadedor theesx:playerLoadedevent instead of reading data at file load. - Script works for admins only: old scripts often checked
xPlayer.getGroup() == 'superadmin'. ESX Legacy convertssuperadmintoadminwhen it loads a player and in the/setgroupcommand, so check foradminor usexPlayer.isAdmin(). - Items do not exist: old scripts often shipped SQL that inserted items. Run that SQL, or with ox_inventory add the items to
data/items.lua. See Add items. - Old
esx_*addon conflicts with the bundled one: ESX Legacy ships its own versions ofesx_policejob,esx_societyand others. Do not mix an old copy with the Legacy addons.
Related
Frequently asked questions
Independent documentation
Last updated on
Using ox_inventory
How to run ox_inventory with ESX Legacy, covering start order, convars, how es_extended switches to ox mode, which xPlayer methods change, and pitfalls.
ESX vs QBCore
A neutral comparison of ESX Legacy and QBCore for FiveM servers covering architecture, data storage, API style, ecosystem, learning curve and migration.