FiveM Docs
Guides

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

AreaOld ESXESX Legacy
Shared objectTriggerEvent('esx:getSharedObject', cb)@es_extended/imports.lua or exports['es_extended']:getSharedObject()
Manifest__resource.luafxmanifest.lua with fx_version 'cerulean' and lua54 'yes'
Databasemysql-async (MySQL.Async.fetchAll)oxmysql (MySQL.query)
Player loopsESX.GetPlayers() plus ESX.GetPlayerFromIdESX.GetExtendedPlayers()
Base resourcesessentialmode, es_admin2, mapmanagernot allowed; ESX stops them
ServerOneSync optionalOneSync required

Step-by-step conversion

Replace __resource.lua with fxmanifest.lua

Old manifest:

__resource.lua
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:

fxmanifest.lua
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:

client/main.lua (old)
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:

client/main.lua
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:

server/main.lua
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:

server/main.lua (old)
MySQL.Async.fetchAll('SELECT * FROM owned_vehicles WHERE owner = @owner', {
    ['@owner'] = xPlayer.identifier
}, function(result)
    cb(result)
end)
server/main.lua
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

server/main.lua (old)
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
end
server/main.lua
local cops = ESX.GetExtendedPlayers('job', 'police')
for i = 1, #cops do
    cops[i].triggerEvent('myalert:notify', 'Robbery in progress')
end

ESX.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: removeAccountMoney and removeMoney return false when the balance would go negative, and true on success. Use the return value instead of reading the balance first.
  • Invalid amounts throw: addAccountMoney with 0, a negative number or NaN raises 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 without license:), and with a charN: 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 / setMeta and xPlayer.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:

EventSideArguments
esx:playerLoadedServerplayerId, xPlayer, isNew
esx:playerLoadedClientxPlayer (player data table)
esx:setJobServerplayerId, job, lastJob
esx:setJobClientjob, lastJob
esx:playerDroppedServerplayerId, reason
client/main.lua
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:

client/main.lua (old, exploitable)
TriggerServerEvent('myjob:pay', 500)
server/main.lua (old, exploitable)
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:

server/main.lua
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.CreateThread and Citizen.Wait still work, but CreateThread and Wait are the short forms.
  • GetPlayerPed(-1) on the client becomes PlayerPedId().
  • RegisterServerEvent plus AddEventHandler can become one RegisterNetEvent(name, handler) call.
  • ESX.UI.Menu still 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 from shared_scripts, or the resource starts before es_extended.
  • attempt to index a nil value (global 'MySQL'): @oxmysql/lib/MySQL.lua is missing from server_scripts.
  • Player data is nil right after join: wait for ESX.PlayerLoaded or the esx:playerLoaded event instead of reading data at file load.
  • Script works for admins only: old scripts often checked xPlayer.getGroup() == 'superadmin'. ESX Legacy converts superadmin to admin when it loads a player and in the /setgroup command, so check for admin or use xPlayer.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 of esx_policejob, esx_society and others. Do not mix an old copy with the Legacy addons.

Frequently asked questions

Independent documentation

esx-fivem.com is a community project and is not affiliated with the ESX Framework team. Facts on this page were checked against the upstream source. Official resources: docs.esx-framework.org and github.com/esx-framework.
Edit on GitHub

Last updated on

On this page