# Convert Old ESX Scripts (/docs/guides/convert-old-esx-scripts)

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 [#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 [#step-by-step-conversion]

<Steps>
<Step>
### Replace `__resource.lua` with `fxmanifest.lua` [#replace-__resourcelua-with-fxmanifestlua]

Old manifest:

```lua title="__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:

```lua title="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.
</Step>
<Step>
### Remove the `esx:getSharedObject` pattern [#remove-the-esxgetsharedobject-pattern]

Old client code often looked like this:

```lua title="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`:

```lua title="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:

```lua title="server/main.lua"
ESX = exports['es_extended']:getSharedObject()
```

<Callout type="info">
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.
</Callout>
</Step>
<Step>
### Convert mysql-async to oxmysql [#convert-mysql-async-to-oxmysql]

Replace the include in the manifest (done above) and rename the calls:

```lua title="server/main.lua (old)"
MySQL.Async.fetchAll('SELECT * FROM owned_vehicles WHERE owner = @owner', {
    ['@owner'] = xPlayer.identifier
}, function(result)
    cb(result)
end)
```

```lua title="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](/docs/guides/oxmysql).
</Step>
<Step>
### Replace player loops [#replace-player-loops]

```lua title="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
```

```lua title="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.
</Step>
<Step>
### Check xPlayer calls [#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](/docs/es_extended/server/xplayer).
</Step>
<Step>
### Update event handlers [#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` |

```lua title="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.
</Step>
<Step>
### Move trust to the server [#move-trust-to-the-server]

Many old scripts did the important part on the client and then told the server what happened:

```lua title="client/main.lua (old, exploitable)"
TriggerServerEvent('myjob:pay', 500)
```

```lua title="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:

```lua title="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](/docs/guides/callbacks).
</Step>
<Step>
### Clean up deprecated natives and helpers [#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](/docs/core/esx_context). Keep the old menus if they work, and switch when you rewrite the UI.
</Step>
<Step>
### Test on a clean ESX Legacy server [#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.
</Step>
</Steps>

## Resources ESX Legacy refuses to run [#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](/docs/guides/admin-permissions).

es_extended also requires FiveM server artifact 10188 or newer and prints an error every minute on older builds.

## Common issues [#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](/docs/guides/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.

## Related [#related]

- [Using oxmysql](/docs/guides/oxmysql)
- [Callbacks](/docs/guides/callbacks)
- [Your first resource](/docs/getting-started/first-resource)
- [Updating ESX](/docs/getting-started/updating)
- [Troubleshooting](/docs/getting-started/troubleshooting)
