# esx_accessories (/docs/addons/esx_accessories) esx_accessories adds shop markers where players buy masks, helmets, glasses and ear accessories, and a personal menu (default key `J`) to put them on and take them off again. Purchases are stored per character in [esx_datastore](/docs/addons/esx_datastore), so the accessory survives relogs. ## Overview [#overview] | | | |---|---| | Resource | `esx_accessories` | | Side | Client + server | | Dependencies | `esx_lib`, `es_extended`, `esx_skin`, `esx_datastore`, `oxmysql` (loaded via `@oxmysql/lib/MySQL.lua`) | | Database | Four rows in the `datastore` table | | UI | `ESX.OpenContext` menus, `esx_skin` restricted skin menu | In-game flow: 1. The player walks into a shop marker (`Config.Zones`) and presses `E`. 2. `esx_skin` opens a restricted menu that only shows the `_1` and `_2` sliders of that accessory. 3. On confirmation the server charges `Config.Price` from cash and stores the chosen drawable and texture. 4. Later the player presses `J`, picks Helmet, Ears, Mask or Glasses and toggles the saved item on or off. ## Installation [#installation] ### Import the SQL [#import-the-sql] Run `esx_accessories.sql` against your ESX database. It inserts the four datastores the script reads and writes: ```sql title="esx_accessories.sql" INSERT INTO `datastore` (name, label, shared) VALUES ('user_ears', 'Ears', 0), ('user_glasses', 'Glasses', 0), ('user_helmet', 'Helmet', 0), ('user_mask', 'Mask', 0) ; ``` Translated labels are in the `localization/` folder (de, fr, it, nl, sl, sr, sv, tr). Import only one of them. ### Ensure the resource [#ensure-the-resource] Start it after its dependencies: ```bash title="server.cfg" ensure oxmysql ensure es_extended ensure esx_lib ensure skinchanger ensure esx_skin ensure esx_datastore ensure esx_accessories ``` ## Configuration [#configuration] All keys live in `config.lua`. | Key | Default | Description | |---|---|---| | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale used for menus and notifications. | | `Config.Price` | `100` | Cash price of one accessory. Charged with `xPlayer.removeMoney`. | | `Config.ShopDistance` | `3.0` | Server-side maximum distance (meters) to a shop position for buying and saving. | | `Config.PurchaseSessionDuration` | `60000` | Milliseconds after payment during which the server accepts the save event. | | `Config.EnableControls` | `true` | Registers the `accessory` key mapping (default `J`) that opens the accessory menu. | | `Config.DrawDistance` | `10` | Distance at which shop markers are drawn. | | `Config.Size` | `{x = 1.5, y = 1.5, z = 1.0}` | Marker size. `Size.x` is also the interaction radius. | | `Config.Color` | `{r = 50, g = 200, b = 50}` | Marker color (alpha is fixed to 255). | | `Config.Type` | `1` | Marker type. Set to `-1` to create no markers at all. | | `Config.ShopsBlips` | Only `Mask` at `vector3(-1338.1, -1278.2, 3.8)` | Map blips per accessory type: `Pos` (list of `vector3`) and `Blip = {sprite, color}`. | | `Config.Zones` | `Ears`, `Mask`, `Helmet`, `Glasses` | Shop positions per accessory type, each `{ Pos = { vector3, ... } }`. | The keys of `Config.Zones` must be `Ears`, `Mask`, `Helmet` or `Glasses`. The server normalizes the accessory name against that list and rejects anything else, so a custom key like `Hats` will never be purchasable. Setting `Config.Type = -1` skips the whole marker thread. Because the markers also handle enter and exit detection, the shops then stop working. Use a very small `Config.Size` or a transparent color if you only want to hide the visuals. The default `Config.ShopsBlips` only adds a blip for the mask shop because the other zones sit inside clothing stores that [esx_clotheshop](/docs/addons/esx_clotheshop) already marks on the map. ## Server events [#server-events] ### esx_accessories:pay [#esx_accessoriespay] ```lua TriggerServerEvent('esx_accessories:pay', accessory) ``` | Name | Type | Description | |---|---|---| | `accessory` | `string` | `'Ears'`, `'Mask'`, `'Helmet'` or `'Glasses'` (case-insensitive). | Charges `Config.Price` and opens a purchase session for that accessory. The server checks that the player exists, stands near a matching zone and has enough cash. The built-in client uses the `esx_accessories:buy` callback instead, which runs the same logic and returns the result. Any client can fire this event. It is safe because the server validates position and balance, but it only removes money; nothing is granted until `esx_accessories:save` follows. ### esx_accessories:save [#esx_accessoriessave] ```lua TriggerServerEvent('esx_accessories:save', skin, accessory) ``` | Name | Type | Description | |---|---|---| | `skin` | `table` | Skin table from `skinchanger:getSkin`. Only `_1` and `_2` are stored. | | `accessory` | `string` | Accessory type, same values as above. | Writes `has = true` and `skin = { _1, _2 }` into the `user_` datastore. The server rejects the call unless there is an unexpired paid session for that accessory and the player is still near the shop. The session is consumed on success. ## Callbacks [#callbacks] Callbacks are registered with `xLib.callback.registerCompat`, so both `xLib.callback` and `ESX.TriggerServerCallback` can call them. | Name | Arguments | Returns | |---|---|---| | `esx_accessories:get` | `accessory` | `hasAccessory` (`boolean`), `skin` (`table` with `_1` and `_2`) | | `esx_accessories:buy` | `accessory` | `true` if payment succeeded and a session was opened | | `esx_accessories:checkMoney` | `accessory` | `true` if the player is near the shop and has at least `Config.Price` cash | Example: check whether the local player owns a helmet before offering it in your own menu. ```lua title="client/main.lua" xLib.callback('esx_accessories:get', false, function(hasHelmet, helmetSkin) if not hasHelmet then return ESX.ShowNotification('You do not own a helmet yet.') end TriggerEvent('skinchanger:getSkin', function(skin) TriggerEvent('skinchanger:loadClothes', skin, helmetSkin) end) end, 'Helmet') ``` ## Client events [#client-events] These are local events (not network events) fired by the marker zones. You can listen to them, for example to show your own help text. | Event | Argument | Fired when | |---|---|---| | `esx_accessories:hasEnteredMarker` | `zone` (`string`, e.g. `'Mask'`) | The player enters a shop marker. | | `esx_accessories:hasExitedMarker` | `zone` | The player leaves a shop marker. The open context menu is closed. | ## Reading the stored data from another resource [#reading-the-stored-data-from-another-resource] The data is a normal private datastore, so server scripts can read it through esx_datastore: ```lua title="server/main.lua" local xPlayer = ESX.GetPlayerFromId(source) TriggerEvent('esx_datastore:getDataStore', 'user_mask', xPlayer.getIdentifier(), function(store) local owned = store.get('hasMask') or false local skin = store.get('skin') or {} print(('Owns mask: %s, drawable %s'):format(owned, skin.mask_1)) end) ``` ## Common issues [#common-issues] - **"No mask/helmet" notification although the item was bought.** The datastore rows from the SQL file are missing, or esx_datastore started after the purchase. Check that `user_mask` and the others exist in the `datastore` table. - **Purchase charged but nothing saved.** The player walked out of `Config.ShopDistance` or took longer than `Config.PurchaseSessionDuration` in the confirmation menu. The money is not refunded in that case. - **Shop does nothing when pressing E.** You changed `Config.Type` to `-1` or renamed a zone key. See the configuration notes above. - **Masks taken off show a blank head.** Mask "off" sets drawable `0`; props (helmet, glasses, ears) use `-1`. Custom clothing packs may need other values. ## Related [#related] - [esx_datastore](/docs/addons/esx_datastore) - [esx_skin](/docs/core/esx_skin) - [skinchanger](/docs/core/skinchanger) - [esx_clotheshop](/docs/addons/esx_clotheshop) - [esx_accessorymanager](/docs/addons/esx_accessorymanager) # esx_accessorymanager (/docs/addons/esx_accessorymanager) esx_accessorymanager is a client-only radial menu that lets players take off and put back the clothing and props they are currently wearing: hat, glasses, mask, watch, top, pants, shoes and more. Use it when you want a quick wardrobe toggle (for example "take off mask") without building outfits or shops. This addon is not covered on docs.esx-framework.org. Everything below is taken from the resource source. ## Overview [#overview] | | | |---|---| | Resource | `esx_accessorymanager` | | Side | Client only (NUI) | | Dependencies | `esx_lib`, `es_extended`, `skinchanger` | | Database | None | | Open with | `F11` key mapping or `/accessories` | | Supported peds | `mp_m_freemode_01` and `mp_f_freemode_01` only | The radial menu has 13 sectors: watch, glasses, mask, necklace, earrings, pants, shoes, top (with arms), hat, bag, undershirt, bracelet and vest. The center button restores everything removed by this resource. Two extra tools are available: - **Fix accessories** reapplies all current props from the skin, useful when a hat or glasses visually disappear. - **Fix hair** toggles the hairstyle to drawable `0` (for example to hide hair under a helmet) and back to the saved hairstyle and colors. Removing a vest only hides the drawable. It does not change gameplay armor. The menu refuses to open (or closes itself) while the player is dead, ragdolling, falling, cuffed, in the pause menu, has `LocalPlayer.state.invBusy` set, already has NUI focus, or sits in a vehicle (unless allowed). ## Installation [#installation] No SQL file. Start it after its dependencies: ```bash title="server.cfg" ensure esx_lib ensure es_extended ensure skinchanger ensure esx_accessorymanager ``` The NUI ships prebuilt in `html/`, there is no build step. ## Configuration [#configuration] All keys live in `config.lua`. | Key | Default | Description | |---|---|---| | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale for labels. English and Spanish are included; missing keys fall back to English. | | `Config.EnableControls` | `true` | Registers the `accessorymanager` key mapping. The `/accessories` command exists either way. | | `Config.Radial.Command` | `'accessories'` | Chat command that toggles the menu. | | `Config.Radial.DefaultKey` | `'F11'` | Default key of the key mapping. Players can rebind it in the FiveM settings. | | `Config.Radial.ActionCooldown` | `350` | Minimum milliseconds between two wardrobe actions. | | `Config.Radial.AllowInVehicle` | `false` | Allow opening the menu inside a vehicle. | | `Config.Radial.Undressed.male` | `{ tshirt = 15, torso = 15, arms = 15, pants = 61, shoes = 34 }` | Drawables used when a male freemode ped takes off these items. | | `Config.Radial.Undressed.female` | `{ tshirt = 15, torso = 15, arms = 15, pants = 15, shoes = 35 }` | Same for the female freemode ped. | | `Config.Radial.Animation.Enabled` | `true` | Play a clothing animation before each change. | | `Config.Radial.Animation.Timeout` | `500` | Milliseconds to wait for the animation dictionary to load before skipping the animation. | | `Config.Radial.Animation.Duration` | `900` | Fallback animation length in milliseconds. | | `Config.Radial.Animation.Flag` | `49` | Fallback animation flag. | | `Config.Radial.Animation.Default` | `{ dict = 'clothingshirt', anim = 'try_shirt_positive_d' }` | Animation for categories without an own entry (`tshirt`). | | `Config.Radial.Animation.ByCategory` | per category | Per category `{ dict, anim, flag, duration }`. `helmet` uses separate `on` and `off` clips. `repairHair` is used by the Fix hair button. | Items other than tops, undershirts, pants and shoes are removed by setting drawable `0` (components) or clearing the prop (`-1`). If you run a custom clothing pack where `15`, `61` or `34` are real clothes, adjust `Undressed`. ## Client exports [#client-exports] ### OpenAccessoryMenu [#openaccessorymenu] ```lua exports.esx_accessorymanager:OpenAccessoryMenu() ``` Opens the radial menu. If it is already open, the call closes it (toggle). Does nothing when the player cannot use the menu right now. ### CloseAccessoryMenu [#closeaccessorymenu] ```lua exports.esx_accessorymanager:CloseAccessoryMenu() ``` Closes the menu and releases NUI focus. ### InvalidateWardrobe [#invalidatewardrobe] ```lua exports.esx_accessorymanager:InvalidateWardrobe() ``` Forgets all items removed by the menu and the Fix hair state. Call it after your own script changes the outfit without going through `skinchanger:loadSkin` or `skinchanger:loadClothes` (those two events already invalidate automatically). Example: open the wardrobe from a radial or target menu of another resource. ```lua title="client/main.lua" RegisterCommand('wardrobe', function() if GetResourceState('esx_accessorymanager') ~= 'started' then return end exports.esx_accessorymanager:OpenAccessoryMenu() end, false) ``` ## Client events [#client-events] | Event | Type | Description | |---|---|---| | `esx_accessorymanager:openMenu` | Local event | Same as the `OpenAccessoryMenu` export. Use `TriggerEvent`, it is not a network event. | The resource listens to `esx:playerLoaded` and `esx:onPlayerLogout` (clears state), `esx:onPlayerDeath` (closes the menu) and the skinchanger load events. When the resource stops, it restores every removed item if `skinchanger` is still running. ## Security notes [#security-notes] The NUI only sends an action id (max 32 characters) from a fixed allowlist. Drawables, textures and models are never taken from the UI; every value is validated against the active ped with `IsPedComponentVariationValid` and the prop variation natives. There are no server events, so the resource cannot be abused to give items or money. ## Integration tips [#integration-tips] - **esx_accessories:** both can run together. esx_accessories sells and saves items; esx_accessorymanager only toggles what is worn. - **Inventories:** set `LocalPlayer.state.invBusy = true` while your inventory is open to block the radial menu. - **Custom skin scripts:** if you apply clothes without skinchanger, call `InvalidateWardrobe` afterwards so "restore" does not bring back old items. ## Common issues [#common-issues] - **"This character model is not supported."** The player uses an addon or story ped. Only the two freemode models are supported. - **Item greyed out.** The item is already in its "off" state, or the off values from `Undressed` are not valid for the ped. - **F11 conflicts with another script.** Change `Config.Radial.DefaultKey`. Existing players keep their old binding until they rebind it, because FiveM stores key mappings per client. ## Related [#related] - [esx_accessories](/docs/addons/esx_accessories) - [skinchanger](/docs/core/skinchanger) - [esx_skin](/docs/core/esx_skin) - [esx_lib](/docs/core/esx_lib) # esx_addonaccount (/docs/addons/esx_addonaccount) esx_addonaccount is a server-only library resource that stores money outside of the normal player accounts. Use it whenever money belongs to a job, a business or a property instead of a player's wallet, for example society funds used by [esx_society](/docs/addons/esx_society) and [esx_billing](/docs/addons/esx_billing). ## Overview [#overview] | | | | --- | --- | | Resource | `esx_addonaccount` (version `1.1`) | | Side | Server only | | Dependencies | `esx_lib`, `es_extended`, `oxmysql` | | SQL file | `esx_addonaccount.sql` | | Exports | `GetAccount`, `GetSharedAccount`, `AddSharedAccount` | There are two kinds of accounts, controlled by the `shared` column of the `addon_account` table: - **Shared** (`shared = 1`): a single account without an owner. Typical for `society_` accounts. - **Personal** (`shared = 0`): one account per player identifier. The resource creates the row automatically when a player loads (`esx:playerLoaded`) and stores the loaded accounts on the player as `xPlayer.get('addonAccounts')`. ## Installation [#installation] ### Import the SQL [#import-the-sql] Run `esx_addonaccount.sql` against your ESX database. It creates `addon_account` (the definitions) and `addon_account_data` (the balances). Both tables must use InnoDB. ### Ensure the resource [#ensure-the-resource] Start it after `es_extended` and before every resource that uses it: ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ensure esx_addonaccount ensure esx_society ensure esx_billing ``` ### Define your accounts [#define-your-accounts] Insert one row per account. Shared accounts get their balance row on startup automatically. ```sql INSERT INTO `addon_account` (`name`, `label`, `shared`) VALUES ('society_mechanic', 'Mechanic', 1), ('property_black_money', 'Black money (property)', 0); ``` On startup the resource runs a schema migration. It adds a generated `owner_key` column and a unique key on `(account_name, owner_key)` if they are missing. The migration aborts (and disables all accounts) instead of changing data when it finds problems: - `addon_account_data` is not InnoDB - rows with `account_name IS NULL` or `owner = ''` - duplicate rows for the same account and owner ## Configuration [#configuration] esx_addonaccount has no `config.lua`. Everything is driven by the `addon_account` table. | Column | Type | Description | | --- | --- | --- | | `name` | `VARCHAR(60)` | Unique account name, e.g. `society_police` | | `label` | `VARCHAR(100)` | Display label (stored, not used by this resource) | | `shared` | `INT` | `1` = shared account, `0` = one account per player | ## Server exports [#server-exports] ### GetSharedAccount [#getsharedaccount] ```lua exports.esx_addonaccount:GetSharedAccount(name) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Account name from `addon_account` | Returns the [account object](#account-object) or `nil` if the account does not exist or the resource is not ready yet. ```lua title="server/main.lua" local account = exports.esx_addonaccount:GetSharedAccount('society_mechanic') if account and account.addMoney(2500) then print(('Mechanic society now has $%s'):format(account.money)) end ``` ### GetAccount [#getaccount] ```lua exports.esx_addonaccount:GetAccount(name, owner) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Name of a non-shared account | | `owner` | `string` | Player identifier (`xPlayer.identifier`) | Returns the personal account object or `nil`. Accounts are only kept in memory for loaded players (offline accounts are evicted about once a minute after they are saved), so call this for online players. ```lua title="server/main.lua" local xPlayer = ESX.GetPlayerFromId(source) local stash = exports.esx_addonaccount:GetAccount('property_black_money', xPlayer.identifier) if stash then stash.removeMoney(1000) end ``` ### AddSharedAccount [#addsharedaccount] ```lua exports.esx_addonaccount:AddSharedAccount(society, amount) ``` | Name | Type | Description | | --- | --- | --- | | `society` | `table` | `{ name = string, label = string }` | | `amount` | `number?` | Starting balance, defaults to `0` | Creates a shared account at runtime (inserts into both tables) and returns it. If the account already exists, the existing object is returned and nothing is inserted. Returns `nil` on invalid input or a failed insert. ```lua title="server/main.lua" exports.esx_addonaccount:AddSharedAccount({ name = 'society_bahama', label = 'Bahama Mamas' }, 10000) ``` ## Account object [#account-object] | Field / method | Description | | --- | --- | | `name` | Account name | | `owner` | Identifier, or `nil` for shared accounts | | `money` | Current balance | | `addMoney(amount)` | Adds money. Returns `true` or `false` | | `removeMoney(amount)` | Removes money. Returns `false` if the balance would drop below 0 | | `setMoney(amount)` | Sets the balance. Returns `true` or `false` | | `save(cb?)` | Forces a write. `cb(success)` runs once the value is persisted | | `isDirty()` | `true` while a change has not been written yet | | `withPersistence(action)` | Runs `action(money)` while holding the persistence lock. Return `success, delta` to apply a change atomically | `amount` must be a whole number between `0` and `2147483647`. Floats, negative numbers and `NaN` are rejected and the method returns `false`. Every successful change is saved to the database right away (with automatic retry every 5 seconds on failure). ## Events [#events] All events are local server events. Clients cannot trigger them. | Event | Arguments | When | | --- | --- | --- | | `esx_addonaccount:getSharedAccount` | `name, cb` | Legacy getter. `cb(account)` | | `esx_addonaccount:getAccount` | `name, owner, cb` | Legacy getter. `cb(account)` | | `esx_addonaccount:addMoney` | `name, amount, owner` | Fired after `addMoney` succeeded | | `esx_addonaccount:removeMoney` | `name, amount, owner` | Fired after `removeMoney` succeeded | | `esx_addonaccount:setMoney` | `name, amount, owner` | Fired after `setMoney` succeeded | | `esx_addonaccount:refreshAccounts` | none | Reloads account definitions from the database | Listening for changes is useful for logging: ```lua title="server/logs.lua" AddEventHandler('esx_addonaccount:removeMoney', function(name, amount, owner) if name:find('^society_') then print(('[audit] %s lost $%s'):format(name, amount)) end end) ``` The getter events still work, but the exports are simpler and avoid callback nesting: ```lua TriggerEvent('esx_addonaccount:getSharedAccount', 'society_police', function(account) if account then account.addMoney(500) end end) ``` ## Integration tips [#integration-tips] - Society accounts follow the `society_` pattern. [esx_society](/docs/addons/esx_society) and [esx_billing](/docs/addons/esx_billing) rely on that naming. - Store item stashes in [esx_addoninventory](/docs/addons/esx_addoninventory) and generic key/value data in [esx_datastore](/docs/addons/esx_datastore). - New rows in `addon_account` are picked up without a restart when ESX fires `esx:sqlCatalogReady` for that table, or when you trigger `esx_addonaccount:refreshAccounts` on the server. ## Common issues [#common-issues] - **`addMoney` returns `false`**: you passed a float (for example a percentage cut like `price * 0.15`). Round first with `math.floor`. - **Console says "Duplicate addon accounts" or "Invalid account_name/empty owner"**: the migration stopped on purpose. Clean up the listed rows in `addon_account_data` by hand, then restart the resource. - **Account is `nil` right after a server start**: another resource asked before esx_addonaccount finished loading. Check for `nil` and retry, or make sure the dependent resource starts later. ## Related [#related] - [esx_society](/docs/addons/esx_society) - [esx_addoninventory](/docs/addons/esx_addoninventory) - [esx_datastore](/docs/addons/esx_datastore) - [Money and accounts guide](/docs/guides/money-and-accounts) - [Database schema](/docs/guides/database-schema) # esx_addoninventory (/docs/addons/esx_addoninventory) esx_addoninventory is a server-only library resource that stores item counts outside of a player's own inventory. Use it for society storage rooms, property stashes and similar containers. Other resources such as [esx_society](/docs/addons/esx_society) and [esx_property](/docs/addons/esx_property) read and write these inventories. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_addoninventory` (version `1.0`) | | Side | Server only | | Dependencies | `esx_lib`, `es_extended`, `oxmysql` | | SQL file | `esx_addoninventory.sql` | | Exports | `GetSharedInventory`, `AddSharedInventory` | Like [esx_addonaccount](/docs/addons/esx_addonaccount), inventories are either **shared** (`shared = 1`, one inventory without an owner) or **personal** (`shared = 0`, one inventory per player identifier, created when the player loads and stored as `xPlayer.get('addonInventories')`). ## Installation [#installation] ### Import the SQL [#import-the-sql] Run `esx_addoninventory.sql`. It creates `addon_inventory` (definitions) and `addon_inventory_items` (item rows). The script uses plain `CREATE TABLE`, so it fails if the tables already exist. That is expected on an existing database. ### Ensure the resource [#ensure-the-resource] ```bash title="server.cfg" ensure es_extended ensure esx_addoninventory ensure esx_society ``` ### Define inventories [#define-inventories] ```sql INSERT INTO `addon_inventory` (`name`, `label`, `shared`) VALUES ('society_mechanic', 'Mechanic', 1); ``` ## Configuration [#configuration] There is no `config.lua`. The `addon_inventory` table is the configuration. | Column | Type | Description | | --- | --- | --- | | `name` | `VARCHAR(60)` | Unique inventory name, e.g. `society_police` | | `label` | `VARCHAR(100)` | Display label (used as stash label with ox_inventory) | | `shared` | `INT` | `1` = shared, `0` = one inventory per player | Item labels come from the ESX `items` table and are loaded on startup. ## ox_inventory mode [#ox_inventory-mode] If ox_inventory is running (ESX sets `Config.OxInventory` automatically), the resource does **not** load its own inventories. Instead it registers each `addon_inventory` row as an ox_inventory stash: | Stash setting | Value | | --- | --- | | Stash id | `name` | | Label | `label` | | Slots | `100` | | Max weight | `200000` | | Owner | `true` for `shared = 0` (one stash per player), otherwise shared | | Groups | Job name for names containing `society`: the text after `society_` | In this mode the exports and the getter events are not registered. Use `exports.ox_inventory` functions on the stash id instead. ## Server exports [#server-exports] ### GetSharedInventory [#getsharedinventory] ```lua exports.esx_addoninventory:GetSharedInventory(name) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Inventory name from `addon_inventory` | Returns the [inventory object](#inventory-object) or `nil`. ```lua title="server/main.lua" local inventory = exports.esx_addoninventory:GetSharedInventory('society_mechanic') if inventory then inventory.addItem('fixkit', 5) end ``` ### AddSharedInventory [#addsharedinventory] ```lua exports.esx_addoninventory:AddSharedInventory(society) ``` | Name | Type | Description | | --- | --- | --- | | `society` | `table` | `{ name = string, label = string }` | Inserts a shared inventory row and creates an empty inventory in memory. It does not check whether the inventory already exists: calling it twice replaces the in-memory inventory with an empty one (the database insert fails on the primary key). Check with `GetSharedInventory` first. ```lua title="server/main.lua" if not exports.esx_addoninventory:GetSharedInventory('society_bahama') then exports.esx_addoninventory:AddSharedInventory({ name = 'society_bahama', label = 'Bahama Mamas' }) end ``` ## Inventory object [#inventory-object] | Field / method | Description | | --- | --- | | `name`, `owner` | Inventory name and owner identifier (`nil` when shared) | | `items` | Array of `{ name, count, label }` | | `getItem(name)` | Returns the item entry. Creates a row with count `0` if it does not exist | | `addItem(name, count)` | Adds `count` and saves | | `removeItem(name, count)` | Removes `count` (only when `count > 0`) and saves | | `setItem(name, count)` | Sets the count and saves | The inventory methods do not check item names, negative counts or whether enough items exist. `removeItem` happily goes below zero. Always validate on your side before you call them, especially when the request comes from a client. ```lua title="server/main.lua" RegisterNetEvent('myresource:takeFromStorage', function(itemName, count) local xPlayer = ESX.GetPlayerFromId(source) count = math.floor(tonumber(count) or 0) if not xPlayer or xPlayer.job.name ~= 'mechanic' or count < 1 then return end local inventory = exports.esx_addoninventory:GetSharedInventory('society_mechanic') local item = inventory and inventory.getItem(itemName) if not item or item.count < count or not xPlayer.canCarryItem(itemName, count) then return end inventory.removeItem(itemName, count) xPlayer.addInventoryItem(itemName, count) end) ``` ## Events [#events] Both events are local server events (`AddEventHandler`). Clients cannot trigger them. | Event | Arguments | Description | | --- | --- | --- | | `esx_addoninventory:getInventory` | `name, owner, cb` | `cb(inventory)` for a personal inventory | | `esx_addoninventory:getSharedInventory` | `name, cb` | `cb(inventory)` for a shared inventory | ```lua TriggerEvent('esx_addoninventory:getInventory', 'property', xPlayer.identifier, function(inventory) if inventory then inventory.addItem('bread', 1) end end) ``` ## Integration tips [#integration-tips] - Use the same `society_` name in [esx_addonaccount](/docs/addons/esx_addonaccount), esx_addoninventory and [esx_datastore](/docs/addons/esx_datastore) so job resources find all three. - Shared inventories are also mirrored to `GlobalState.SharedInventories`. - New `addon_inventory` rows are loaded without a restart when ESX fires `esx:sqlCatalogReady` for `addon_inventory` or `items`. ## Common issues [#common-issues] - **`No such export GetSharedInventory`**: ox_inventory is running, so the resource is in stash mode. Use ox_inventory stashes. - **Item shows without a label**: the item is missing from the `items` table. - **SQL import error "table already exists"**: the tables are already there. Skip the import. ## Related [#related] - [esx_addonaccount](/docs/addons/esx_addonaccount) - [esx_society](/docs/addons/esx_society) - [ox_inventory with ESX](/docs/guides/ox-inventory) - [Add items](/docs/guides/add-items) # esx_adminmenu (/docs/addons/esx_adminmenu) esx_adminmenu is the official staff tool for ESX Legacy servers. It ships two interfaces: a full NUI dashboard (default `F10`) for players, bans, vehicles, logs and server settings, and a compact keyboard menu (default `F9`) for quick actions in-game. Every action is checked on the server against a configurable, per-feature permission system and written to an admin action log. ## Overview [#overview] | | | |---|---| | Resource | `esx_adminmenu` | | Side | Client + server (Svelte 5 NUI in `web/dist`) | | Dependencies | `esx_lib`, `es_extended`, `oxmysql` | | Optional | `esx_ambulancejob` (revive), `esx_garage` (impounds), `pma-voice` (radio), `esx_skin` (clothing menu), `esx_status` (hunger/thirst) | | Database | Creates `bans` and `admin_logs`, extends `users` (automatic) | Main features: - **Players:** online list, search (including offline characters), recent players, detail panel, goto, bring, spectate, kick, ban, revive, freeze, health/armor, money, job, name, hunger/thirst, radio, routing bucket, ped model, clothing menu, give weapons, clean inventory, delete character, ACE group management. - **Troll actions:** burn, explode, sky, random teleport, nausea (each can be disabled). - **Vehicles:** spawner with customization (performance levels, turbo, xenon, neon, colors, tint, wheels, bulletproof tires), repair, clean, flip, delete; owned vehicle list with impound, unimpound and delete; `/admincar` to register a vehicle to yourself. - **Server:** weather, time, blackout, PvP, population density, freeze/bring/revive/kill/kick all, delete world vehicles, peds and objects, broadcasts, give money to all. - **Bans:** connection-time ban check across multiple identifier types, paginated ban list, expiry editing and unban. - **Logs:** database-backed admin action log with retention, log viewer and optional Discord webhook. ## Installation [#installation] ### Add the resource [#add-the-resource] Place `esx_adminmenu` in your resources (for example inside `[esx_addons]`). The UI is prebuilt in `web/dist`; you do not need Node.js on the server. ### Ensure it after its dependencies [#ensure-it-after-its-dependencies] ```bash title="server.cfg" ensure oxmysql ensure es_extended ensure esx_lib ensure esx_adminmenu ``` ### Give yourself an admin group [#give-yourself-an-admin-group] Access depends on the ESX group (`xPlayer.getGroup()`), not on ACE. Set your group to `admin` with the ESX `setgroup` command from the server console, see [admin permissions](/docs/guides/admin-permissions). ### Restart and check the console [#restart-and-check-the-console] On start the resource prints `Database tables/indexes checked/created!`. No SQL import is needed. ### Database changes on start [#database-changes-on-start] - `bans` (`id`, `identifier`, `identifiers`, `reason`, `banned_by`, `expires_at`, `banned_at`). Old bans with a bare license get the `license:` prefix added. - `admin_logs` (`id`, `created_at`, `actor_identifier`, `actor_name`, `namespace`, `action`, `target_identifier`, `target_name`, `success`, `error`, `payload`). - `users`: adds `created_at`, `last_seen` and `phone_number` if missing, plus a `(firstname, lastname)` search index. - Search indexes on `owned_vehicles` and `admin_logs`. ## Permissions [#permissions] Access is checked in three layers, all in `shared/config.lua`: | Layer | Config key | Purpose | |---|---|---| | Menu access | `Config.AllowedGroups` | ESX groups that may open the menu and use its commands. Default `{ admin = true }`. | | Feature groups | `Config.FeaturePermissions` | Per feature key, the ESX groups allowed to use it. Every key defaults to `{ admin = true }`. | | Action mapping | `Config.AdminMenu.ActionPermissions`, `Config.PlayerActions.ActionPermissions`, `Config.ServerManagement.ActionPermissions` | Maps each concrete action to one feature key. | Actions or feature keys without an entry are denied, so a new action never inherits broad access by accident. ### Feature keys [#feature-keys] | Key | Grants | |---|---| | `selfTools` | Own noclip, godmode, invisible, revive, heal, armor, waypoint teleport | | `playerVisibility` | Player names and blips | | `infiniteAmmo` | Infinite ammo toggle | | `helperCommands` | `/vec2`, `/vec3`, `/vec4`, `/heading`, `/copycoords`, `/rot`, `/model`, `/vehplate`, copy coords | | `sensitiveInfo` | Identifiers, IP addresses and owner fields in player and vehicle data | | `playerData` | Radio, job, name, thirst and hunger | | `playerTeleport` | Goto and bring | | `spectate` | Spectate a player | | `playerNotify` | Notify a single player | | `playerModeration` | Set health/armor, kill, revive, freeze | | `destructive` | Clean inventory, delete character | | `troll` | Troll actions | | `banManagement` | View, edit and revoke bans, `/refreshbans` | | `money` | Give and take money | | `weapons` | Give all weapons from `Config.AdminWeapons` | | `routingBucket` | Change routing bucket | | `setModel` | Change ped model | | `openClothing` | Open the clothing menu on a player | | `acePermissions` | Add and remove ACE groups listed in `Config.AllowedAceGroups` | | `vehicleSpawner` | Spawn vehicles | | `vehicleTools` | Repair, clean, flip | | `vehiclePerformance` | Tuning and visual customization | | `vehicleManagement` | Impound and unimpound owned vehicles | | `vehicleDestructive` | Delete vehicles | | `vehicleOwnership` | Ownership data and `/admincar` | | `serverManagement` | Server management tab | | `serverEnvironment` | Weather, time, blackout, PvP, density | | `serverPlayerModeration` | Freeze/unfreeze all, bring all, revive all | | `serverPlayerDestructive` | Kick all, kill all | | `serverCleanup` | Delete world vehicles, peds, objects | | `serverBroadcast` | Notify all players | | `serverEconomy` | Give money to all players | | `radioLookup` | Look up a player's pma-voice radio channel | | `logViewer` | Read the admin action log | Example: a `mod` group that can open the menu, teleport, spectate and kick, but cannot see identifiers or touch money: ```lua title="shared/config.lua" Config.AllowedGroups = { admin = true, mod = true, } Config.FeaturePermissions.playerTeleport = { admin = true, mod = true } Config.FeaturePermissions.spectate = { admin = true, mod = true } Config.FeaturePermissions.playerModeration = { admin = true, mod = true } ``` The chat commands are registered with `ESX.RegisterCommand` using the groups from `Config.AllowedGroups`. Groups added there can run every command; the feature checks inside each command then decide what actually happens. ## Configuration [#configuration] ### General [#general] | Key | Default | Description | |---|---|---| | `Config.Debug` | `false` | Extra debug prints. | | `Config.Locale` | `"en"` | Locale. Note: hardcoded, not read from the `esx:locale` convar. | | `Config.AllowedAceGroups` | `{ admin = true }` | ACE groups the menu may add or remove (without the `group.` prefix). | | `Config.Keybinds.AdminMenu` | `"F9"` | Default key for `/adminmenu`. | | `Config.Keybinds.AdminDashboard` | `"F10"` | Default key for `/admindashboard`. | ### Config.AdminMenu [#configadminmenu] | Key | Default | Description | |---|---|---| | `NoClipSpeed` | `1.5` | Base noclip speed. | | `NamesDistance` | `75.0` | Range for player name tags. | | `ModelLoadTimeout` | `5000` | Milliseconds to wait for a model to load. | | `ServerDataInterval` | `60000` | Refresh interval of dashboard server data (ms). | | `Blip` | `{ sprite = 1, scale = 0.85, colour = 5 }` | Player blip style. | | `Spectate` | `{ height = 15.0, escCooldown = 5000, streamWait = 500 }` | Spectate camera settings. | | `Troll` | `{ skyHeight = 120.0, randomRange = 500, randomHeight = 40.0, nauseaDuration = 5000 }` | Troll action tuning. | | `WaypointScan` | `{ max = 1000, step = 25 }` | Ground search for waypoint teleport. | | `ActionPermissions` | see config | Self, visibility and vehicle actions mapped to feature keys. | ### Config.VehicleSpawner [#configvehiclespawner] | Key | Default | Description | |---|---|---| | `DefaultModel` | `"sultan"` | Model preselected in the spawner. | | `DeleteCurrentVehicleOnSpawn` | `true` | Delete the vehicle you sit in before spawning. | | `WarpIntoVehicle` | `true` | Put the admin into the spawned vehicle. | | `DefaultColor` | `"black"` | Default color preset id. | | `ColorPresets`, `NeonPresets`, `WindowTints`, `WheelCategories`, `WheelDesigns` | lists | Options shown in the customization UI. | ### Config.ServerManagement [#configservermanagement] | Key | Default | Description | |---|---|---| | `DefaultKickReason` | `"Kicked by admin"` | Reason used when none is given. | | `DefaultWeather` | `"CLEAR"` | Initial weather. | | `DefaultHour` / `DefaultMinute` | `12` / `0` | Initial time. | | `DefaultBlackout` | `false` | Initial blackout state. | | `DefaultPvp` | `true` | Initial PvP state. | | `DefaultDensity` | `false` | Population density used when no saved override exists. `false` keeps es_extended's multipliers; or `{ pedDensity = 0.5, vehicleDensity = 0.5, randomVehicleDensity = 0.5 }`. | | `WeatherTypes` | `CLEAR`, `EXTRASUNNY`, `CLOUDS`, `OVERCAST`, `RAIN`, `THUNDER`, `FOGGY`, `XMAS` | Weather types allowed in the UI. | | `ActionPermissions` | see config | Server actions mapped to feature keys. | Density changes made in the dashboard are stored in the resource KVP key `populationDensity:v1` and published through `GlobalState["esx:populationDensity"]`, so they survive restarts. This needs the matching density support in es_extended's world adjustments. ### Config.AdminLimits [#configadminlimits] | Key | Default | |---|---| | `MaxReasonLength` | `180` | | `MaxNotificationLength` | `240` | | `MaxNotificationDuration` | `30000` | | `MaxBanDurationMinutes` | `2628000` (5 years) | | `MaxMoneyAmount` | `10000000` | | `MaxRoutingBucket` | `2147483647` | | `MaxRadioChannel` | `10000` | | `MaxVehiclePropsBytes` | `16384` | | `OfflineSearchResults` | `25` | | `OfflineSearchCooldownMs` | `1000` | | `MinOfflineSearchLength` | `3` | | `VehiclePageSize` | `100` | | `BanPageSize` | `100` | | `RecentPlayersCap` | `100` | | `PlateGenAttempts` | `50` | | `AllowedMoneyAccounts` | `{ money = true, bank = true, black_money = true }` | | `AllowedNotificationTypes` | `{ info = true, success = true, error = true }` | ### Other sections [#other-sections] | Key | Default | Description | |---|---|---| | `Config.Ban.Identifiers` | `license`, `license2`, `discord`, `steam`, `xbl`, `live`, `fivem` = `true`; `ip` = `false` | Identifier types stored with a ban and matched on connect. A player is blocked if any identifier matches. IP is off to avoid false positives on shared IPs. | | `Config.Plate` | `{ Letters = 3, Numbers = 3, UseSpace = true }` | Format of generated plates for `/admincar`. | | `Config.Clothing.Events` | `{ "esx_skin:openSaveableMenu" }` | Client events fired on the target to open a clothing menu. | | `Config.Revive.Events` | `{ "esx_ambulancejob:revive" }` | Client events sent to the target from the server on revive. | | `Config.Revive.UseNativeFallback` | `true` | Also run a native resurrect after the events. | | `Config.Revive.NativeFallbackDelay` | `250` | Delay (ms) before the native fallback when an event was sent. | | `Config.AdminWeapons` | 9 weapons (pistol to nightstick) | Weapons and ammo for "give all weapons". | | `Config.TrollActions` | `burn`, `explode`, `sky`, `randomTeleport`, `nausea` = `true` | Enable or disable each troll action. | ### Config.Logs [#configlogs] | Key | Default | Description | |---|---|---| | `Enabled` | `true` | Record admin actions to `admin_logs`. | | `RetentionDays` | `30` | Rows older than this are purged hourly. `0` keeps everything. | | `PurgeBatchSize` | `250` | Rows deleted per purge statement. | | `PurgeBatchDelay` | `250` | Wait (ms) between purge batches. | | `PurgeTimeBudget` | `5000` | Max time (ms) per purge run. | | `PurgeBacklogDelay` | `60000` | Delay (ms) before the next run when a backlog remains. | | `FlushInterval` | `5000` | Queue flush interval (ms). | | `MaxQueue` | `500` | In-memory queue cap; oldest entries are dropped when the database is down. | | `BatchSize` | `100` | Rows per `INSERT`. | | `MaxPayloadBytes` | `2048` | Larger payloads are replaced by a truncation marker. | | `MaxPerMinutePerActor` | `120` | Per-source log rate limit, protects against spam of denied attempts. | | `Ignore` | `noclip`, `godmode`, `invisible`, `names`, `blips`, `copyCoords` | Actions that are never logged. | | `Webhook.Url` | `""` | Discord webhook URL. Empty disables it. | | `Webhook.Actions` | `ban`, `unban`, `kick`, `deleteCharacter`, `cleanInventory`, `giveMoney`, `takeMoney`, `giveMoneyAll`, `kickAll`, `killAll`, `aceAdd`, `aceRemove` | Actions forwarded to the webhook. | Anyone with the webhook URL can post to your channel. Do not commit a real URL to a public repository. ## Commands [#commands] Opening the menus: | Command | Description | |---|---| | `/adminmenu` | Opens the compact admin menu (client command, key `F9`). | | `/admin` | Opens the compact admin menu (server-checked). | | `/admindashboard` | Opens the full dashboard (key `F10`). | | `/information ` | Opens the dashboard on a player's detail panel. | Moderation: | Command | Description | |---|---| | `/noclip`, `/names`, `/blips` | Toggle noclip, name tags, player blips. | | `/goto ` | Teleport to a player. | | `/bring ` | Bring a player to you. | | `/spectate ` / `/stopspectate` | Start or stop spectating. | | `/kick ` | Kick a player. | | `/ban ` | Ban a player. Duration in minutes, `0` = permanent. | | `/notify ` | Send a notification (`info`, `success`, `error`). | | `/setmodel <playerId> <model>` | Change a player's ped model. | | `/refreshbans` (`/refreshbancache`) | Reload the ban cache after editing `bans` by hand. | Helpers (copy to clipboard, need `helperCommands`): | Command | Copies | |---|---| | `/vec2`, `/vec3`, `/vec4` | Position as `vec2(...)`, `vec3(...)`, `vec4(x, y, z, heading)` | | `/heading` | Heading | | `/copycoords` | `x = ..., y = ..., z = ..., heading = ...` | | `/rot` (`/rotation`) | Entity rotation as `vec3(...)` | | `/model` (`/entitymodel`) | Model hash of your vehicle, or your ped | | `/vehplate` (`/copyplate`) | Current vehicle plate | `/admincar [model]` spawns the given model, or takes the vehicle you are driving, and inserts it into `owned_vehicles` with your identifier and a generated plate. It requires `vehicleOwnership`. ## Server exports [#server-exports] ### logAdminAction [#logadminaction] ```lua exports.esx_adminmenu:logAdminAction(entry) ``` Queues an entry in the admin action log (respects `Config.Logs.Enabled`, `Ignore`, rate limit and webhook rules). | Field | Type | Description | |---|---|---| | `action` | `string` | Required. Action name, max 64 chars. | | `namespace` | `string?` | Group of the action, max 32 chars. Defaults to `"unknown"`. | | `actor` | `number \| string` | Player source or a raw identifier. | | `target` | `number \| string?` | Target source or identifier. | | `actorName` / `targetName` | `string?` | Fallback names when no source is given. | | `success` | `boolean?` | `false` marks a failed action. Default success. | | `err` | `string?` | Error text. | | `payload` | `table?` | Extra data, JSON encoded and truncated at `MaxPayloadBytes`. | ```lua title="server/main.lua" exports.esx_adminmenu:logAdminAction({ namespace = 'myjail', action = 'jail', actor = adminSource, target = targetSource, payload = { minutes = 30, reason = 'RDM' }, }) ``` ### getAdminLogs [#getadminlogs] ```lua local result = exports.esx_adminmenu:getAdminLogs(filters) ``` Reads the log, newest first. All filters are optional: `search` (2-100 chars, prefix match on names, identifiers and action), `actor`, `target`, `action`, `namespace`, `days`, `limit` (1-200, default 50) and `beforeId` (cursor). Returns `{ logs, hasMore, nextCursor, limit }`. ## Events and security [#events-and-security] - All dashboard and menu actions go through server callbacks (`esx-adminmenu:server:*`). Each one re-checks the caller's group and feature permission on the server, so a modified client cannot run admin actions. - `esx-adminmenu:server:impoundDeletedVehicle` (server net event, `plate`) is client-triggerable but requires `vehicleDestructive`. - `esx-adminmenu:client:*` events (`setWeather`, `setTime`, `kill`, `revive`, `troll`, and so on) are sent by the server only. Do not trigger them from other resources to bypass the permission checks and logs. - Denied attempts are logged too, which helps spot players probing the callbacks. ## Integration tips [#integration-tips] - **Revive with another ambulance script:** replace `Config.Revive.Events` with your script's client revive event. The native fallback still runs unless you disable it. - **Other clothing resources:** put your resource's open-menu client event into `Config.Clothing.Events`. - **Impounds:** with [esx_garage](/docs/addons/esx_garage) running, the vehicle tools use its `getImpounds` and `impoundVehicle` exports. - **Hunger and thirst:** set via `esx_status:set`, so [esx_basicneeds](/docs/addons/esx_basicneeds) and esx_status must run. ## Common issues [#common-issues] - **Menu does not open.** Your ESX group is not in `Config.AllowedGroups`. Check with `xPlayer.getGroup()`; the default `user` group has no access. - **Button greyed out or "Insufficient Permissions".** The group is allowed to open the menu but missing in the feature key for that action. - **Banned player still joins.** The ban was added directly in SQL. Run `/refreshbans`, and make sure the identifier has a type prefix such as `license:`. - **Ban check fails open.** If the ban cache errors during `playerConnecting`, the player is let in and the error is printed to the console. - **`admin_logs` grows large.** Lower `RetentionDays` or reduce what is logged with `Ignore`. ## Related [#related] - [Admin permissions guide](/docs/guides/admin-permissions) - [ESX commands](/docs/es_extended/commands) - [esx_ambulancejob](/docs/addons/esx_ambulancejob) - [esx_garage](/docs/addons/esx_garage) - [oxmysql guide](/docs/guides/oxmysql) # esx_ambulancejob (/docs/addons/esx_ambulancejob) esx_ambulancejob adds a playable EMS job to ESX Legacy: paramedics can revive dead players, heal injured ones, take medical supplies from the hospital pharmacy and buy ambulances and helicopters. Death itself (death screen, bleedout, respawn) is handled by [esx_death](/docs/addons/esx_death); this resource only adds the medic side. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_ambulancejob` (version 1.0.2) | | Side | client + server | | Dependencies | `esx_lib`, `esx_death`, `es_extended`, `esx_skin`, `esx_vehicleshop` | | Optional | `esx_society` (boss menu, society account), `esx_phone` (registers the `ambulance` number) | | Job name | `ambulance` (grades `ambulance`, `doctor`, `chief_doctor`, `boss`) | | Items | `medikit`, `bandage` | In-game features: - **F6 menu** (`/ambulance` command, rebindable key mapping): revive, small heal (bandage), big heal (medikit), put a patient in the closest vehicle, search a nearby dead player. - **Hospital markers**: cloakroom (go on and off duty in uniform), boss actions, pharmacy, vehicle and helicopter garages, fast-travel elevators. - **Dead player blips** for on-duty medics, plus a distress blip when a downed player sends a distress signal through esx_death. - **Usable items**: `medikit` restores full health, `bandage` restores one eighth of max health. ## Installation [#installation] <Steps> <Step> ### Import the SQL [#import-the-sql] Run `esx_ambulancejob.sql`. It creates the `ambulance` job and its 4 grades, the `society_ambulance` account, inventory and datastore, and the `medikit` and `bandage` items. Translated variants live in `localization/`. </Step> <Step> ### Check the users column [#check-the-users-column] The SQL also runs `ALTER TABLE users ADD is_dead`. If esx_death already created that column (it does with `Config.AutoMigrate = true`), the statement fails with a duplicate column error. Remove that last statement before importing, or run the file statement by statement. </Step> <Step> ### Ensure the resource [#ensure-the-resource] Start it after its dependencies: ```bash title="server.cfg" ensure esx_lib ensure es_extended ensure esx_skin ensure esx_vehicleshop ensure esx_death ensure esx_society ensure esx_ambulancejob ``` </Step> </Steps> ## Configuration [#configuration] All keys live in `config.lua`. | Key | Default | Description | | --- | --- | --- | | `Config.DrawDistance` | `10.0` | Distance at which markers are drawn. | | `Config.Debug` | `false` | Prints client debug messages (duty, blips, healing). | | `Config.DebugDeath` | `false` | Prints server debug lines for every revive attempt, including why it was blocked. | | `Config.Marker` | `{type = 1, x = 1.5, y = 1.5, z = 0.5, r = 102, g = 0, b = 102, a = 100, rotate = false}` | Default marker style for hospital points. | | `Config.ReviveReward` | `700` | Cash paid to the medic per successful revive. `0` disables the reward. | | `Config.LoadIpl` | `true` | Loads the morgue IPL. Disable if you use an IPL loader such as bob74_ipl. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language. | | `Config.DistressBlip` | `{Sprite = 310, Color = 48, Scale = 2.0}` | Blip shown to medics for a distress call. | | `Config.EnablePlayerManagement` | `false` | Shows the esx_society boss menu for the `boss` grade. | | `Config.PharmacyItems` | medikit, bandage | Menu entries `{ title = 'Medikit', item = 'medikit' }`. Only `medikit` and `bandage` are accepted by the server. | | `Config.MaxPharmacyTake` | `5` | Max amount per pharmacy request. | | `Config.PharmacyCooldown` | `5000` | Cooldown between pharmacy requests in ms. | | `Config.Hospitals` | `CentralLosSantos` | Hospital definitions, see below. | | `Config.AuthorizedVehicles` | see below | Buyable vehicles per type and grade. | ### Config.Hospitals [#confighospitals] Each hospital has these fields: | Field | Description | | --- | --- | | `Blip` | `{ coords, sprite, scale, color }` map blip. | | `AmbulanceActions` | List of `vector3` points for the cloakroom / boss menu. | | `Pharmacies` | List of `vector3` points. The server checks the medic is within 8 m of one. | | `Vehicles`, `Helicopters` | List of `{ Spawner, InsideShop (vector4), Marker, SpawnPoints = {{ coords, heading, radius }} }`. | | `FastTravels` | `{ From, To = { coords, heading }, Marker }` teleport on entering the marker. | | `FastTravelsPrompt` | Same as `FastTravels` plus `Prompt`; the player must press the key. | ### Config.AuthorizedVehicles [#configauthorizedvehicles] Keyed by vehicle type (`car`, `helicopter`) and then by job grade name: ```lua title="config.lua" Config.AuthorizedVehicles = { car = { ambulance = { { model = 'ambulance', price = 5000 } }, boss = { { model = 'ambulance', price = 2000 } }, }, helicopter = { doctor = { { model = 'buzzard2', price = 150000 } }, }, } ``` An entry can also carry a `props` table; it is merged into the vehicle properties saved in `owned_vehicles`. Purchased vehicles get an `AMB` plate prefix and are stored with `job = 'ambulance'`. <Callout type="info" title="Removed keys"> `Config.EarlyRespawnTimer`, `Config.BleedoutTimer`, `Config.RespawnPoints`, `Config.RemoveWeaponsAfterRPDeath`, `Config.RemoveCashAfterRPDeath`, `Config.RemoveItemsAfterRPDeath`, `Config.EarlyRespawnFine*` and the death camera zoom no longer exist here. They live in `esx_death/config.lua`. </Callout> ## Server events [#server-events] These are net events, so any client can trigger them. Each one validates the caller on the server, which is why custom scripts should not bypass them. ### esx_ambulancejob:revive [#esx_ambulancejobrevive] ```lua TriggerServerEvent('esx_ambulancejob:revive', targetServerId) ``` | Name | Type | Description | | --- | --- | --- | | `targetServerId` | `number` | Server ID of the dead player. | <Callout type="warn" title="Client-triggerable"> Revives a player and pays `Config.ReviveReward`. The server requires: caller has job `ambulance` and is on duty, caller is not dead, 8 second rate limit, target within 8 m and dead (according to esx_death), caller owns a `medikit` (one is consumed). To revive from server code use `exports.esx_death:Revive(target, reason)` instead. </Callout> ### esx_ambulancejob:heal [#esx_ambulancejobheal] ```lua TriggerServerEvent('esx_ambulancejob:heal', targetServerId, healType) ``` | Name | Type | Description | | --- | --- | --- | | `targetServerId` | `number` | Player to heal (within 8 m). | | `healType` | `'small' \| 'big'` | `small` uses a bandage, `big` uses a medikit. | On-duty medics only, 3 second rate limit. On success the server sends `esx_ambulancejob:heal` to the target client. ### esx_ambulancejob:putInVehicle [#esx_ambulancejobputinvehicle] ```lua TriggerServerEvent('esx_ambulancejob:putInVehicle', targetServerId) ``` Warps the target (within 8 m) into the closest free seat of the nearest vehicle. On-duty medics only, 3 second rate limit. ### esx_ambulancejob:giveItem [#esx_ambulancejobgiveitem] ```lua TriggerServerEvent('esx_ambulancejob:giveItem', itemName, amount) ``` <Callout type="warn" title="Client-triggerable"> Gives pharmacy items. The server only accepts `medikit` or `bandage`, an amount between 1 and `Config.MaxPharmacyTake`, an on-duty medic within 8 m of a pharmacy, and enforces `Config.PharmacyCooldown`. Rejected attempts are printed as a warning. </Callout> ### esx_ambulancejob:svsearch [#esx_ambulancejobsvsearch] ```lua TriggerServerEvent('esx_ambulancejob:svsearch') ``` Sends `esx_ambulancejob:clsearch` to every dead player within 3 m of the medic. esx_death handles that client event. ## Client events [#client-events] Sent by the server; listen to them if you build a custom MDT or HUD. | Event | Arguments | When | | --- | --- | --- | | `esx_ambulancejob:heal` | `healType`, `quiet` | Heals the local ped (`small` = +1/8 max health, `big` = full). | | `esx_ambulancejob:useItem` | `itemName` | Player used a `medikit` or `bandage`. | | `esx_ambulancejob:putInVehicle` | none | Warp into the nearest vehicle. | | `esx_ambulancejob:PlayerDead` | `playerId` | Sent to all medics when esx_death marks a player dead. | | `esx_ambulancejob:PlayerNotDead` | `playerId` | Sent to all medics when a player is no longer dead. | | `esx_ambulancejob:PlayerDistressed` | `playerId`, `coords` | Distress signal from a downed player. | | `esx_ambulancejob:setDeadPlayers` | `deadPlayers`, `locations` | Local event that rebuilds the dead player blips. | ```lua title="client/mdt.lua" RegisterNetEvent('esx_ambulancejob:PlayerDistressed', function(playerId, coords) print(('Distress call from %s at %.1f, %.1f'):format(playerId, coords.x, coords.y)) end) ``` ## Server callbacks [#server-callbacks] Registered with `xLib.callback.registerCompat`, so they work with `xLib.callback` and `ESX.TriggerServerCallback`. | Callback | Arguments | Returns | | --- | --- | --- | | `esx_ambulancejob:getItemAmount` | `item` | Count of that item in the caller's inventory. | | `esx_ambulancejob:getDeadPlayers` | none | `deadPlayers, locations` from esx_death (empty table if the caller is not an on-duty medic). | | `esx_ambulancejob:buyJobVehicle` | `vehicleProps`, `type` | `true` when bought. Validates grade, price, money and distance to the shop. | | `esx_ambulancejob:storeNearbyVehicle` | `plates` | Result of `es_extended:StoreJobVehicle`. | ## Commands [#commands] | Command | Side | Description | | --- | --- | --- | | `/ambulance` | client | Opens the mobile EMS menu. Mapped to `F6` by default (players can rebind it in the FiveM key settings). Only works for the `ambulance` job while alive. | ## Integration tips [#integration-tips] - **Revive from other scripts**: use the esx_death export on the server. ```lua title="server/main.lua" local ok = exports.esx_death:Revive(playerId, 'admin') if ok then print(('Revived %s'):format(playerId)) end ``` - **Duty**: the server checks `xPlayer.job.onDuty ~= false`. If you use a duty script, set ESX duty through it so medics who are off duty cannot revive. - **Uniforms** come from the `skin_male` / `skin_female` columns of `job_grades` and are applied through [esx_skin](/docs/core/esx_skin). - **Society money**: the resource registers `society_ambulance` with [esx_society](/docs/addons/esx_society) when that resource is present. ## Common issues [#common-issues] - **Duplicate column `is_dead`** when importing the SQL: the column already exists (esx_death). Skip the `ALTER TABLE` statement. - **Revive or heal does nothing**: the player is not on duty, has no `medikit`/`bandage`, is too far away, or hit the rate limit. Turn on `Config.DebugDeath`. - **F6 does nothing**: the key was rebound in the FiveM settings, or `ESX.PlayerData.dead` is true. - **Pharmacy returns nothing**: you added an item to `Config.PharmacyItems` that is not `medikit` or `bandage`, or requested more than `Config.MaxPharmacyTake`. ## Related [#related] - [esx_death](/docs/addons/esx_death) - [esx_society](/docs/addons/esx_society) - [esx_vehicleshop](/docs/addons/esx_vehicleshop) - [Create a job](/docs/guides/create-a-job) - [Add items](/docs/guides/add-items) # esx_animations (/docs/addons/esx_animations) esx_animations is a client-side emote menu for ESX Legacy. Players open it with `F3`, pick a category and play an animation, a GTA scenario (smoking, fishing, taking photos) or a walking style. It ships with a NUI interface and can fall back to the classic [esx_context](/docs/core/esx_context) menu. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_animations` (version `1.1`) | | Side | Client only (plus NUI) | | Dependencies | `esx_lib`, `es_extended`; [esx_context](/docs/core/esx_context) when `Config.UseNui = false` | | SQL file | none | | Commands | `animmenu` (`F3`), `cleartasks` (`Z`) | | Client exports | `GetConfig`, `SetConfig` | ## Installation [#installation] ```bash title="server.cfg" ensure es_extended ensure esx_context ensure esx_animations ``` No database changes are needed. ## Usage in game [#usage-in-game] | Action | Default key | Command | | --- | --- | --- | | Open the menu | `F3` | `/animmenu` | | Stop the animation | `Z` | `/cleartasks` | The keys are registered with `RegisterKeyMapping`, so each player can change them under Settings, Key Bindings, FiveM. The menu does not open while the player is dead. While the NUI is open, several controls (F2, F3, F4, F6, F7, Enter and exit vehicle) are disabled so other menus do not open behind it. ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language of the NUI texts | | `Config.UseNui` | `true` | `true` uses the NUI menu, `false` uses the ESX context menu | | `Config.Animations` | 8 categories | The menu content, see below | The default categories are `festives`, `greetings`, `work`, `humors`, `sports`, `misc`, `attitudem` (walk styles) and `porn` (adult gestures). Remove the last one if it does not fit your server. ### Config.Animations format [#configanimations-format] ```lua title="config.lua" Config.Animations = { { name = 'greetings', -- unique id of the category label = 'Greetings', -- shown in the menu items = { { label = 'Hello', type = 'anim', data = { lib = 'gestures@m@standing@casual', anim = 'gesture_hello' } }, { label = 'Smoke', type = 'scenario', data = { anim = 'WORLD_HUMAN_SMOKING' } }, { label = 'Confident', type = 'attitude', data = { lib = 'move_m@confident', anim = 'move_m@confident' } }, } }, } ``` | `type` | `data` fields | What happens | | --- | --- | --- | | `anim` | `lib` (animation dictionary), `anim` (clip name) | Loads the dictionary and plays the clip once | | `scenario` | `anim` (scenario name) | Starts the scenario in place | | `attitude` | `lib` (clip set to load), `anim` (clip set to apply) | Changes the walking style until it is changed again | ### Translations [#translations] The labels in `config.lua` are English. The `localization` folder contains complete replacement configs: `br`, `cs`, `de`, `en`, `fi`, `it`, `nl`, `pl`, `ru`, `sl`, `sr` and `sv` (for example `de_config.lua`). Copy the one you need over `config.lua`. The NUI language follows the `esx:locale` convar. ## Client exports [#client-exports] ### GetConfig [#getconfig] ```lua local config = exports.esx_animations:GetConfig() ``` Returns the full `Config` table of the resource, including `Config.Animations`. ### SetConfig [#setconfig] ```lua exports.esx_animations:SetConfig(newConfig) ``` | Name | Type | Description | | --- | --- | --- | | `newConfig` | `table` | Replaces the whole `Config` table | `SetConfig` replaces the table instead of merging it, so always start from `GetConfig()`: ```lua title="client/main.lua" local config = exports.esx_animations:GetConfig() config.Animations[#config.Animations + 1] = { name = 'gang', label = 'Gang', items = { { label = 'Lean on wall', type = 'scenario', data = { anim = 'WORLD_HUMAN_LEANING' } }, } } exports.esx_animations:SetConfig(config) ``` The change is client-side only and lasts until the resource restarts. Run it on every client, for example on `esx:playerLoaded`. ## Common issues [#common-issues] - **Animation does nothing**: the dictionary or clip name is wrong. Check the name with a tool such as the [FiveM natives reference](https://docs.fivem.net/natives/) or an animation list. - **Walk style sticks after `/cleartasks`**: walk styles are not tasks. Apply a different `attitude` entry, or call `ResetPedMovementClipset` in your own code. - **Menu looks like the old ESX menu**: `Config.UseNui` is `false`. ## Related [#related] - [esx_context](/docs/core/esx_context) - [Streaming module](/docs/es_extended/client/modules/streaming) - [Addons overview](/docs/addons) # esx_banking (/docs/addons/esx_banking) esx_banking is the official bank and ATM resource for ESX Legacy. Players walk up to a bank counter or an ATM, press `E` and use a web UI to deposit, withdraw, transfer money to other players and set an ATM PIN. Every transaction is validated on the server and written to a history table. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_banking` (version `1.0.1`) | | Side | Client, server and NUI | | Dependencies | `esx_lib`, `es_extended`, `oxmysql` | | SQL file | `banking.sql` (table `banking`, adds `users.pincode`) | | Interaction | `E` near a bank or ATM, shown with [esx_textui](/docs/core/esx_textui) | | Server export | `logTransaction` | ## Installation [#installation] <Steps> <Step> ### Import the SQL [#import-the-sql] Run `banking.sql`. It creates the `banking` history table and adds a nullable `pincode` column to `users`. If you skip it, the resource runs the same migration on start, but it requires both tables to use InnoDB. On failure the console prints `Schema migration failed; banking disabled`. </Step> <Step> ### Ensure the resource [#ensure-the-resource] ```bash title="server.cfg" ensure es_extended ensure esx_banking ``` </Step> </Steps> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Debug` | `false` | Prints debug messages on the server | | `DrawMarker` | `10` | Distance at which bank markers are drawn (only with `ShowMarker`) | | `Locale` | `GetConvar('esx:locale', 'en')` | Language | | `EnablePeds` | `true` | Spawns the networked bank clerk peds from `Peds` | | `ShowMarker` | `false` | Draws a marker at each bank position | | `InteractionDistance` | `1.5` | Max distance to a bank `Position`, client and server | | `AtmInteractionDistance` | `2.0` | Max distance to an ATM, checked on the server | | `ClientAtmFallback` | `false` | Trusts ATM coords sent by the client, but only if they match an `AtmLocations` entry | | `ClientAtmFallbackDistance` | `1.75` | Distance used for that fallback check | | `RequireAtmPin` | `true` | ATM sessions need the 4-digit PIN before money can move | | `SessionDuration` | `90` | Seconds a bank/ATM session stays valid, refreshed by each action | | `TransactionCooldown` | `1250` | Rate limit in ms between transactions | | `PinAttemptCooldown` | `1500` | Rate limit in ms between PIN attempts | | `MaxTransactionAmount` | `100000000` | Highest amount per transaction | | `HistoryLimit` | `20` | Rows shown in the history (max 100) | | `HistoryDays` | `30` | Only rows from the last N days are shown | | `LogRetentionDays` | `60` | Rows older than this are deleted on start. `0` keeps everything | | `LogValueMaxLength` | `80` | Max length of label and type in a log row | | `AtmModels` | `prop_fleeca_atm`, `prop_atm_01`, `prop_atm_02`, `prop_atm_03` | ATM prop hashes | | `AtmLocations` | 70 `vector3` positions | Known ATM positions used for server-side distance checks | | `Banks` | 8 entries | Bank counters, see below | | `Peds` | 8 entries | Clerk peds, see below | Each `Banks` entry: ```lua { Position = vector4(149.91, -1040.74, 29.374, 160), Blip = { Enabled = true, Color = 69, Label = 'Bank', Sprite = 108, Scale = 0.7 } } ``` Each `Peds` entry has `Position` (`vector4`), `Model` (hash, default `` `U_M_M_BankMan` ``) and `Scenario` (default `'WORLD_HUMAN_CLIPBOARD'`). <Callout type="info" title="Custom ATMs"> The server only opens an ATM session when the player stands near an `AtmLocations` entry or a networked ATM object. If you add ATMs with a map, add their positions to `AtmLocations`, otherwise the UI will refuse to open. </Callout> ## Security model [#security-model] The client only sends requests. The server: - starts a session in `esx_banking:getPlayerData` only when the player is near a bank or ATM, - re-checks the distance on every action and expires sessions after `SessionDuration`, - allows transfers and PIN changes only in bank sessions, - requires the PIN for ATM sessions when `RequireAtmPin` is on, - rate limits opening (750 ms), transactions and PIN attempts, and locks one transaction per player at a time. ## Server export [#server-export] ### logTransaction [#logtransaction] Adds a row to a player's bank history. It does not move money. Use it when another resource charges the bank account and you want it to show up in the banking UI. ```lua exports.esx_banking:logTransaction(playerId, label, key, amount) ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | `number` | Server id of an online player | | `label` | `string` | Text shown in the history, e.g. `CAR PURCHASE` | | `key` | `string` | Transaction type, e.g. `WITHDRAW` or `DEPOSIT`. Required, stored upper-case | | `amount` | `number` | Positive amount up to `MaxTransactionAmount` | ```lua title="server/main.lua" local xPlayer = ESX.GetPlayerFromId(source) if xPlayer.getAccount('bank').money >= price then xPlayer.removeAccountMoney('bank', price, 'Vehicle purchase') exports.esx_banking:logTransaction(source, 'CAR PURCHASE', 'WITHDRAW', price) end ``` ## Events and callbacks [#events-and-callbacks] | Name | Type | Description | | --- | --- | --- | | `esx_banking:doingType` | Net event (client to server) | Deposit, withdraw, transfer or PIN change. Payload `{ action = 'deposit', amount = 500 }`, `{ action = 'transfer', target = id, amount = n }`, `{ action = 'pincode', pin = '1234' }` | | `esx_banking:getPlayerData` | Callback | Opens a session. Argument `{ accessType = 'bank' or 'atm', atm = ... }` | | `esx_banking:checkPincode` | Callback | Unlocks an ATM session with a 4-digit PIN | | `esx_banking:closebanking` | Client event | Closes the banking UI. Useful when you cuff or kill a player | | `esx_banking:updateMoneyInUI` | Client event | Server pushes fresh balances to the UI | | `esx_banking:pedHandler` | Client event | Server sends the network ids of the clerk peds | <Callout type="warn" title="Client-triggerable"> `esx_banking:doingType` and the callbacks can be called by any client. They are safe only because of the session and distance checks above. Do not loosen `ClientAtmFallback` or the distances without a reason. </Callout> Close the UI from another resource: ```lua title="server/main.lua" TriggerClientEvent('esx_banking:closebanking', targetId) ``` ## Common issues [#common-issues] - **`E` prompt shows but the UI does not open**: the server distance check failed. Usually a custom ATM not listed in `AtmLocations`. - **"Payments disabled" or "banking disabled" in the console**: `users` or `banking` is not InnoDB, or the `identifier` column is not `VARCHAR`. - **Old scripts call `TriggerServerEvent('esx_banking:logTransaction', ...)`**: that event was removed. Move the call to the server export. ## Related [#related] - [Money and accounts guide](/docs/guides/money-and-accounts) - [xPlayer](/docs/es_extended/server/xplayer) - [esx_billing](/docs/addons/esx_billing) - [esx_textui](/docs/core/esx_textui) # esx_barbershop (/docs/addons/esx_barbershop) esx_barbershop places barber shops on the map where players pay to change their hairstyle, beard, eyebrows, makeup, lipstick and ear accessories. It reuses the [esx_skin](/docs/core/esx_skin) menu in restricted mode and saves the result server-side, with an automatic refund if saving fails. ## Overview [#overview] | | | |---|---| | Resource | `esx_barbershop` | | Side | Client + server | | Dependencies | `esx_lib`, `es_extended`, `esx_skin` | | Database | None (uses the skin column via `esx_skin`) | | Payment | Cash (`xPlayer.removeMoney`) | In-game flow: 1. The player walks into one of the 7 default shop markers and presses `E`. 2. The esx_skin menu opens, limited to the fields listed below. 3. After confirming with "Yes", the server checks distance and money, charges `Config.Price` and saves the skin. 4. "No", leaving the marker or a failed payment reloads the previous look. Editable fields: `hair_1`, `hair_2`, `hair_color_1`, `hair_color_2`, `beard_1` to `beard_4`, `eyebrows_1` to `eyebrows_4`, `makeup_1` to `makeup_4`, `lipstick_1` to `lipstick_4`, `ears_1`, `ears_2`. ## Installation [#installation] There is no SQL file. Make sure `esx_skin` (which exports `SaveSkin`) starts first: ```bash title="server.cfg" ensure es_extended ensure esx_lib ensure skinchanger ensure esx_skin ensure esx_barbershop ``` ## Configuration [#configuration] All keys live in `config.lua`. | Key | Default | Description | |---|---|---| | `Config.Price` | `100` | Cash price per visit. | | `Config.ShopDistance` | `3.0` | Server-side maximum distance (meters) between the player and a shop for paying. | | `Config.DrawDistance` | `10.0` | Distance at which markers are drawn. | | `Config.MarkerSize` | `vector3(1.5, 1.5, 1.0)` | Marker size. The interaction radius is fixed to `1.5` in the client. | | `Config.MarkerColor` | `{r = 102, g = 102, b = 204}` | Marker color (alpha fixed to 100). | | `Config.MarkerType` | `1` | Marker type. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale for help text and notifications. | | `Config.Shops` | 7 `vector3` positions | Shop locations. Each gets a marker and a blip (sprite `71`, color `51`). | Default shop positions: ```lua title="config.lua" Config.Shops = { vector3(-814.3, -183.8, 36.6), vector3(136.8, -1708.4, 28.3), vector3(-1282.6, -1116.8, 6.0), vector3(1931.5, 3729.7, 31.8), vector3(1212.8, -472.9, 65.2), vector3(-32.9, -152.3, 56.1), vector3(-278.1, 6228.5, 30.7) } ``` ## Callbacks [#callbacks] Both callbacks are registered with `xLib.callback.registerCompat`, so they work with `xLib.callback` and `ESX.TriggerServerCallback`. ### esx_barbershop:pay [#esx_barbershoppay] ```lua xLib.callback('esx_barbershop:pay', false, function(paid, reason) end, skin) ``` | Name | Type | Description | |---|---|---| | `skin` | `table` | Full skin table from `skinchanger:getSkin`. | Returns `paid` (`boolean`) and, on failure, a `reason` string. Reasons set by this resource: `player_unavailable`, `rate_limited`, `not_near_shop`, `invalid_appearance:payload:expected_table`, `payment_pending`, `insufficient_funds`, `payment_failed`, `appearance_save_failed`, `appearance_save_error`, `appearance_save_rejected`, `resource_stopped`. esx_skin can pass its own reasons through as well (for example `appearance_scope_mismatch:sex`). The server: - rate limits each player (2 requests, refilled 1 per 2 seconds), - requires the player within `Config.ShopDistance` of a shop, - locks the account with `xPlayer.beginAccountOperation()` and allows only one pending payment per identifier, - calls `exports.esx_skin:SaveSkin(source, skin, finish, 'barber')` and refunds the price with reason `Haircut Refund` if the save does not succeed. ### esx_barbershop:checkMoney [#esx_barbershopcheckmoney] ```lua xLib.callback('esx_barbershop:checkMoney', false, function(canPay) end) ``` Returns `true` when the player is near a shop and has at least `Config.Price` cash. ## Server events [#server-events] ### esx_barbershop:pay [#esx_barbershoppay-1] ```lua TriggerServerEvent('esx_barbershop:pay', skin) ``` Legacy net event that runs the same logic as the callback without returning a result. The bundled client uses the callback. <Callout type="info" title="Client-triggerable"> Any client can send this event with an arbitrary skin table. The server still enforces the distance check, rate limit and full price, and esx_skin's `SaveSkin` with scope `'barber'` only copies the fields listed in its `Config.SkinEditFields.barber` onto the stored skin and rejects a changed `sex`. A cheater cannot get a free haircut or swap clothes this way, as long as esx_skin is current. </Callout> ## Client events [#client-events] Local events fired by the marker zones: | Event | Argument | Fired when | |---|---|---| | `esx_barbershop:hasEnteredMarker` | `zone` (shop index) | Player enters a shop marker. Shows the help text with `ESX.TextUI`. | | `esx_barbershop:hasExitedMarker` | `zone` | Player leaves. Closes the menu and restores the old skin if nothing was paid. | ## Common issues [#common-issues] - **Purchase failed notification, money returned.** esx_skin rejected the save. Check the server console for `[esx_barbershop] Appearance save failed` and the reason; usually esx_skin is outdated and has no `SaveSkin` export. - **"Not enough money" although the bank is full.** Only cash (`money` account) is used. - **Look resets after relog.** The save failed or another skin script overwrote the skin. Make sure only one appearance resource writes to `users.skin`. ## Related [#related] - [esx_skin](/docs/core/esx_skin) - [skinchanger](/docs/core/skinchanger) - [esx_clotheshop](/docs/addons/esx_clotheshop) - [esx_accessories](/docs/addons/esx_accessories) # esx_basicneeds (/docs/addons/esx_basicneeds) esx_basicneeds adds hunger and thirst to ESX Legacy. It registers both as statuses in [esx_status](/docs/addons/esx_status), drains them over time, damages the player when one hits zero, and turns food and drink items (bread and water by default) into usable items with an eating or drinking animation. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_basicneeds` (version 1.0) | | Side | client + server | | Dependencies | `esx_lib`, `es_extended`, `esx_status` (also loads `@oxmysql`) | | Items | `bread`, `water` | | Command | `/heal` (admin) | How it behaves in-game: - Hunger and thirst have a max of `1000000`. Hunger drains `100` and thirst `75` per esx_status tick. - At `0` the player takes damage each tick (1 HP, or 5 HP when health is at or below 150). - After death and respawn both statuses are reset to `500000`. - Using an item adds `status` to hunger (`food`) or thirst (`drink`), plays a 3 second animation with an attached prop and optionally removes the item. ## Installation [#installation] <Steps> <Step> ### Import the items [#import-the-items] Run `esx_basicneeds.sql` (or a translated file from `localization/`). It inserts `bread` and `water` into `items`. </Step> <Step> ### Ensure the resource [#ensure-the-resource] ```bash title="server.cfg" ensure esx_lib ensure es_extended ensure esx_status ensure esx_basicneeds ``` </Step> </Steps> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language. | | `Config.Visible` | `true` | Whether the hunger and thirst bars are visible in the esx_status HUD. | | `Config.Items` | `bread`, `water` | Usable consumables, keyed by item name. | ### Config.Items fields [#configitems-fields] | Field | Type | Description | | --- | --- | --- | | `type` | `'food' \| 'drink'` | `food` adds to hunger, `drink` adds to thirst. Any other value logs an error. | | `prop` | `string` | Prop model attached to the right hand. | | `status` | `number` | Amount added (out of `1000000`). Default bread `200000`, water `100000`. | | `remove` | `boolean` | Remove one item on use. | | `anim` | `{ dict, name, settings }` | Animation. `settings` holds the 8 extra `TaskPlayAnim` arguments. | | `pos` | `vector3` | Prop offset on the hand bone. | | `rot` | `vector3` | Prop rotation. | ```lua title="config.lua" Config.Items['sandwich'] = { type = 'food', prop = 'prop_sandwich_01', status = 350000, remove = true, anim = { dict = 'mp_player_inteat@burger', name = 'mp_player_int_eat_burger_fp', settings = { 8.0, -8, -1, 49, 0, 0, 0, 0 } }, pos = vector3(0.15, 0.03, 0.0), rot = vector3(15.0, 175.0, 5.0), } ``` Remember to add `sandwich` to the `items` table as well. ## Commands [#commands] ### /heal [#heal] ```txt /heal <playerId> ``` Admin only (`admin` group). Sets hunger and thirst to the maximum, restores full health and notifies the player. The `playerId` argument is required, so to heal yourself pass your own ID. Healing a player through the txAdmin menu triggers the same logic. ## Events [#events] ### esx_basicneeds:healPlayer (client) [#esx_basicneedshealplayer-client] ```lua TriggerClientEvent('esx_basicneeds:healPlayer', playerId) ``` Sets hunger and thirst to `1000000` and health to max on that client. Net event, so trigger it from the server. ### esx_basicneeds:resetStatus (client, local) [#esx_basicneedsresetstatus-client-local] ```lua TriggerEvent('esx_basicneeds:resetStatus') ``` Sets hunger and thirst to `500000`. Local only, use it from client scripts (for example after a custom respawn). ### esx_basicneeds:onUse (client) [#esx_basicneedsonuse-client] ```lua TriggerClientEvent('esx_basicneeds:onUse', playerId, itemType, propName, anim, pos, rot) ``` | Name | Type | Description | | --- | --- | --- | | `itemType` | `'food' \| 'drink'` | Picks the default animation and prop. | | `propName` | `string?` | Prop model. Defaults to `prop_cs_burger_01` or `prop_ld_flow_bottle`. | | `anim` | `table?` | `{ dict, name, settings }`. | | `pos`, `rot` | `vector3?` | Prop offset and rotation. | Plays only the animation; it does not change any status. Useful when your own script handles the item logic. Arguments are validated and a second call is ignored while an animation is running. ### esx_basicneeds:isEating (client, local) [#esx_basicneedsiseating-client-local] ```lua TriggerEvent('esx_basicneeds:isEating', function(isEating) print('Eating or drinking:', isEating) end) ``` Returns whether the eat or drink animation is currently playing. <Callout type="info" title="Deprecated"> `esx_basicneeds:onEat` and `esx_basicneeds:onDrink` still work but print a warning and forward to `esx_basicneeds:onUse`. </Callout> ## Integration tips [#integration-tips] - Change statuses from the server through esx_status. The server side only has `esx_status:set` and `esx_status:add`; to subtract, send the client event: ```lua title="server/main.lua" TriggerEvent('esx_status:add', source, 'hunger', 100000) TriggerClientEvent('esx_status:remove', source, 'thirst', 100000) ``` - Read the current value on the client: ```lua title="client/main.lua" TriggerEvent('esx_status:getStatus', 'hunger', function(status) print(status.percent) end) ``` - [esx_optionalneeds](/docs/addons/esx_optionalneeds) adds a `drunk` status on top of these two. ## Common issues [#common-issues] - **Bars not showing**: esx_status must start before esx_basicneeds, and `Config.Visible` must be `true`. - **Item does nothing**: the item is missing from the `items` table, or `type` is not `food` or `drink`. - **Values reset on reconnect**: esx_status saves statuses to the `users.status` column. Make sure that column exists. ## Related [#related] - [esx_status](/docs/addons/esx_status) - [esx_optionalneeds](/docs/addons/esx_optionalneeds) - [esx_hud](/docs/addons/esx_hud) - [Add items](/docs/guides/add-items) # esx_billing (/docs/addons/esx_billing) esx_billing adds invoices to ESX Legacy. Job resources such as [esx_policejob](/docs/addons/esx_policejob), [esx_taxijob](/docs/addons/esx_taxijob) and [esx_mechanicjob](/docs/addons/esx_mechanicjob) use it to fine or bill players. Players open their unpaid bills with `F7` and pay them from cash or bank, and the money goes to the society account of the issuing job. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_billing` (version `1.0`) | | Side | Client and server | | Dependencies | `esx_lib`, `es_extended`, `oxmysql`; society bills need [esx_addonaccount](/docs/addons/esx_addonaccount) | | SQL file | `esx_billing.sql` (table `billing`) | | Command | `showbills` (key mapping `F7`), console command `billingpayments` | | Server exports | `BillPlayer`, `BillPlayerByIdentifier` | ## Installation [#installation] <Steps> <Step> ### Import the SQL [#import-the-sql] Run `esx_billing.sql` to create the `billing` table. On first start the resource also creates a `billing_payments` journal table and checks that `billing` uses InnoDB. If the migration fails, payments are disabled and the console prints `[esx_billing] Schema migration failed; payments disabled`. </Step> <Step> ### Ensure the resource [#ensure-the-resource] ```bash title="server.cfg" ensure es_extended ensure esx_addonaccount ensure esx_billing ``` </Step> <Step> ### Create society accounts [#create-society-accounts] Every job that bills needs a shared `society_<job>` row in `addon_account`. See [esx_addonaccount](/docs/addons/esx_addonaccount). </Step> </Steps> ## Configuration [#configuration] All keys live in `config.lua`. | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language for notifications | | `Config.BillingDistance` | `10.0` | Max distance between sender and target, checked on the server | | `Config.BillingCooldown` | `3000` | Cooldown in ms between two bills from the same player | | `Config.MinimumBillingGrade` | `1` | Minimum job grade required to send bills | | `Config.BillingMinimumGrades` | `{}` | Per-job override, e.g. `{ police = 0, taxi = 2 }` | | `Config.MaxBillAmount` | `100000` | Highest amount a single bill may have | | `Config.MaxDailyBillAmount` | `250000` | Total a single sender may bill per day. `0` disables the limit | | `Config.MaxBillLabelLength` | `80` | Labels are trimmed to this length. Empty labels become `Invoice` | | `Config.HighBillConfirmationAmount` | `50000` | Bills at or above this amount must be accepted by the target | | `Config.HighBillConfirmationTimeout` | `30000` | Time in ms the target has to accept a high bill | | `Config.BillsPageSize` | `50` | Bills per page in the menu (clamped to 1-100) | | `Config.BillsReadCooldown` | `1000` | Rate limit in ms for loading the bill list | | `Config.BillPaymentCooldown` | `500` | Rate limit in ms for payment attempts | The daily total is kept in memory per sender identifier and calendar day, so it resets on a resource restart. ## How payments work [#how-payments-work] 1. The player selects a bill in the `F7` menu. 2. The server takes the money from `money` (cash) if the player has enough, otherwise from `bank`. 3. **Society bills** (`target_type = 'society'`) credit the shared account named in the bill. 4. **Player bills** (`target_type = 'player'`) credit the sender's same account type. The sender must be online, otherwise the payment is refused. 5. The bill is moved to `billing_payments` in a transaction and marked `applied` once both players and the account are saved. Failed credits are rolled back. ## Server events [#server-events] ### esx_billing:sendBill [#esx_billingsendbill] ```lua TriggerServerEvent('esx_billing:sendBill', targetId, sharedAccountName, label, amount) ``` | Name | Type | Description | | --- | --- | --- | | `targetId` | `number` | Server id of the player who receives the bill | | `sharedAccountName` | `string` | Society account of the sender's job, e.g. `society_taxi` | | `label` | `string` | Bill text shown to the target | | `amount` | `number` | Amount, floored to a whole number, 1 to `Config.MaxBillAmount` | <Callout type="warn" title="Client-triggerable"> Any client can fire this net event, so the server enforces several checks. The bill is silently dropped unless all of these pass: `sharedAccountName` matches `society_<sender job>`, the sender's grade meets the minimum, the target is not the sender, the target is within `Config.BillingDistance`, the cooldown has passed and the daily quota is not exceeded. Keep these limits tight on live servers. </Callout> ```lua title="client/main.lua" local player, distance = ESX.Game.GetClosestPlayer() if player ~= -1 and distance <= 3.0 then TriggerServerEvent('esx_billing:sendBill', GetPlayerServerId(player), 'society_mechanic', 'Engine repair', 750) else ESX.ShowNotification('No player nearby') end ``` ### esx_billing:sendBillToIdentifier [#esx_billingsendbilltoidentifier] Local server event (not networked). It only runs when triggered by another resource and bills an identifier with sender `server`. ```lua TriggerEvent('esx_billing:sendBillToIdentifier', targetIdentifier, sharedAccountName, label, amount) ``` Use it with a society account. A non-society bill with sender `server` can never be paid, because player bills require the sender to be online. ### esx_billing:paidBill [#esx_billingpaidbill] Fired after a bill was paid. | Side | Arguments | | --- | --- | | Server (local event) | `source, billId` | | Client (local event) | `billId` | ```lua title="server/main.lua" AddEventHandler('esx_billing:paidBill', function(playerId, billId) print(('Player %s paid bill #%s'):format(playerId, billId)) end) ``` ## Server exports [#server-exports] ### BillPlayer [#billplayer] ```lua exports.esx_billing:BillPlayer(targetId, senderIdentifier, sharedAccountName, label, amount) ``` | Name | Type | Description | | --- | --- | --- | | `targetId` | `number` | Server id of an online player | | `senderIdentifier` | `string` | Identifier stored as sender | | `sharedAccountName` | `string?` | `society_<job>` for a society bill. Anything else creates a player bill paid to `senderIdentifier` | | `label` | `string` | Bill text | | `amount` | `number` | Amount, 1 to `Config.MaxBillAmount` | Returns the inserted bill id, or `false`. The export skips the job, grade, distance and confirmation checks of the net event, but still counts toward the daily quota. ```lua title="server/main.lua" local xTarget = ESX.GetPlayerFromId(targetId) local xSender = ESX.GetPlayerFromId(source) local billId = exports.esx_billing:BillPlayer(xTarget.source, xSender.identifier, 'society_police', 'Speeding', 500) ``` ### BillPlayerByIdentifier [#billplayerbyidentifier] ```lua exports.esx_billing:BillPlayerByIdentifier(targetIdentifier, senderIdentifier, sharedAccountName, label, amount) ``` Same as `BillPlayer`, but targets an identifier, so the player can be offline. Returns the bill id or `false`. ## Callbacks [#callbacks] Registered with `xLib.callback.registerCompat`, so they work with `ESX.TriggerServerCallback` and `xLib.callback`. | Name | Arguments | Returns | Notes | | --- | --- | --- | --- | | `esx_billing:getBills` | `afterId?` | `bills, hasNext` | Own bills (`id`, `label`, `amount`), paged | | `esx_billing:getTargetBills` | `target, afterId?` | `bills, hasNext` | Only for job `police`, target must be nearby | | `esx_billing:payBill` | `billId` | `boolean` | Pays one of the caller's own bills | | `esx_billing:respondHighBill` | `token, accepted` | `boolean` | Answer to a high-bill confirmation | ```lua title="client/main.lua" ESX.TriggerServerCallback('esx_billing:getTargetBills', function(bills) for _, bill in ipairs(bills) do print(bill.label, bill.amount) end end, GetPlayerServerId(closestPlayer)) ``` ## Console command [#console-command] `billingpayments` works only from the server console and helps with stuck payments: ```bash billingpayments list billingpayments show <operation_id> billingpayments retry <operation_id> ``` `list` prints up to 20 receipts still in state `claimed`. ## Common issues [#common-issues] - **Bills never arrive**: check the sender's job grade against `Config.MinimumBillingGrade`, the distance and that `sharedAccountName` is exactly `society_<job>`. Failed authorization prints `attempted to send an unauthorized bill` in the console. - **"Attempted to send bill from invalid society"**: the `society_<job>` row is missing in `addon_account`. - **Paying a player bill fails**: the original sender is offline. - **Large fines need an extra click**: bills of `Config.HighBillConfirmationAmount` or more open a confirmation menu on the target. ## Related [#related] - [esx_addonaccount](/docs/addons/esx_addonaccount) - [esx_society](/docs/addons/esx_society) - [esx_policejob](/docs/addons/esx_policejob) - [Server callbacks guide](/docs/guides/callbacks) - [Create a job](/docs/guides/create-a-job) # esx_boat (/docs/addons/esx_boat) esx_boat lets players buy boats at a marina, store them in boat garages along the coast and buy a boat license through `esx_license`. Purchased boats are saved in `owned_vehicles` with `type = 'boat'`, so they are separate from cars handled by [esx_garage](/docs/addons/esx_garage). ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_boat` | | Side | Client + server | | Dependencies | `esx_lib`, `es_extended`, `esx_vehicleshop` (plus `oxmysql`) | | Optional | `esx_license` (when `Config.LicenseEnable = true`) | | Database | `owned_vehicles` (from es_extended), `licenses` row `boat` | ### In-game flow [#in-game-flow] 1. At the boat shop marker press **E**. If the license is required and missing, a menu offers the license for `Config.LicensePrice`. 2. Pick a boat, preview it on the water and choose **Buy**. Money is taken from the cash account. 3. At a garage marker press **E** to take out a stored boat at the garage's `SpawnPoint`. 4. Drive into the large red marker (`StorePos`) as the driver and press **E** to store the boat. You are teleported to `StoreTP`. ## Installation [#installation] <Steps> <Step> ### Database [#database] The resource uses the esx_lib SQL catalog and inserts the `boat` license row automatically, with a label in your `esx:locale`. The manual equivalent is `esx_boat.sql` (translations live in `localization/`): ```sql title="esx_boat.sql" INSERT INTO `licenses` (`type`, `label`) VALUES ('boat', 'Boat License'); ``` </Step> <Step> ### server.cfg [#servercfg] ```bash title="server.cfg" ensure esx_vehicleshop ensure esx_license ensure esx_boat ``` </Step> </Steps> ## Configuration [#configuration] All keys live in `config.lua`. | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale used for texts. | | `Config.LicenseEnable` | `true` | Require the `boat` license to open the shop. Needs `esx_license`. | | `Config.LicensePrice` | `50000` | Cash price of the boat license. | | `Config.MarkerType` | `1` | Marker type for shop and garage markers. | | `Config.DrawDistance` | `10.0` | Distance at which markers are drawn. | | `Config.Marker` | `{ r = 100, g = 204, b = 100, x = 1.5, y = 1.5, z = 1.0 }` | Color and size of shop and garage markers. `x` is also the interaction radius. | | `Config.StoreMarker` | `{ r = 255, g = 0, b = 0, x = 5.0, y = 5.0, z = 1.0 }` | Color and size of the store marker. `x` is also the interaction radius. | | `Config.Zones.Garages` | 6 garages | List of garages (see below). | | `Config.Zones.BoatShops` | 1 shop (Shank St) | List of shops with `Outside` (`vector3`) and `Inside` (`vector4` preview spot). | | `Config.Vehicles` | 11 boats | Boats for sale: `{ model, label, price, props? }`. | Each garage entry has four positions: | Field | Type | Description | | --- | --- | --- | | `GaragePos` | `vector3` | Marker to open the garage. | | `SpawnPoint` | `vector4` | Where a boat is spawned (`w` = heading). | | `StorePos` | `vector3` | Marker where you drive the boat in to store it. | | `StoreTP` | `vector4` | Where the player is teleported after storing. | Default boats and prices: | Model | Price | | --- | --- | | `seashark`, `seashark3` | 7500 | | `suntrap` | 10000 | | `dinghy2` | 32500 | | `dinghy` | 40000 | | `jetmax` | 45000 | | `squalo` | 61000 | | `tropic2` | 65000 | | `speeder` | 85000 | | `toro` | 200000 | | `submersible` | 750000 | ```lua title="config.lua" Config.Vehicles = { { model = 'seashark', label = 'Seashark (Random Color)', price = 7500 }, -- optional props are applied in the preview and saved on purchase { model = 'speeder', label = 'Speeder (Black)', price = 90000, props = { color1 = 0, color2 = 0 } }, } ``` ## Exports [#exports] ### getGarages [#getgarages] Shared export defined in `config.lua`. Returns the `Config.Zones.Garages` table, for example to show boat garages on a phone map. ```lua local garages = exports['esx_boat']:getGarages() for i, garage in ipairs(garages) do print(i, garage.GaragePos) end ``` ## Server callbacks and events [#server-callbacks-and-events] These are triggered by the esx_boat client. They are listed so you can audit them or call them from your own UI. All are validated on the server. | Name | Type | Arguments | Server checks | | --- | --- | --- | --- | | `esx_boat:buyBoat` | callback | `vehicleProps` | Model must be in `Config.Vehicles`, player within 20 m of a shop `Outside` or 35 m of `Inside`, enough cash. Plate is generated on the server (`BO` + 2 letters + 4 digits); only `model`, `plate` and config `props` are stored. | | `esx_boat:buyBoatLicense` | callback | none | Near a shop, enough cash, no `boat` license yet. Refunds if adding the license fails. | | `esx_boat:getGarage` | callback | none | Within 20 m of a `GaragePos`. Returns the JSON of stored boats. | | `esx_boat:storeVehicle` | callback | `plate` | Within 25 m of a `StorePos` and a boat with that plate within 20 m. Returns rows changed. | | `esx_boat:takeOutVehicle` | net event | `plate` | Within 20 m of a `GaragePos`, boat owned and stored. | <Callout type="warn" title="Security notes"> `esx_boat:takeOutVehicle` is a client-triggerable net event. It only flips `stored` to false in the database; the boat itself is spawned by the client. The license requirement is only checked on the client before the shop opens: `esx_boat:buyBoat` does not verify the `boat` license on the server. If the license matters on your server, add an `esx_license:checkLicense` check to that callback. </Callout> ## Integration tips [#integration-tips] - Other garage scripts can read boats with `SELECT * FROM owned_vehicles WHERE type = 'boat'`. - The client still calls `exports['esx_vehicleshop']:GeneratePlate()` before buying, which is why `esx_vehicleshop` must be running even though the server replaces the plate. - Blips: garages use sprite `356`, the shop uses sprite `427`, both color `3`. ## Common issues [#common-issues] - **"Unknown model" in the garage**: the boat's model is no longer in `Config.Vehicles`. Keep old entries or change the label lookup. - **Cannot store the boat**: you must be the driver and inside the red `StorePos` marker. The server also requires the boat with your plate to be within 20 m. - **Shop does not open, license menu appears every time**: `esx_license` is not started or the `boat` row is missing in `licenses`. - **Resource fails at start**: `esx_vehicleshop` is a hard dependency in `fxmanifest.lua`. Start it before esx_boat. ## Related [#related] - [esx_vehicleshop](/docs/addons/esx_vehicleshop) - [esx_garage](/docs/addons/esx_garage) - [esx_license](/docs/addons/esx_license) - [Database schema](/docs/guides/database-schema) # esx_clotheshop (/docs/addons/esx_clotheshop) esx_clotheshop places clothing stores around the map where players change their outfit through the `esx_skin` menu and pay with cash. After paying, players can optionally save the outfit to their wardrobe (the `property` datastore used by `esx_property`). ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_clotheshop` | | Side | Client + server | | Dependencies | `esx_lib`, `es_extended`, `esx_skin` (plus `oxmysql`) | | Optional | `esx_datastore` with a `property` datastore for saved outfits | | Database | None of its own | ### In-game flow [#in-game-flow] 1. Walk into a store marker and press **E**. 2. Change clothing in the restricted skin menu (t-shirt, torso, decals, arms, pants, shoes, bags, chain, helmet, glasses, watches). 3. Confirm the purchase. The server charges cash and saves the new skin through `esx_skin`. 4. If a `property` datastore exists, choose whether to save the outfit and type a name. If the player cancels, leaves the marker or cannot pay, the previous skin is restored. ## Installation [#installation] There is no SQL file. Make sure the dependencies start first: ```bash title="server.cfg" ensure esx_skin ensure esx_datastore ensure esx_clotheshop ``` To enable saved outfits, create the per-player datastore once: ```sql INSERT INTO `datastore` (`name`, `label`, `shared`) VALUES ('property', 'Property', 0); ``` ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale used for texts. | | `Config.Price` | `250` | Price of a purchase (or of each changed piece). | | `Config.ChargePerPiece` | `false` | Charge `Config.Price` per changed item in `Config.SkinProps` instead of a flat price. | | `Config.ShopDistance` | `3.0` | Max distance in meters from a shop that the server accepts for a purchase. | | `Config.PurchaseSessionDuration` | `60000` | Time in ms after paying during which the outfit can be saved. | | `Config.MaxOutfits` | `20` | Max outfits in the `dressing` list. New saves are ignored when full. | | `Config.MaxOutfitLabelLength` | `40` | Outfit names are trimmed to this length. Control characters are removed. | | `Config.DrawDistance` | `10.0` | Marker draw distance. | | `Config.MarkerSize` | `{ x = 1.5, y = 1.5, z = 1.0 }` | Marker size. `x` is also the interaction radius. | | `Config.MarkerColor` | `{ r = 102, g = 102, b = 204 }` | Marker color (alpha is fixed at 100). | | `Config.MarkerType` | `1` | Marker type. | | `Config.Shops` | 14 `vector3` positions | Store locations. Each also gets a blip (sprite `73`, color `47`). | | `Config.SkinProps` | `{ 'tshirt', 'torso', 'decals', 'arms', 'pants', 'shoes', 'bags', 'chain', 'helmet', 'glasses', 'watches' }` | Items compared for per-piece pricing (`<name>_1` and `<name>_2`). | ```lua title="config.lua" -- Charge 150 per changed piece, minimum 150 Config.Price = 150 Config.ChargePerPiece = true ``` ## Server callbacks and events [#server-callbacks-and-events] ### esx_clotheshop:buyClothes (callback) [#esx_clotheshopbuyclothes-callback] Triggered by the client with `(newSkin, oldSkin)`. Returns `bought, failure` where `failure` is `'not_enough_money'` or `'save_failed'`. Server-side safeguards: - The player must be within `Config.ShopDistance` of a shop. - `newSkin` is validated with `exports.esx_skin:ValidateSkin`. - The price is calculated against the skin stored on the server (`exports.esx_skin:GetSkin`), not the `oldSkin` sent by the client. - The money operation is wrapped in `beginAccountOperation`/`endAccountOperation` and refunded if `esx_skin:SaveSkin` fails. - Only one purchase per identifier can run at a time. ### esx_clotheshop:checkPropertyDataStore (callback) [#esx_clotheshopcheckpropertydatastore-callback] Returns `true` when the player has a `property` datastore. The client uses it to decide whether to show the save prompt. ### esx_clotheshop:saveOutfit (net event) [#esx_clotheshopsaveoutfit-net-event] ```lua TriggerServerEvent('esx_clotheshop:saveOutfit', label, skin) ``` <Callout type="info" title="Client-triggerable"> This event can be fired by any client. It is safe by design: it only works within `Config.PurchaseSessionDuration` after a successful purchase, ignores the `skin` argument and stores the skin that was actually paid for, sanitizes the label and respects `Config.MaxOutfits`. </Callout> Saved outfits are appended to `dressing` in the player's `property` datastore as `{ label = string, skin = table }`. ## Integration tips [#integration-tips] - `esx_property` reads the same `dressing` list, so outfits bought here show up in property wardrobes. - To read outfits from your own resource on the server: ```lua title="server/main.lua" TriggerEvent('esx_datastore:getDataStore', 'property', xPlayer.getIdentifier(), function(store) local outfits = store and store.get('dressing') or {} for i, outfit in ipairs(outfits) do print(i, outfit.label) end end) ``` - Payment uses the cash account (`xPlayer.removeMoney`). Edit `server/main.lua` if you want bank payments. ## Common issues [#common-issues] - **Purchase always fails with a "save failed" message**: the server distance check uses `Config.ShopDistance` (3.0). If you moved a shop marker, update the `Config.Shops` entry, not just the visual marker. - **Outfit not saved**: the `property` datastore is missing, the 60 second window expired, or the player already has `Config.MaxOutfits` outfits. - **Skin resets after buying**: `esx_skin` failed to save the skin. Check the server console; the purchase is refunded in that case. ## Related [#related] - [esx_skin](/docs/core/esx_skin) - [skinchanger](/docs/core/skinchanger) - [esx_datastore](/docs/addons/esx_datastore) - [esx_barbershop](/docs/addons/esx_barbershop) - [esx_property](/docs/addons/esx_property) # esx_cruisecontrol (/docs/addons/esx_cruisecontrol) esx_cruisecontrol is a client-only ESX Legacy addon that adds two driving features: cruise control that holds your current speed, and a seatbelt that blocks exiting the vehicle and prevents you from being thrown through the windshield in a crash. It pushes both states to [esx_hud](/docs/addons/esx_hud) by default. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_cruisecontrol` (version 1.2) | | Side | client only | | Dependencies | `esx_lib`, `es_extended` | | Optional | `esx_hud` (or any HUD set in `Config.HudResource`) | | Export | `isSeatbeltOn` (client) | **Cruise control** engages only when you are the driver, the engine runs and you are moving forward. It then applies throttle to hold the speed, with reduced throttle while steering. Each speed step is 10 mph and the maximum target is 120 mph; stepping down to 0 turns it off. It disengages on brake, handbrake, leaving the seat, engine off, standing still or reversing, or a sudden engine damage hit (more than 10 engine health in one frame). **Seatbelt** blocks the exit control while buckled. Unbuckled, a frontal crash at more than `EjectCheckSpeed` mph with a strong deceleration throws the player out of the vehicle and ragdolls them. The seatbelt unbuckles automatically when you leave the vehicle. Ejection only applies to regular vehicle classes (classes 0-7, 9-12 and 17-20; motorcycles, bicycles, boats, helicopters, planes and trains are excluded). ## Installation [#installation] No SQL is needed. ```bash title="server.cfg" ensure esx_lib ensure es_extended ensure esx_hud ensure esx_cruisecontrol ``` ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language. | | `Config.HudResource` | `'esx_hud'` | HUD resource that receives the state. Skipped if it is not started. | | `Config.Cruise.Enable` | `true` | Enables cruise control and its keybinds. | | `Config.Cruise.Key` | `'CAPITAL'` | Default key to toggle cruise control (Caps Lock). | | `Config.Cruise.Export` | `exports[Config.HudResource]:CruiseControlState(state)` | Function called with `true`/`false` when cruise control changes. | | `Config.Seatbelt.Enable` | `true` | Enables the seatbelt and ejection. | | `Config.Seatbelt.Key` | `'B'` | Default key to toggle the seatbelt. | | `Config.Seatbelt.EjectCheckSpeed` | `45` | Minimum speed in **mph** before an unbuckled player can be ejected. | | `Config.Seatbelt.RagdollTime` | `1` | How long the player ragdolls after ejection. The code multiplies it by 1000, so the value is in **seconds** (the config comment says MS). | | `Config.Seatbelt.Export` | `exports[Config.HudResource]:SeatbeltState(state)` | Function called with `true`/`false` when the seatbelt changes. | The increase and decrease speed keys are not in the config; their defaults are `ADD` and `SUBTRACT` (numpad plus and minus). ### Using another HUD [#using-another-hud] Replace the two `Export` functions with whatever your HUD expects: ```lua title="config.lua" Config = { Locale = GetConvar('esx:locale', 'en'), HudResource = 'my_hud', Cruise = { Enable = true, Key = 'CAPITAL', Export = function(state) exports['my_hud']:setCruise(state) end, }, Seatbelt = { Enable = true, Key = 'B', EjectCheckSpeed = 45, RagdollTime = 1, Export = function(state) exports['my_hud']:setBelt(state) end, }, } ``` If the export call errors, the resource prints the error and a hint, but keeps working. ## Keybinds [#keybinds] Registered through `xLib.addKeybind`, so players can rebind them in **Settings > Key Bindings > FiveM**. | Name | Default | Action | | --- | --- | --- | | `esx_cruisecontrol:Enable` | `Config.Cruise.Key` | Toggle cruise control | | `esx_cruisecontrol:IncreaseSpeed` | `ADD` | Raise target speed by 10 mph | | `esx_cruisecontrol:DecreaseSpeed` | `SUBTRACT` | Lower target speed by 10 mph | | `esx_cruisecontrol:ToggleSeatbelt` | `Config.Seatbelt.Key` | Toggle seatbelt | ## Exports [#exports] ### isSeatbeltOn (client) [#isseatbelton-client] ```lua local buckled = exports.esx_cruisecontrol:isSeatbeltOn() ``` Returns `true` when the local player's seatbelt is fastened. Useful for police fines or a custom HUD: ```lua title="client/main.lua" RegisterCommand('checkbelt', function() local buckled = exports.esx_cruisecontrol:isSeatbeltOn() ESX.ShowNotification(buckled and 'Seatbelt on' or 'Seatbelt off') end, false) ``` The resource registers no server events or callbacks. ## Common issues [#common-issues] - **Cruise control will not engage**: you must be the driver, moving forward with the engine running. - **Keys changed in config have no effect**: FiveM stores key mappings per player after the first bind. Existing players keep their old key until they rebind it. - **Console error about `CruiseControlState`**: `Config.HudResource` points to a resource that does not provide these exports. Adjust the `Export` functions. - **Players never get ejected**: ejection needs a frontal crash above `EjectCheckSpeed` mph with a very sharp deceleration, in a supported vehicle class. ## Related [#related] - [esx_hud](/docs/addons/esx_hud) - [esx_policejob](/docs/addons/esx_policejob) - [Client functions](/docs/es_extended/client/functions) # esx_datastore (/docs/addons/esx_datastore) esx_datastore is a server-only library resource that stores arbitrary Lua tables as JSON in MySQL. Other resources use it for things like society armories, saved outfits (`property` → `dressing`) and accessory slots (`user_mask`, `user_helmet`). Use it when you need small, persistent key-value data that is tied to a player or to a job. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_datastore` | | Side | Server only | | Dependencies | `esx_lib` (plus `es_extended` and `oxmysql` via imports) | | Tables | `datastore`, `datastore_data` | | Used by | `esx_society`, `esx_policejob`, `esx_ambulancejob`, `esx_taxijob`, `esx_property`, `esx_accessories`, `esx_clotheshop` | There are two kinds of datastores: - **Shared** (`shared = 1`): one store for the whole server, without an owner. Typical for job data such as `society_police`. - **Per player** (`shared = 0`): one store per character identifier. esx_datastore creates the missing `datastore_data` row for every non-shared datastore when a player loads (`esx:playerLoaded`). ## Installation [#installation] <Steps> <Step> ### Database [#database] The resource loads the esx_lib SQL catalog on start. It creates the `datastore` and `datastore_data` tables automatically if they are missing. `esx_datastore.sql` contains the same schema if you prefer to import it manually: ```sql title="esx_datastore.sql" CREATE TABLE `datastore` ( `name` VARCHAR(60) NOT NULL, `label` VARCHAR(100) NOT NULL, `shared` INT NOT NULL, PRIMARY KEY (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE `datastore_data` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(60) NOT NULL, `owner` VARCHAR(60), `data` LONGTEXT, PRIMARY KEY (`id`), UNIQUE INDEX `index_datastore_data_name_owner` (`name`, `owner`), INDEX `index_datastore_data_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ``` </Step> <Step> ### server.cfg [#servercfg] Start it after `esx_lib` and `es_extended` and before every resource that reads datastores: ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ensure esx_datastore ``` The default txAdmin recipe already starts it as part of `[esx_addons]`. </Step> <Step> ### Register a datastore [#register-a-datastore] Add one row per datastore. `label` is informational only. ```sql INSERT INTO `datastore` (`name`, `label`, `shared`) VALUES ('society_mafia', 'Mafia', 1), -- one store for the whole job ('property', 'Property', 0); -- one store per player ``` Restart the server (or restart `esx_datastore` and relog players) so the new row is loaded. </Step> </Steps> <Callout type="info" title="Seeded datastores"> Some addons seed their own rows through the esx_lib SQL catalog: `esx_accessories` (`user_ears`, `user_glasses`, `user_helmet`, `user_mask`) and the job addons (`society_ambulance`, `society_mechanic`, `society_police`, `society_taxi`). When such a resource seeds new rows, esx_datastore receives `esx:sqlCatalogReady` and loads them without a restart. </Callout> ## Configuration [#configuration] esx_datastore has no `config.lua`. Everything is driven by the rows in the `datastore` table. | Column | Type | Description | | --- | --- | --- | | `name` | `VARCHAR(60)` | Unique datastore name used in code. | | `label` | `VARCHAR(100)` | Display label. Not used by the resource. | | `shared` | `INT` | `1` = one shared store, `0` = one store per player. | ## Server events [#server-events] All three events are registered with `AddEventHandler` only, so they can be triggered from other **server** resources but not from clients. The callback runs synchronously. ### esx_datastore:getSharedDataStore [#esx_datastoregetshareddatastore] ```lua TriggerEvent('esx_datastore:getSharedDataStore', name, cb) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Name of a datastore with `shared = 1`. | | `cb` | `function(store)` | Receives the store object, or `nil` if it does not exist. | ```lua title="server/main.lua" TriggerEvent('esx_datastore:getSharedDataStore', 'society_police', function(store) if not store then return end local weapons = store.get('weapons') or {} weapons[#weapons + 1] = { name = 'WEAPON_PUMPSHOTGUN', count = 1 } store.set('weapons', weapons) end) ``` ### esx_datastore:getDataStore [#esx_datastoregetdatastore] ```lua TriggerEvent('esx_datastore:getDataStore', name, owner, cb) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Name of a datastore with `shared = 0`. | | `owner` | `string` | Character identifier (`xPlayer.getIdentifier()`). | | `cb` | `function(store)` | Receives the store object, or `nil` if it does not exist. | ```lua title="server/main.lua" local xPlayer = ESX.GetPlayerFromId(source) TriggerEvent('esx_datastore:getDataStore', 'property', xPlayer.getIdentifier(), function(store) if not store then return end local dressing = store.get('dressing') or {} print(('%s has %d saved outfits'):format(xPlayer.getName(), #dressing)) end) ``` ### esx_datastore:getDataStoreOwners [#esx_datastoregetdatastoreowners] ```lua TriggerEvent('esx_datastore:getDataStoreOwners', name, cb) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Name of a non-shared datastore. | | `cb` | `function(owners)` | Receives an array of identifiers that have a row for this datastore. | ## Store object [#store-object] Every store returned by the events above has these fields and functions: | Member | Description | | --- | --- | | `store.name` | Datastore name. | | `store.owner` | Owner identifier, or `nil` for shared stores. | | `store.data` | The raw Lua table. Prefer `get`/`set` so changes are saved. | | `store.get(key, i?)` | Returns `data[key]`. Dotted keys walk nested tables (`'outfits.work'`). If `i` is given and the value is a table, returns `value[i]`. Returns `nil` for empty or non-string keys. | | `store.set(key, value)` | Sets `data[key]` and schedules a save. Returns `false` for empty or non-string keys. Dotted keys are **not** split on set. | | `store.count(key, i?)` | Length (`#`) of the table at `key`, or `0`. | | `store.save()` | Marks the store dirty and schedules a save. Called by `set` automatically. | | `store.flush()` | Writes pending changes immediately. Blocks the current thread until MySQL answers. | | `store.isDirty()` | `true` while unsaved changes exist or a write is in progress. | ### Saving behavior [#saving-behavior] - Saves are debounced through `xLib.timeout`: the last change wins and the JSON is written **10 seconds** after it. - A failed write is logged (`[esx_datastore] Save failed for ...`) and rescheduled. - On `onResourceStop` all stores are flushed, so a clean restart of esx_datastore does not lose data. <Callout type="warn"> `get` returns the live table, not a copy. If you modify a nested table in place you must still call `store.set(key, value)` (or `store.save()`), otherwise the change never reaches the database. </Callout> ## Integration tips [#integration-tips] - Name society stores `society_<job>`. `esx_society` and the job addons expect that pattern. - Use a per-player store (`shared = 0`) instead of new `users` columns for small optional data like favorites or saved presets. - Keep values small. Every save writes the whole JSON blob of that store. - Because `getDataStore` callbacks run synchronously, you can capture the store in a local variable and use it after the `TriggerEvent` call. ## Common issues [#common-issues] - **`attempt to index a nil value (local 'store')`**: the datastore row is missing or was added after the resource started. Insert the row and restart. - **Outfits are not saved in the clothes shop**: `esx_clotheshop` writes to a per-player datastore named `property`. No addon seeds it, so insert `('property', 'Property', 0)` yourself. - **Data lost after killing the server process**: changes from the last ~10 seconds are only in memory. Stop the server normally so stores get flushed. ## Related [#related] - [esx_addonaccount](/docs/addons/esx_addonaccount) - [esx_addoninventory](/docs/addons/esx_addoninventory) - [esx_society](/docs/addons/esx_society) - [esx_clotheshop](/docs/addons/esx_clotheshop) - [Database schema](/docs/guides/database-schema) # esx_death (/docs/addons/esx_death) esx_death is the global death system for ESX Legacy. It shows a branded death screen when a player dies, persists the dead state in the database, runs the early-respawn and bleedout timers, applies respawn penalties and exposes exports to revive players. [esx_ambulancejob](/docs/addons/esx_ambulancejob) builds on it for EMS revives and distress calls. <Callout type="info"> esx_death has no page in the official ESX documentation. This reference is written from the resource source (version 1.0.0). </Callout> ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_death` | | Side | Client + server + NUI | | Dependencies | `es_extended`, `esx_lib`, `oxmysql`, OneSync (`/onesync`) | | Database | `users.is_dead`, `users.death_time` | | Commands | `/revive`, `/reviveall` (group `admin`) | | Required by | `esx_ambulancejob` | ### How it works [#how-it-works] 1. es_extended detects the death on the client and triggers `esx:onPlayerDeath` (client and server) with killer data. 2. esx_death stores `is_dead = 1` and `death_time` (unix seconds), sets the `isDead` state bag and opens the death screen. 3. After `Config.EarlyRespawnTimer` the player can hold **E** for `Config.HoldDuration` ms to respawn at the closest `Config.RespawnPoints` entry. Respawning before the bleedout ends costs `Config.EarlyRespawnFineAmount` from the bank when `Config.EarlyRespawnFine` is enabled. 4. When the bleedout timer (`EarlyRespawnTimer + BleedoutTimer`) runs out, the client respawns automatically without a fine. 5. On respawn the configured penalties remove items, cash, black money and weapons. A revive (EMS, admin, txAdmin) keeps everything. ### Death screen controls [#death-screen-controls] | Key | Action | | --- | --- | | **E** (hold) | Respawn when allowed. | | **G** | Send a distress signal (cooldown `Config.DistressCooldown`). | | **F2** | Toggle the mouse cursor to click the NUI buttons. | Chat (**T**) and push-to-talk stay enabled. All other controls are disabled while dead. ## Installation [#installation] <Steps> <Step> ### Database [#database] With `Config.AutoMigrate = true` (default) the server adds the two `users` columns on start. To migrate manually, run `esx_death.sql`, which adds them only if missing: ```sql ALTER TABLE `users` ADD COLUMN `is_dead` TINYINT(1) NOT NULL DEFAULT 0; ALTER TABLE `users` ADD COLUMN `death_time` BIGINT NULL DEFAULT NULL; ``` The resource runs `SELECT is_dead, death_time FROM users LIMIT 0` at start, so it fails loudly if the columns are missing. </Step> <Step> ### server.cfg [#servercfg] OneSync must be enabled. Start esx_death before esx_ambulancejob: ```bash title="server.cfg" set onesync on ensure esx_lib ensure es_extended ensure esx_death ensure esx_ambulancejob ``` </Step> </Steps> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.Debug` | `false` | Print detailed client and server debug logs. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale for the death screen and commands. | | `Config.DeathAnim` | `{ enabled = true, dict = 'misslamar1dead_body', name = 'dead_idle', fadeIn = 10.0, fadeOut = 10.0, flags = 1 \| 2 \| 8, playbackRate = 1.0 }` | Looping animation while dead. | | `Config.zoom` | `{ min = 1, max = 6, step = 0.5 }` | Zoom limits for the death camera. | | `Config.MedalClip` | `true` | Trigger a Medal.tv clip on death through `xLib.medal.triggerClip()`. | | `Config.EarlyRespawnTimer` | `60000 * 1` | Time in ms before respawn becomes available. | | `Config.BleedoutTimer` | `60000 * 10` | Additional time in ms until the player bleeds out and respawns automatically. | | `Config.RemoveWeaponsAfterRPDeath` | `true` | Remove all weapons on respawn (ESX inventory only). | | `Config.RemoveCashAfterRPDeath` | `true` | Remove all cash and black money on respawn. | | `Config.RemoveItemsAfterRPDeath` | `true` | Clear the inventory on respawn (`ox_inventory:ClearInventory` with ox_inventory). | | `Config.EarlyRespawnFine` | `false` | Charge a bank fine when respawning before the bleedout ends. | | `Config.EarlyRespawnFineAmount` | `5000` | Amount of that fine. Respawn is refused if the bank balance is too low. | | `Config.OxInventory` | `ESX.GetConfig('CustomInventory') == 'ox' or GetResourceState('ox_inventory') == 'started'` | Detected automatically. Switches item and weapon handling to ox_inventory. | | `Config.RespawnPoints` | 2 points | `{ coords = vector3, heading = number }` list. The closest point is used. | | `Config.HoldDuration` | `1500` | Ms to hold **E** for respawn. | | `Config.DistressCooldown` | `60000` | Ms between distress signals. | | `Config.ShowDeathReason` | `true` | Show "killed by a player" under the buttons for player kills. | | `Config.ShowKillerName` | `false` | Show the killer's name instead (requires `ShowDeathReason`). | | `Config.CameraEnabled` | `false` | Use the orbit death camera instead of the normal camera. | | `Config.AutoMigrate` | `true` | Add missing `users.is_dead` / `users.death_time` columns on start. | Default respawn points: ```lua title="config.lua" Config.RespawnPoints = { { coords = vector3(341.0, -1397.3, 32.5), heading = 48.5 }, -- Central Los Santos { coords = vector3(1836.03, 3670.99, 34.28), heading = 296.06 } -- Sandy Shores } ``` ## Server exports [#server-exports] ### IsDead [#isdead] ```lua exports.esx_death:IsDead(playerId) --> boolean ``` Returns `true` if the cached state of the player is dead. Returns `false` for players whose state is not loaded yet. ### GetDeathState [#getdeathstate] ```lua exports.esx_death:GetDeathState(playerId) --> table | nil ``` Returns a snapshot or `nil` when no state is cached: | Field | Type | Description | | --- | --- | --- | | `dead` | `boolean` | Current state. | | `elapsed` | `number` | Seconds since death. | | `earlyRemaining` | `number` | Seconds until respawn is allowed. | | `remaining` | `number` | Seconds until bleedout. | | `total` | `number` | Total bleedout length in seconds. | | `distressRemaining` | `number` | Seconds until the next distress signal. | | `distressSent` | `boolean` | A distress signal was sent during this death. | | `fine` | `number` | Early respawn fine, `0` when disabled. | | `killedByPlayer` | `boolean` | Death was caused by another player. | | `killerServerId` | `number?` | Validated killer server id. | | `deathCause` | `number\|string?` | Weapon hash or cause name. | ### GetDeadPlayers [#getdeadplayers] ```lua exports.esx_death:GetDeadPlayers() --> { [playerId] = 'dead' | 'distress' } ``` ### GetDeadPlayerLocations [#getdeadplayerlocations] ```lua exports.esx_death:GetDeadPlayerLocations() --> { [playerId] = { x, y, z } } ``` ### Revive [#revive] ```lua exports.esx_death:Revive(playerId, reason?) --> ok, wasDead ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | `number` | Server id of the player. | | `reason` | `string?` | Passed to `esx_death:stateChanged`. Defaults to `'revive'`. | Revives in place, keeps the inventory, sets hunger and thirst to 500000 through `esx_status:set` and clears the dead flag in the database. Returns `false, false` if the player was not dead. ```lua title="server/main.lua" local ok = exports.esx_death:Revive(targetId, 'mymedic') if ok then TriggerClientEvent('esx:showNotification', targetId, 'You have been revived.') end ``` ### SetDead [#setdead] ```lua exports.esx_death:SetDead(playerId) --> boolean ``` Marks a player as dead in the database and syncs the death screen to that client. This does not kill the ped; use it together with your own logic. ## Client exports [#client-exports] ```lua exports.esx_death:IsDead() --> boolean exports.esx_death:GetDeathState() --> { dead, earlyRemaining, remaining, distressRemaining } ``` The server also sets the replicated state bag `isDead`, so `LocalPlayer.state.isDead` (client) and `Player(src).state.isDead` (server) work without exports. ## Events [#events] ### Server events (local) [#server-events-local] | Event | Arguments | When | | --- | --- | --- | | `esx_death:stateChanged` | `playerId`, `dead`, `reason` | State flips. `reason` is `'death'`, `'respawn'`, `'revive'`, `'admin'`, `'txadmin'`, `'restore'`, `'logout'` or a custom revive reason. | | `esx_death:distress` | `playerId`, `coords` | A dead player sent a distress signal. esx_ambulancejob uses it to alert medics. | ```lua title="server/main.lua" AddEventHandler('esx_death:stateChanged', function(playerId, dead, reason) if dead then print(('Player %s died'):format(playerId)) elseif reason == 'respawn' then print(('Player %s respawned at a hospital'):format(playerId)) end end) ``` ### Client events (local) [#client-events-local] | Event | Arguments | When | | --- | --- | --- | | `esx_death:entered` | none | The death screen opened. | | `esx_death:recovered` | `'respawn'` or `'revive'` | The player was respawned or revived. | | `esx_death:requestDistress` | none | Trigger it to send a distress signal (same as pressing **G**). | On recovery esx_death also fires `esx:onPlayerSpawn` (client and server), `playerSpawned` and `esx_basicneeds:resetStatus`. ### Net events and callbacks (security) [#net-events-and-callbacks-security] | Name | Type | Notes | | --- | --- | --- | | `esx:onPlayerDeath` | net event | Client-triggerable. A client can only mark **itself** dead. Rate limited (3 per 5 s), killer id and cause are sanitized, timestamps and penalties are always set by the server. | | `esx_ambulancejob:onPlayerDistress` | net event | Client-triggerable. Only works while the sender is dead and respects `Config.DistressCooldown`. | | `esx_death:respawn` | callback | Refused if not dead, too early or the fine cannot be paid. | | `esx_death:distress`, `esx_death:getState` | callbacks | Only affect the calling player. | | `esx_death:sync`, `esx_death:recover`, `esx_ambulancejob:clsearch` | client net events | The client ignores them unless they come from the server. | ## Commands [#commands] | Command | Group | Arguments | Description | | --- | --- | --- | --- | | `/revive` | `admin` | `playerId` | Revive one player. Also works from the server console. | | `/reviveall` | `admin` | none | Revive every online player. | The txAdmin "Heal" action (`txAdmin:events:healedPlayer`) revives the player, or everyone when txAdmin heals all. ## Integration tips [#integration-tips] - Block actions while dead (shops, phones, inventories) with `LocalPlayer.state.isDead` on the client and `exports.esx_death:IsDead(source)` on the server. - Use `Revive` instead of calling `NetworkResurrectLocalPlayer` yourself; otherwise the database still says dead and the player is restored as dead on the next login. - To change who gets alerted on distress, listen to `esx_death:distress` in your own dispatch resource. ## Common issues [#common-issues] - **SQL error about `is_dead` on start**: the `users` table lacks `is_dead`/`death_time` and `Config.AutoMigrate` is off. Run `esx_death.sql`. - **Two death screens or instant respawn**: another death or revive script is still running. Only esx_death should handle death. - **Weapons not removed with ox_inventory**: with ox_inventory, weapons are items. Use `Config.RemoveItemsAfterRPDeath` instead of the weapons option. - **Player respawns dead after relog**: expected when they logged out while dead. Revive them or let the timer run out. ## Related [#related] - [esx_ambulancejob](/docs/addons/esx_ambulancejob) - [esx_basicneeds](/docs/addons/esx_basicneeds) - [Client events](/docs/es_extended/events/client) - [Commands](/docs/es_extended/commands) - [OneSync functions](/docs/es_extended/server/onesync) # esx_dmvschool (/docs/addons/esx_dmvschool) esx_dmvschool is the driving school for ESX Legacy. Players first pass a multiple-choice theory test (`dmv` license) and then take practical road tests for cars (`drive`), motorcycles (`drive_bike`) and trucks (`drive_truck`). Licenses are stored through [esx_license](/docs/addons/esx_license), so police and vehicle scripts can check them. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_dmvschool` | | Side | Client + server + NUI | | Dependencies | `esx_lib`, `es_extended`, `esx_license` (plus `oxmysql`) | | Licenses | `dmv`, `drive`, `drive_bike`, `drive_truck` | ### In-game flow [#in-game-flow] 1. Go to the DMV marker (blip sprite `408`) and press **E**. 2. Pay for the theory test. Answer 10 questions; 5 correct answers pass and grant `dmv`. 3. With `dmv`, the menu offers the road tests you do not own yet. The test vehicle spawns at `VehicleSpawnPoint`. 4. Drive through all checkpoints. Speeding and vehicle damage count as errors. Speed limits switch between `residence`, `town` and `freeway` at certain checkpoints. 5. At the last checkpoint the vehicle is deleted and, if you passed, the license is granted. ## Installation [#installation] <Steps> <Step> ### Database [#database] esx_dmvschool uses the esx_lib SQL catalog and inserts the four `licenses` rows automatically, labeled in your `esx:locale`. The bundled `esx_dmvschool.sql` contains French labels; localized versions live in `localization/`. ```sql title="esx_dmvschool.sql (English labels)" INSERT INTO `licenses` (`type`, `label`) VALUES ('dmv', 'Driving Permit'), ('drive', 'Drivers License'), ('drive_bike', 'Motorcycle License'), ('drive_truck', 'Commercial Drivers License'); ``` </Step> <Step> ### server.cfg [#servercfg] ```bash title="server.cfg" ensure esx_license ensure esx_dmvschool ``` </Step> </Steps> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.DrawDistance` | `10.0` | Marker draw distance. | | `Config.MaxErrors` | `5` | Error budget. More than this ends the test early; you pass only with fewer than this. | | `Config.SpeedMultiplier` | `3.6` | Converts m/s to your unit. `3.6` = km/h, `2.236936` = mph. | | `Config.SpeedingErrorDelay` | `5000` | Cooldown in ms after a speeding error before the next one can count. Values below 5000 are raised to 5000. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale used for texts. | | `Config.Prices` | see below | Price per test type, paid in cash. | | `Config.VehicleModels` | see below | Vehicle model per road test. | | `Config.SpeedLimits` | see below | Speed limit per zone type, in the unit of `SpeedMultiplier`. | | `Config.Zones` | `DMVSchool`, `VehicleSpawnPoint` | Marker zones. `Type = -1` hides a marker. | | `Config.CheckPoints` | 16 checkpoints | Ordered route with `Pos` and an `Action` function. | ```lua title="config.lua (defaults)" Config.Prices = { dmv = 500, drive = 2500, drive_bike = 3000, drive_truck = 5000 } Config.VehicleModels = { drive = 'blista', drive_bike = 'sanchez', drive_truck = 'mule3' } Config.SpeedLimits = { residence = 50, town = 80, freeway = 120 } ``` ### Checkpoints [#checkpoints] Each checkpoint runs its `Action` when the player reaches it. The function receives the player ped, the test vehicle and a setter for the current speed zone: ```lua title="config.lua" { Pos = { x = -453.790, y = -1444.726, z = 27.665 }, Action = function(playerPed, vehicle, setCurrentZoneType) setCurrentZoneType('freeway') DrawMissionText(TranslateCap('hway_time', Config.SpeedLimits['freeway']), 5000) end }, ``` The last checkpoint must stay near the DMV: the server only grants road-test licenses when the player is within 35 m of the **last** checkpoint, and its default action deletes the test vehicle. <Callout type="info"> Adding a new test type needs three changes: a `Config.Prices` entry, a `Config.VehicleModels` entry and a matching row in `licenses`. The server rejects any type that is missing from `Config.Prices` or (for road tests) `Config.VehicleModels`. The menu itself only lists the three built-in road tests, so add a menu entry in `client/main.lua` too. </Callout> ## Server callbacks and events [#server-callbacks-and-events] ### esx_dmvschool:canYouPay (callback) [#esx_dmvschoolcanyoupay-callback] Called with a test type. The server checks that the type is valid, the player is within 20 m of the DMV marker, does not own that license yet and has enough cash. It then removes the money and records a paid test that is valid for **15 minutes**. ### esx_dmvschool:addLicense (net event) [#esx_dmvschooladdlicense-net-event] ```lua TriggerServerEvent('esx_dmvschool:addLicense', type) ``` <Callout type="warn" title="Client-triggerable"> Any client can fire this event. The server only grants the license if the player paid for that exact test in the last 15 minutes and stands near the DMV (theory) or the last checkpoint (road tests). The test result itself is decided on the client (NUI quiz and checkpoint loop), so a modified client that paid could skip the actual test. Treat DMV licenses as roleplay gates, not as hard security. </Callout> ### esx_dmvschool:loadLicenses (client event) [#esx_dmvschoolloadlicenses-client-event] Sent by the server with the player's license list on `esx:playerLoaded` and after a license is added. The client uses it to build the menu. ## Integration tips [#integration-tips] - Check licenses from other resources with esx_license: ```lua title="server/main.lua" TriggerEvent('esx_license:checkLicense', source, 'drive', function(hasLicense) if not hasLicense then TriggerClientEvent('esx:showNotification', source, 'You need a drivers license.') end end) ``` - Police can revoke `drive*` licenses through [esx_policejob](/docs/addons/esx_policejob), which uses esx_license as well. - The test vehicle gets 100 fuel and the `_FUEL_LEVEL` decor, so legacy fuel scripts do not drain it. ## Common issues [#common-issues] - **Prices or menu text show the wrong language**: set `setr esx:locale "en"` before resources start. The license labels come from the SQL catalog in the same locale. - **Paid but no license**: the paid test expired (15 minutes) or you were too far from the last checkpoint when it finished. - **Speeding errors on the first meters**: if you use mph limits, also change `Config.SpeedMultiplier` to `2.236936`. ## Related [#related] - [esx_license](/docs/addons/esx_license) - [esx_policejob](/docs/addons/esx_policejob) - [esx_boat](/docs/addons/esx_boat) - [Server events](/docs/es_extended/events/server) # esx_drugs (/docs/addons/esx_drugs) esx_drugs is a small weed economy for ESX Legacy: players pick `cannabis` plants in a field, process them into `marijuana` and sell it to a dealer for black money. The server validates every step by position and rate limits, so it is a reasonable base for your own drug scripts. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_drugs` | | Side | Client + server | | Dependencies | `esx_lib`, `es_extended` (plus `oxmysql`) | | Optional | `esx_license` when `Config.LicenseEnable = true` | | Items | `cannabis` (weight 3), `marijuana` (weight 2) | | License | `weed_processing` | ### In-game flow [#in-game-flow] 1. **Harvest**: near the weed field the client spawns 25 local `prop_weed_02` plants within 50 m of the field center. Press **E** next to a plant to pick it. After about 3 seconds the server gives 1 to 3 `cannabis`. 2. **Process**: stand on the processing spot and press **E**. Every `Config.Delays.WeedProcessing` ms the server converts 3 `cannabis` into 1 `marijuana` until you run out, walk away or your inventory is full. 3. **Sell**: at the dealer marker press **E**, choose an item and an amount between `Config.SellMenu.Min` and `Config.SellMenu.Max`. ## Installation [#installation] <Steps> <Step> ### Database [#database] The resource uses the esx_lib SQL catalog and inserts the items and the license automatically in your `esx:locale`. The manual equivalent is `esx_drugs.sql`: ```sql title="esx_drugs.sql" INSERT INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES ('cannabis', 'Cannabis', 3, 0, 1), ('marijuana', 'Marijuana', 2, 0, 1); INSERT INTO `licenses` (`type`, `label`) VALUES ('weed_processing', 'Weed Processing License'); ``` If you use ox_inventory, add the two items to `ox_inventory/data/items.lua` instead. See [ox_inventory](/docs/guides/ox-inventory). </Step> <Step> ### server.cfg [#servercfg] ```bash title="server.cfg" ensure esx_license # only needed with Config.LicenseEnable = true ensure esx_drugs ``` </Step> </Steps> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale used for texts. | | `Config.Delays.WeedProcessing` | `1000 * 7` | Time in ms per processing step (3 cannabis → 1 marijuana). | | `Config.DrugDealerItems` | `{ marijuana = 91 }` | Items the dealer buys and the price per unit. | | `Config.LicenseEnable` | `false` | Ask for the `weed_processing` license before processing. Needs `esx_license`. | | `Config.LicensePrices` | `{ weed_processing = { label = TranslateCap('license_weed'), price = 15000 } }` | License label and cash price. | | `Config.GiveBlack` | `true` | Pay sales into `black_money`. `false` pays cash. | | `Config.CircleZones.WeedField` | `vector3(2220.72, 5582.52, 53.81)`, radius `100.0` | Field center. Blip color `25`, sprite `496`, plus a radius blip. | | `Config.CircleZones.WeedProcessing` | `vector3(2329.02, 2571.29, 46.68)` | Processing spot. Blip color `25`, sprite `496`. | | `Config.CircleZones.DrugDealer` | `vector3(-1172.02, -1571.98, 4.66)` | Dealer spot. Blip color `6`, sprite `378`. | | `Config.Marker.Distance` | `100.0` | Distance at which the dealer marker is drawn. | | `Config.Marker.Color` | `{ r = 60, g = 230, b = 60, a = 255 }` | Dealer marker color. | | `Config.Marker.Size` | `vector3(1.5, 1.5, 1.0)` | Dealer marker size. | | `Config.Marker.Type` | `1` | Dealer marker type. | | `Config.SellMenu.Min` | `1` | Minimum amount per sale. | | `Config.SellMenu.Max` | `50` | Maximum amount per sale. The server rejects (and kicks for) amounts outside this range. | ### Hard-coded server values [#hard-coded-server-values] These are local constants at the top of `server/main.lua`, not config keys: | Constant | Value | Meaning | | --- | --- | --- | | `PickupCooldown` | `3500` | Min ms between pickups (rate limiter). | | `PickupDuration` | `3000` | Delay before cannabis is added. | | `PickupDistance` / `PickupFieldDistance` | `3.0` / `55.0` | Max distance to the plant / to the field center. | | `ProcessDistance` | `5.0` | Max distance to the processing spot. | | `SellDistance` / `SellCooldown` | `3.0` / `1500` | Max distance to the dealer / min ms between sales. | | `MinCannabisPickup` / `MaxCannabisPickup` | `1` / `3` | Cannabis per plant. | <Callout type="warn"> `Config.CircleZones.WeedField.radius` only affects the map blip. Plants spawn within 50 m and the server accepts pickups within 55 m of the field center, so a larger radius does not make the field bigger. </Callout> ## Server events and callbacks [#server-events-and-callbacks] All net events below can be triggered by clients. The server validates them as follows. | Name | Type | Arguments | Server checks | | --- | --- | --- | --- | | `esx_drugs:pickedUpCannabis` | net event | `plantCoords` | Rate limited. Plant and player must be inside the field and within 3 m of each other; re-checked after 3 s. Fails → player is dropped. | | `esx_drugs:processCannabis` | net event | none | Within 5 m of `WeedProcessing`, at least 3 cannabis, one loop per player. Fails position check → player is dropped. | | `esx_drugs:sellDrug` | net event | `itemName`, `amount` | Rate limited, within 3 m of the dealer, integer amount within `SellMenu`, item listed in `DrugDealerItems`, enough items. Wrong types, position or amount → player is dropped. | | `esx_drugs:cancelProcessing` / `esx_drugs:outofbound` | net event | none | Stops the player's processing loop. | | `esx_drugs:buyLicense` | callback | `licenseName` | License must exist in `Config.LicensePrices` and the player must have the cash. | | `esx_drugs:canPickUp` | callback | `item` | Returns whether the player can carry 1 `cannabis`. | | `esx_drugs:cannabis_count` | callback | none | Returns the player's cannabis count. | <Callout type="error" title="License is client-side only"> With `Config.LicenseEnable = true` the license is only checked on the client before processing starts. `esx_drugs:processCannabis` does not check `weed_processing` on the server, and `esx_drugs:buyLicense` has no distance check. Add an `esx_license:checkLicense` check in the server event if the license must be enforced. </Callout> The kick function is `FoundExploiter(src, reason)` in `server/main.lua`. Replace `DropPlayer` there with your ban system if you want stricter handling. ## Integration tips [#integration-tips] - To sell more drugs at the dealer, add items to `Config.DrugDealerItems` (for example `{ marijuana = 91, cocaine = 150 }`) and create the items. The menu lists every item from that table that the player carries. - Sales use `xPlayer.addAccountMoney('black_money', ...)` with the reason `"Drugs Sold"`, which shows up in ESX money logs. - esx_drugs stops processing for a player on `esx:onPlayerDeath` and on disconnect. ## Common issues [#common-issues] - **Plants spawn in the air or underground**: plant height is searched between Z 48 and 58. If you move the field, adjust `GetCoordZ` in `client/weed.lua`. - **Dealer menu is empty**: the dealer menu lists items from `ESX.GetPlayerData().inventory` on the client. With a custom inventory, make sure it keeps ESX player data in sync. - **Processing stops after one step**: the player cannot carry the marijuana (`canSwapItem` failed) or has fewer than 3 cannabis. ## Related [#related] - [Add items](/docs/guides/add-items) - [esx_license](/docs/addons/esx_license) - [Money and accounts](/docs/guides/money-and-accounts) - [xPlayer](/docs/es_extended/server/xplayer) # esx_garage (/docs/addons/esx_garage) esx_garage is the ESX Legacy garage and impound system. Players park owned vehicles at garages, take them out again through a React UI, favorite, rename or transfer them, and pay a fee to recover vehicles from an impound lot. Version 2.0 replaced the old marker-based config completely, so configs from older versions do not work. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_garage` (version 2.0.0) | | Side | client + server, NUI (`web/dist`) | | Dependencies | `esx_lib`, `es_extended`, `oxmysql` | | Optional | `esx_hud` (mileage shown in the UI) | | Data | `owned_vehicles` plus columns `parking`, `pound`, `custom_name`, `is_favorite`, `last_used`, `mileage`; table `esx_garage_migrations` | Features in-game: - Garages with an entry point (opens the menu), an optional store point and an optional NPC. - Impound lots with a per-lot fee. The fee is taken from cash first, then from the bank. - Paginated vehicle list with search and filters (stored, impounded, favorite). - Rename and favorite vehicles, transfer a stored vehicle to a player within 10 m. - Optional "give keys" hook for a key script. ### Vehicle state [#vehicle-state] | `stored` | `parking` | `pound` | Meaning | | --- | --- | --- | --- | | `1` | garage id or `NULL` | `NULL` | Parked in a garage | | `1` | `NULL` | impound id | Impounded | | `0` | `NULL` | `NULL` | Out in the world | A vehicle that is out (`stored = 0`) but whose entity no longer exists can only be recovered at an impound, never at a garage. Before respawning it the server deletes every live entity with that plate, so it cannot be duplicated. ## Installation [#installation] <Steps> <Step> ### Database [#database] Nothing to do on an existing server: on start the resource adds missing columns and indexes, converts legacy `stored = 2` rows to the new impound state and records version `1.16.0` in `esx_garage_migrations`. For a fresh or manual install you can import `esx_garage.sql`; every statement is guarded, so it is safe to run again. </Step> <Step> ### Ensure the resource [#ensure-the-resource] ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ensure esx_garage ``` </Step> </Steps> ## Configuration [#configuration] ### Config.Settings [#configsettings] | Key | Default | Description | | --- | --- | --- | | `interactionDistance` | `3.0` | Distance to a garage or impound point to interact. | | `restrictToGarage` | `false` | Only allow retrieval at the garage stored in `parking`. `NULL` or removed garages count as "any garage". | | `defaultImpoundFee` | `400` | Fee for impound lots without their own `cost`. | | `vehicleKeys` | `false` | Shows the give keys button and enables the `esx_garage:giveKeys` callback. | | `storeMarkerOnlyInVehicle` | `true` | Only draw the store marker while sitting in a vehicle. | | `showMarker` | `true` | Draw ground markers at the points. | | `vehiclesPerPage` | `30` | Default page size of the vehicle list. | | `maxVehiclesPerMenu` | `100` | Largest page size a client may request (hard capped at 100). | `Config.Locale` defaults to `GetConvar('esx:locale', 'en')`. ### Config.Garages [#configgarages] A list of garages. Defaults: `vespucci_boulevard` and `san_andreas_avenue`. | Field | Type | Description | | --- | --- | --- | | `id` | `string` | Unique id, stored in `owned_vehicles.parking`. Do not rename it once used. | | `label` | `string` | Display name and blip label. | | `type` | `'public' \| 'job' \| 'gang' \| 'impound'` | Passed to the UI. Access is not derived from it; use `access`. | | `entryPoint` | `vector3` | Opens the menu. | | `storePoint` | `vector3?` | Where vehicles are parked. | | `spawns` | `vector4[]` | Spawn positions; at least one. | | `blip` | `{ sprite, scale, color }?` | Map blip. | | `ped` | `{ model, z?, heading?, snapToGround? }?` | Optional NPC. | | `access` | `{ jobs?, authorize? }?` | `jobs` maps job name to minimum grade. `authorize(source, garage)` is a server-side function that returns `true` to allow. | | `logo` | `string?` | Image URL shown in the menu header. | | `color` | `string?` | Hex accent color for the menu. | | `pound` | `string?` | Linked impound id. | ```lua title="config.lua" Config.Garages[#Config.Garages + 1] = { id = 'mrpd', label = 'Mission Row PD', type = 'job', entryPoint = vec3(454.6, -1017.4, 28.4), storePoint = vec3(447.6, -1021.5, 28.5), spawns = { vec4(438.4, -1018.3, 27.7, 90.0) }, blip = { sprite = 357, scale = 0.8, color = 38 }, access = { jobs = { police = 0 } }, pound = 'los_santos', } ``` ### Config.Impounds [#configimpounds] Defaults: `los_santos`, `paleto_bay`, `sandy_shores`, each with `cost = 3000`. The **first** entry is the default lot for automatic impounds. | Field | Type | Description | | --- | --- | --- | | `id` | `string` | Unique id, stored in `owned_vehicles.pound`. | | `label` | `string` | Display name. | | `getOutPoint` | `vector3` | Opens the impound menu. | | `spawns` | `vector4[]` | Spawn positions. | | `blip` | `{ sprite, scale, color }?` | Map blip. | | `ped` | `table?` | Optional NPC. | | `cost` | `integer?` | Fee; falls back to `defaultImpoundFee`. | ## Server exports [#server-exports] ### registerGarage [#registergarage] ```lua exports.esx_garage:registerGarage(garage) ``` Adds or replaces a garage at runtime and refreshes all clients. Throws (via `assert`) if `id`, `entryPoint` or `spawns` are missing or invalid. Use it for property or gang garages created by other resources. ```lua title="server/main.lua" exports.esx_garage:registerGarage({ id = 'apartment_12', label = 'Apartment 12', type = 'public', entryPoint = vec3(-796.1, 304.4, 85.7), spawns = { vec4(-791.6, 300.3, 85.4, 180.0) }, access = { authorize = function(source) return exports.my_housing:isTenant(source, 12) end, }, }) ``` ### registerGarages [#registergarages] ```lua exports.esx_garage:registerGarages(list) ``` Calls `registerGarage` for every entry in `list`. ### unregisterGarage [#unregistergarage] ```lua exports.esx_garage:unregisterGarage(id) ``` Removes a garage by id and refreshes clients. Vehicles parked there remain available at any garage. ### getGarages / getImpounds [#getgarages--getimpounds] ```lua local garages = exports.esx_garage:getGarages() local impounds = exports.esx_garage:getImpounds() ``` Return arrays of all registered garages and impound lots (including runtime registrations). ### impoundVehicle [#impoundvehicle] ```lua local ok = exports.esx_garage:impoundVehicle(plate, lotId) ``` | Name | Type | Description | | --- | --- | --- | | `plate` | `string` | Plate of an owned vehicle. | | `lotId` | `string?` | Impound id. Unknown or `nil` falls back to the first lot. | Deletes the live vehicle if it exists and marks it impounded. Returns `true` when the database row is in the impound afterwards. Typical use: a police or tow job command. ```lua title="server/main.lua" ESX.RegisterCommand('impound', 'admin', function(xPlayer, args) local ok = exports.esx_garage:impoundVehicle(args.plate, 'los_santos') xPlayer.showNotification(ok and 'Vehicle impounded' or 'Impound failed') end, false, { help = 'Impound a vehicle by plate', arguments = { { name = 'plate', help = 'Plate', type = 'string' } }, }) ``` ## Server events (hooks) [#server-events-hooks] Local server events you can listen to with `AddEventHandler`. They cannot be triggered by clients. | Event | Arguments | Fired when | | --- | --- | --- | | `esx_garage:vehicleTransferred` | `source, targetId, plate, fromIdentifier, toIdentifier` | A player transferred ownership of a vehicle. | | `esx_garage:giveKeys` | `source, plate` | A player pressed "give keys" (only with `vehicleKeys = true`). | ```lua title="server/keys.lua" AddEventHandler('esx_garage:giveKeys', function(source, plate) exports.my_keys:giveKey(source, plate) end) ``` ## Callbacks [#callbacks] The UI talks to the server through these callbacks. Each is rate limited (1 to 1.5 s) and checks ownership, distance and garage access on the server, so they are safe against spoofed clients. They return `{ success = boolean, error = string? }`. | Callback | Purpose | | --- | --- | | `esx_garage:getGarages` | Garages the player can access, plus all impounds. | | `esx_garage:getVehicles` | Paginated vehicle list. | | `esx_garage:retrieveVehicle` | Take a vehicle out (charges the impound fee). | | `esx_garage:storeVehicle` | Park the current vehicle. | | `esx_garage:toggleFavorite` | Toggle favorite. | | `esx_garage:renameVehicle` | Set `custom_name`. | | `esx_garage:transferVehicle` | Give a stored vehicle to a player within 10 m. | | `esx_garage:giveKeys` | Fire the `esx_garage:giveKeys` hook. | The client event `esx_garage:refresh` rebuilds blips and points; it is also triggered on `esx:playerLoaded` and `esx:setJob`. ## Integration tips [#integration-tips] - Vehicles bought in [esx_vehicleshop](/docs/addons/esx_vehicleshop) land in `owned_vehicles` and show up automatically. - The garage list does not filter by the vehicle `type` column, so boats and aircraft appear in every garage. Use separate garages with `access` if that matters. - Despawning an owned vehicle with another script sends it to the default impound. Store it through esx_garage or call `impoundVehicle` with the right lot. - The `maxVehicles` field from the type annotations is not enforced by the current code. ## Common issues [#common-issues] - **Old config does not load**: v2 uses `Config.Settings`, `Config.Garages` as a list with `id`/`entryPoint`/`spawns`, and `Config.Impounds` with `getOutPoint`. `Config.DrawDistance`, `Config.Markers` and the old `EntryPoint`/`SpawnPoint` keys are gone. - **`no_money` at the impound**: the player has neither enough cash nor enough bank money for the fee. - **`wrong_garage`**: `restrictToGarage` is on and the vehicle is parked elsewhere. - **`model_mismatch` or `plate_mismatch` when storing**: the vehicle entity does not match the owned record (for example after a plate change by another script). - **Every vehicle impounded after restart**: expected, see the FAQ below. ## Related [#related] - [esx_vehicleshop](/docs/addons/esx_vehicleshop) - [esx_property](/docs/addons/esx_property) - [Database schema](/docs/guides/database-schema) - [oxmysql](/docs/guides/oxmysql) # esx_hud (/docs/addons/esx_hud) esx_hud is the default heads-up display for ESX Legacy. It shows health, armor, hunger, thirst, stamina and oxygen bars, cash and bank balance, the job label, the current weapon with ammo, voice range, and a vehicle speedometer with fuel, damage, indicators and persistent mileage. Use it as-is or as a reference for building your own HUD. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_hud` (version 1.10.2) | | Side | Client UI (NUI) with a small server part for mileage | | Dependencies | `esx_lib`, `es_extended`, `esx_status`, `oxmysql` | | Optional | `esx_cruisecontrol` (seatbelt and cruise control state), `LegacyFuel`, `pma-voice` or `saltychat` | | Database | Adds `owned_vehicles.mileage` automatically | The UI ships prebuilt in `web/dist`. You only need Node.js if you change the React source in `web/src` and rebuild it. ## Installation [#installation] <Steps> <Step> ### Place the resource [#place-the-resource] Put `esx_hud` in your addons folder, for example `resources/[esx_addons]/esx_hud`. </Step> <Step> ### Ensure it after its dependencies [#ensure-it-after-its-dependencies] ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ensure esx_status ensure esx_hud ``` </Step> <Step> ### Let it migrate the database [#let-it-migrate-the-database] On first start the server calls `xLib.schema.ensureMileage()`, which adds `mileage DECIMAL(10,2) NOT NULL DEFAULT 0.00` to `owned_vehicles`. Check the console for `[esx_hud] 1.16.0 mileage schema ready`. If the migration fails, mileage tracking is disabled and the error is printed. </Step> </Steps> ## Configuration [#configuration] All options live in `shared/config.lua`. The locale comes from the `esx:locale` convar (default `en`). ### Config.Disable [#configdisable] Set a key to `true` to turn a feature off. | Key | Default | Description | | --- | --- | --- | | `Status` | `false` | Health, armor, hunger, thirst, stamina and oxygen bars | | `Vehicle` | `false` | Speedometer and the whole mileage system (server side too) | | `Weapon` | `false` | Weapon name, image and ammo | | `Position` | `false` | Street and zone display | | `Voice` | `false` | Voice range indicator | | `Money` | `false` | Cash and bank display | | `Info` | `false` | Info panel (logo, job, online players) | | `IndicatorSound` | `true` | Turn-signal sound | | `IndicatorSeatbeltSound` | `false` | Seatbelt warning sound | | `VehicleHandlers` | `false` | Indicator and engine keybinds | | `MinimapOnFoot` | `false` | `true` hides the minimap while on foot | | `Needle` | `false` | Legacy option; the current speedometer is digital | | `StatusPercent` | `false` | Percentage numbers on status bars | | `CenterStatuses` | `true` | `true` places statuses above the minimap, `false` stacks them on the left | ### Config.Default [#configdefault] | Key | Default | Description | | --- | --- | --- | | `ServerLogo` | `""` | Logo URL; empty uses the bundled ESX logo | | `ServerName` | `""` | Server name in the info panel | | `ServerTagline` | `""` | Tagline under the server name | | `AccentColor` | `""` | Primary theme color; empty uses the ESX theme from esx_lib | | `Kmh` | `true` | `true` for km/h, `false` for mph | | `PassengerSpeedo` | `false` | Show the speedometer to passengers too | ### Config.Colors [#configcolors] Three color groups are passed straight to the UI as CSS values: - `Info`: `money-text-color` (`#0aad20`), `bank-text-color` (`#666666`), `job-text-color` (`#fb9b04`) - `Status`: `healthBar`, `armorBar`, `drinkBar`, `foodBar`, `staminaBar`, `oxygenBar` - `Speedo`: about 25 keys such as `segment-color`, `speedo-progress-color`, `fuel-icon-color` and `speedo-background-color` Players can also change colors and units in-game through `/hudsettings`. Those changes only affect their own client. ## Commands and keybinds [#commands-and-keybinds] | Command or key | Description | | --- | --- | | `/hud`, `/togglehud` | Toggle the HUD for yourself | | `/hudsettings` | Open the settings panel (colors, unit, minimap, cinematic mode) | | `LEFT` / `RIGHT` arrow | Left or right indicator (also on an attached trailer) | | `UP` arrow | Hazard lights | | `N` | Toggle the engine | The keybinds are registered through `xLib.addKeybind`, so players can rebind them in the FiveM key settings. They are not registered when `VehicleHandlers` or `Vehicle` is disabled. Indicators are ignored in aircraft. ## Client exports [#client-exports] ### HudToggle [#hudtoggle] ```lua exports.esx_hud:HudToggle(state) ``` | Name | Type | Description | | --- | --- | --- | | `state` | `boolean` | `true` shows the HUD, `false` hides it | The same function is registered as the net event `esx_hud:HudToggle`, so the server can call it with `TriggerClientEvent('esx_hud:HudToggle', playerId, false)`. ```lua title="client/cutscene.lua" exports.esx_hud:HudToggle(false) -- play your cutscene exports.esx_hud:HudToggle(true) ``` ### SeatbeltState and CruiseControlState [#seatbeltstate-and-cruisecontrolstate] ```lua exports.esx_hud:SeatbeltState(state) exports.esx_hud:CruiseControlState(state) ``` Both take a `boolean` and only update the icons. esx_cruisecontrol calls `SeatbeltState` through its `Config.HudResource` setting. If you use a different seatbelt script, call these exports from it. ### GetCurrentMileage [#getcurrentmileage] ```lua local mileage, kmh, loaded = exports.esx_hud:GetCurrentMileage() ``` Returns the mileage of the vehicle the player is driving, whether it is in km (`kmh`), and whether the value was already loaded from the server. Before it is loaded the first value is `nil`. Only available when `Config.Disable.Vehicle` is `false`. ## Server exports [#server-exports] Mileage is cached on the server per plate (trimmed and uppercased). Only vehicles that exist in `owned_vehicles` are cached as "owned"; unowned vehicles get a random value that is never saved. ### GetMileage [#getmileage] ```lua local miles = exports.esx_hud:GetMileage(plate) ``` Returns the cached mileage in miles (rounded to two decimals) for an owned vehicle, or `nil` if the plate is not cached yet. ### GetMileages [#getmileages] ```lua local map = exports.esx_hud:GetMileages({ 'ABC 123', 'XYZ 999' }) -- map['ABC 123'] = 1520.4 ``` Takes an array of plates and returns a table keyed by plate. Plates without a cached value are left out. esx_garage uses this to show mileage in its menu. ## Events [#events] | Event | Side | Description | | --- | --- | --- | | `esx_hud:HudToggle` | client (net) | Show or hide the HUD | | `esx_hud:UpdateMileage` | client (net) | Server sends the loaded mileage to the driver | | `esx_hud:UnitChanged` | client (local) | Fired when the player switches km/h and mph in settings | | `esx_hud:EnteredVehicle` | server (net) | `(plate, kmh)` driver entered; loads mileage | | `esx_hud:ExitedVehicle` | server (net) | `(plate, mileage, kmh)` driver left; updates cache | | `esx_hud:UpdateVehicleMileage` | server (net) | `(plate, mileage, kmh)` used by esx_garage on store | esx_hud listens to `esx_status:onTick` for hunger and thirst, `esx:setAccountMoney` for money, `esx:setJob` for the job, `esx:pauseMenuActive` to hide itself in the pause menu, and `esx:enteredVehicle` / `esx:exitedVehicle` from es_extended. <Callout type="warn" title="Client-trusted mileage"> The three server net events take the plate and mileage from the client without checking that the player actually drives that vehicle. A modified client can rewrite the stored mileage of any owned vehicle. The value is clamped to 0 – 99,999,999.99, so it cannot break the database, but do not use mileage for anything valuable (insurance payouts, resale price) without your own server-side checks. </Callout> ## Saving mileage [#saving-mileage] The server writes cached mileage of owned vehicles to `owned_vehicles.mileage`: - every 5 minutes - when `esx_hud` stops - 10 seconds before a txAdmin scheduled restart, and on txAdmin shutdown The value is always stored in miles. With `Kmh = true` the client shows it multiplied by 1.61. ## Integration tips [#integration-tips] - **Voice:** pma-voice and saltychat are detected automatically. For any other voice resource, edit `client/voice.lua` and send `xLib.nui.send({ type = "VOICE_RANGE", value = index })`. - **Fuel:** LegacyFuel is read through its export. Other fuel scripts work if they set the native fuel level. - **Status bars:** hunger and thirst come from the `hunger` and `thirst` statuses of [esx_status](/docs/addons/esx_status). Custom statuses are not displayed. - **Other HUDs:** if you replace esx_hud, keep esx_cruisecontrol happy by pointing its `Config.HudResource` at your resource or removing the export call. ## Common issues [#common-issues] - **HUD is blank or missing:** `web/dist` is missing (you cloned the source without the build). Download the release, or run `npm install` and `npm run build` in `web`. - **"Setup your custom voice resource" in the console:** neither pma-voice nor saltychat is started. Set `Config.Disable.Voice = true` or add your own voice handler. - **Mileage stays at 0 after restart:** the schema migration failed (check the console) or the vehicle is not in `owned_vehicles`. - **Indicators do nothing:** another resource uses the arrow keys, or `VehicleHandlers` is disabled. Players can rebind under Settings, Key Bindings, FiveM. ## Related [#related] - [esx_status](/docs/addons/esx_status) - [esx_cruisecontrol](/docs/addons/esx_cruisecontrol) - [esx_garage](/docs/addons/esx_garage) - [esx_lib](/docs/core/esx_lib) - [Client events](/docs/es_extended/events/client) # esx_joblisting (/docs/addons/esx_joblisting) esx_joblisting is the job center: a map marker that opens a NUI where players browse non-whitelisted jobs, read a description, set a waypoint, apply, or quit to unemployed. It also exposes a small task and progress API that other job scripts can use to show goals in the UI. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_joblisting` (version `1.0`) | | Side | client + server, NUI (`web/index.html`) | | Dependencies | `esx_lib`, `es_extended` | | Also used | `oxmysql` | | Database | `jobs.whitelisted` column | Which jobs appear is controlled by the database, not by the config: every job with `whitelisted = false` that has a grade `0` is listed. `Config.JobDetails` only adds text, images and tasks to those cards. ## Installation [#installation] <Steps> <Step> ### Make sure the `whitelisted` column exists [#make-sure-the-whitelisted-column-exists] `esx_joblisting.sql` runs: ```sql ALTER TABLE jobs add whitelisted BOOLEAN NOT NULL DEFAULT FALSE; ``` Current ESX Legacy databases (including the txAdmin recipe) already have this column. Running the file again fails with "Duplicate column name", which you can ignore. Only run it on very old databases. </Step> <Step> ### Start it [#start-it] ```bash title="server.cfg" ensure es_extended ensure esx_lib ensure esx_joblisting ``` </Step> <Step> ### Hide jobs that need an application [#hide-jobs-that-need-an-application] ```sql UPDATE jobs SET whitelisted = 1 WHERE name IN ('police', 'ambulance', 'mechanic'); ``` Restart the server so ESX reloads the jobs. </Step> </Steps> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.DrawDistance` | `15.0` | Marker draw distance. Also the server-side radius for opening the menu and changing jobs. | | `Config.ZoneSize` | `{ x = 2.7, y = 2.7, z = 0.5 }` | Marker size. The interaction radius is half of `x`. | | `Config.MarkerColor` | `{ r = 100, g = 200, b = 104 }` | Marker color. | | `Config.MarkerType` | `27` | Marker type. | | `Config.Debug` | `ESX.GetConfig().EnableDebug` | Read from es_extended. Not used by the current code. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language. | | `Config.JobDetails` | `miner`, `delivery`, `garbage` | Extra card data per job name, see below. | | `Config.UnemployedJob` | `'unemployed'` | Job set when a player clicks **Quit**. | | `Config.Zones` | `{ vector3(-265.08, -964.1, 30.3) }` | Job center locations. | | `Config.Blip` | `{ Enabled = true, Sprite = 407, Display = 4, Scale = 0.8, Colour = 27, ShortRange = true }` | Map blip for each zone. | ### `Config.JobDetails` [#configjobdetails] Keyed by job name. Every field is optional. | Field | Type | Description | | --- | --- | --- | | `subtitle` | `string` | Short line under the job name. | | `description` | `string` | Long text. `\n\n` creates paragraphs. | | `image` | `string` | Path inside `web/`, for example `'assets/miner.png'`. | | `rating` | `number` | Difficulty, 0 to 5 stars. | | `requirement` | `string` | Shown as a badge. Defaults to "No requirements". | | `location` | `{ x, y }` | Enables the **mark location** button, which sets a GPS waypoint. | | `tasks` | `table` | Default task list: `{ title, description, current, target }`. Replaced by stored progress when the player has some. | ```lua title="config.lua" Config.JobDetails.taxi = { subtitle = 'Drive the city', rating = 2, requirement = 'Driver license', location = { x = 903.3, y = -170.5 }, description = 'Pick up passengers at the depot and drive them anywhere in Los Santos.', } ``` <Callout type="info"> The default config describes `delivery` and `garbage`, which are not part of the official addons. Their cards only appear if your database has jobs with those names. </Callout> ## Exports [#exports] ### UpdateJobProgress (server) [#updatejobprogress-server] Stores stats and tasks for a job in the player's metadata (`joblisting` key) and pushes the updated profile to the client. ```lua exports.esx_joblisting:UpdateJobProgress(playerId, jobName, data) ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | `number` | Server ID. | | `jobName` | `string` | Job the progress belongs to. | | `data.stats` | `table?` | Up to 64 key/value pairs (numbers or strings up to 64 characters). `stats.extra` may hold up to 3 `{ label, value }` entries. | | `data.tasks` | `table?` | Up to 64 `{ title, description?, current, target }` entries. `current` and `target` are integers from 0 to 100000. | Returns `true` on success, `false` when the player is not loaded or the arguments have the wrong type. Invalid entries are dropped silently. ```lua title="server/main.lua" local function rewardRock(playerId, mined) exports.esx_joblisting:UpdateJobProgress(playerId, 'miner', { stats = { rocks = mined }, tasks = { { title = 'Extract mineral rock', description = 'Break rocks from active quarry veins.', current = mined, target = 40 }, }, }) end ``` ### ShowTasks (client) [#showtasks-client] ```lua exports.esx_joblisting:ShowTasks(tasks, grade) ``` | Name | Type | Description | | --- | --- | --- | | `tasks` | `table` | List of `{ title, description?, current, target }`. | | `grade` | `string?` | Subtitle shown in the HUD, usually the grade label. | ### HideTasks (client) [#hidetasks-client] ```lua exports.esx_joblisting:HideTasks() ``` ### ShowTaskProgress (client) [#showtaskprogress-client] Shows a toast with a progress bar for 5 seconds. ```lua exports.esx_joblisting:ShowTaskProgress({ title = 'Deliver processed sacks', current = 3, target = 10 }) ``` ## Server callbacks [#server-callbacks] | Callback | Arguments | Returns | | --- | --- | --- | | `esx_joblisting:open` | none | `{ ok, jobs, profile }`. `ok` is `false` outside a job center. Rate limited. | | `esx_joblisting:apply` | `jobName` | `{ ok, message?, profile? }`. | | `esx_joblisting:quit` | none | Sets `Config.UnemployedJob`. | | `esx_joblisting:getJobsList` | none | List of available jobs. Rate limited. | ## Events [#events] | Event | Side | Description | | --- | --- | --- | | `esx_joblisting:setJob` | server (net) | `(jobName)` legacy entry point. Runs the same checks as `apply`. | | `esx_joblisting:profileUpdated` | client | `(profile)` sent by `UpdateJobProgress`, refreshes the open UI. | <Callout type="warn" title="Security"> `esx_joblisting:apply` and `esx_joblisting:setJob` change a player's job, so they are classic targets. The server checks distance to a job center, rejects whitelisted or missing jobs, only sets grade `0`, and applies a 1.5 second cooldown. Keep jobs that need vetting marked `whitelisted = 1`, or a cheater can still join them by standing at the job center. </Callout> ## Common issues [#common-issues] - **Police or EMS show up in the list.** Their `whitelisted` value is `0`. See the installation steps. - **A job is missing.** It needs `whitelisted = 0` and a grade `0` row in `job_grades`. - **Menu opens and closes immediately with an error.** The server did not confirm you are near a zone. Make sure `Config.Zones` matches the marker you are standing on, and do not move `Config.DrawDistance` below the marker size. - **Card has no picture.** Add `image` in `Config.JobDetails` and put the file in `web/assets/`. It is covered by the `web/assets/*` files entry. ## Related [#related] - [esx_jobs](/docs/addons/esx_jobs) - [esx_whitelist](/docs/core/esx_whitelist) - [Create a job](/docs/guides/create-a-job) - [Database schema](/docs/guides/database-schema) # esx_jobs (/docs/addons/esx_jobs) esx_jobs provides simple gather-process-sell jobs: put on work clothes, take a company truck for a deposit, stand in work zones to receive raw items, process them in the next zones, and sell the final product for cash. Use it for starter jobs, or as a data-driven framework for your own farming-style jobs. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_jobs` (version `1.0`) | | Side | shared config, client + server | | Dependencies | `esx_lib`, `es_extended`, OneSync (`/onesync`) | | Also used | `oxmysql`, [esx_addonaccount](/docs/addons/esx_addonaccount) (for the `caution` deposit account) | | Jobs | `fisherman`, `fueler`, `lumberjack`, `miner`, `slaughterer`, `tailor` | <Callout type="info"> The official docs note that esx_jobs is being rewritten. The description below matches the current source. </Callout> ### Job chains [#job-chains] | Job | Raw → processed → sold | Sale price per unit | | --- | --- | --- | | `miner` | `stone` → `washed_stone` → `copper`, `iron`, `gold`, `diamond` (5% drop chance) | copper 5, iron 9, gold 25, diamond 250 | | `lumberjack` | `wood` → `cutted_wood` → `packaged_plank` | 13 | | `slaughterer` | `alive_chicken` → `slaughtered_chicken` → `packaged_chicken` | 23 | | `tailor` | `wool` → `fabric` → `clothe` | 40 | | `fueler` | `petrol` → `petrol_raffin` → `essence` | 61 | | `fisherman` | `fish` (from a boat) | 11 | Money from deliveries is paid as cash (`xPlayer.addMoney`). ## Installation [#installation] <Steps> <Step> ### Import the SQL [#import-the-sql] `esx_jobs.sql` adds the private `caution` addon account, the seven jobs (including `reporter`) with one `employee` grade each, and the 19 items. The job and item labels in the file are French. Rename them in the database if your server is in another language. If your database came from the ESX txAdmin recipe, these rows already exist and the import fails with duplicate key errors. </Step> <Step> ### Start it [#start-it] ```bash title="server.cfg" ensure esx_addonaccount ensure esx_jobs ``` OneSync must be enabled, because work vehicles are spawned on the server. </Step> </Steps> ## Configuration [#configuration] `config.lua` holds the global options. Each job lives in its own file under `jobs/` and adds itself to `Config.Jobs`. | Key | Default | Description | | --- | --- | --- | | `Config.DrawDistance` | `10.0` | Distance at which markers are drawn. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language. | | `Config.Jobs` | `{}` | Filled by the files in `jobs/`. | | `Config.MaxCaution` | `10000` | Highest deposit a vehicle spawner may charge. Spawners above this are refused. | | `Config.RequestIPL` | `true` | Loads a few map IPLs (`CS1_02_cf_onmission1` to `4`, `id2_14_during_door`, `id2_14_during1`) used by job locations. Set to `false` if another resource already manages them. | | `Config.PublicZones` | `EnterBuilding`, `ExitBuilding` | Teleport markers available to everyone. Each has `Pos`, `Size`, `Color`, `Marker`, `Blip`, `Name`, `Type = "teleport"`, `Hint` and `Teleport`. | ### Job file format [#job-file-format] ```lua title="jobs/miner.lua" Config.Jobs.miner = { BlipInfos = { Sprite = 68, Color = 5 }, Vehicles = { Truck = { Spawner = 1, Hash = 'rubble', Trailer = 'none', HasCaution = true }, }, Zones = { Mine = { Pos = { x = 2962.40, y = 2746.20, z = 42.39 }, Size = { x = 5.0, y = 5.0, z = 1.0 }, Color = { r = 50, g = 200, b = 50 }, Marker = 1, Blip = true, Name = TranslateCap('m_rock'), Type = 'work', Item = { { name = TranslateCap('m_rock'), db_name = 'stone', time = 3, max = 7, add = 1, remove = 0, requires = 'nothing', requires_name = 'Nothing', drop = 100, }, }, Hint = TranslateCap('m_pickrocks'), GPS = { x = 289.24, y = 2862.90, z = 42.64 }, }, -- cloakroom, vehspawner, vehspawnpt, vehdelete and delivery zones follow }, } ``` #### Zone types [#zone-types] | `Type` | Purpose | Extra fields | | --- | --- | --- | | `cloakroom` | Toggle work and civilian clothes. Also toggles the client "on duty" state that unlocks the other zones. | | | `work` | Gives items over time while the player stands in it on foot. | `Item` | | `vehspawner` | Spawns the job vehicle for a deposit. | `Spawner`, `Caution` | | `vehspawnpt` | Where the vehicle appears. | `Spawner`, `Heading` | | `vehdelete` | Returns the vehicle and refunds the deposit. | `Spawner`, `Caution`, `Teleport` | | `delivery` | Sells items for cash. | `Item`, `Spawner` | Zones, spawners and vehicles are linked through the same `Spawner` number. The fisherman uses `Spawner = 2` for the boat and its delivery point. #### `Item` entries [#item-entries] | Field | Description | | --- | --- | | `name` | Display name. For delivery zones this must be `TranslateCap('delivery')`. | | `db_name` | Item to give (not used for delivery). | | `time` | Seconds between ticks. Read from the first entry of the list. | | `max` | Stop when the player holds this many. | | `add` | Amount given per tick. | | `remove` | Amount of `requires` removed per tick. | | `requires` | Required input item, or `'nothing'`. | | `requires_name` | Label used in the "not enough" message. | | `drop` | Chance in percent that `add` is given on a tick. | | `price` | Cash paid per tick (delivery zones only). | The first entry drives the timing and the required input. Extra entries in the same zone, like iron, gold and diamond in the miner foundry, are extra outputs from the same input. #### Vehicles [#vehicles] | Field | Description | | --- | --- | | `Spawner` | Links the vehicle to its `vehspawner` zone. | | `Hash` | Model name. | | `Trailer` | Trailer model to attach, or `'none'`. | | `HasCaution` | Marks the vehicle as a deposit vehicle. | ## Vehicle deposit (caution) [#vehicle-deposit-caution] When a player uses a `vehspawner` zone, the server takes `Caution` from their **bank** account and stores it in their personal `caution` addon account, then spawns the vehicle. When they return it in the `vehdelete` zone, the refund is the deposit multiplied by the vehicle condition: `(engine health + body health) / 2000`. A destroyed vehicle refunds nothing. Outstanding deposits are settled automatically when the player changes job, disconnects or the resource stops. ## Server events [#server-events] | Event | Arguments | Description | | --- | --- | --- | | `esx_jobs:startWork` | `zoneIndex, zoneKey` | Starts the work loop for the zone `zoneKey` of the player's current job. The first argument is ignored. | | `esx_jobs:stopWork` | none | Stops the work loop. | | `esx_jobs:caution` | `cautionType, zoneKey` | `'take'` spawns a vehicle at a nearby `vehspawner`, `'give_back'` returns the vehicle the player drives at a nearby `vehdelete`. | The server looks up zones from its own config by job and key, never from client data, and stops the work loop as soon as the player is farther from the zone center than the zone's `Size.x`. Deposit amounts are read from the config and capped by `Config.MaxCaution`. ## Client events [#client-events] | Event | Arguments | Description | | --- | --- | --- | | `esx_jobs:spawnJobVehicle` | `netId, spawnPoint, vehicle` | Sent after the server spawned the vehicle. Attaches the trailer and seats the player. | | `esx_jobs:vehicleReturned` | `netId, teleport` | Sent after a successful return. Teleports the player if `teleport` is set. | | `esx_jobs:action` | `job, zone, zoneKey` | Local event fired when the player presses `E` in a zone. | ## Common issues [#common-issues] - **Work zone does nothing.** Put on the work clothes at the cloakroom first, and step out of your vehicle. Work zones only run on foot. - **"Not enough" right away.** The zone requires an input item. Process the previous step first. - **Reporter job has no markers.** `reporter` exists in the SQL and has a public teleport building, but no file under `jobs/`. - **Deposit not taken or vehicle not spawned.** The player needs enough money in the bank, the spawn point must be free, and `esx_addonaccount` must be running so the `caution` account exists. - **Items show French names.** Update the labels in the `items` table. ## Related [#related] - [esx_joblisting](/docs/addons/esx_joblisting) - [esx_addonaccount](/docs/addons/esx_addonaccount) - [Create a job](/docs/guides/create-a-job) - [Add items](/docs/guides/add-items) # esx_license (/docs/addons/esx_license) `esx_license` keeps track of which licenses each player owns, for example a driving license or a weapon permit. Other resources such as [esx_dmvschool](/docs/addons/esx_dmvschool), [esx_boat](/docs/addons/esx_boat), [esx_vehicleshop](/docs/addons/esx_vehicleshop) and [esx_weaponshop](/docs/addons/esx_weaponshop) grant and check licenses through it. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_license` | | Side | Server only | | Dependencies | `esx_lib`, `es_extended`, `oxmysql` | | SQL | `esx_license.sql` (`licenses`, `user_licenses`) | ## Installation [#installation] <Steps> <Step> ### Import the SQL [#import-the-sql] ```sql title="esx_license.sql" CREATE TABLE `licenses` ( `type` varchar(60) NOT NULL, `label` varchar(60) NOT NULL, PRIMARY KEY (`type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE `user_licenses` ( `id` int NOT NULL AUTO_INCREMENT, `type` varchar(60) NOT NULL, `owner` VARCHAR(60) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uq_user_licenses_owner_type` (`owner`, `type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ``` `licenses` is the catalog of license types. The addons that use a license insert their own rows, for example: ```sql INSERT INTO `licenses` (`type`, `label`) VALUES ('weapon', 'Weapon License'); ``` </Step> <Step> ### Start it [#start-it] ```bash title="server.cfg" ensure esx_license ``` </Step> </Steps> On start the resource checks `user_licenses` for duplicate `owner` + `type` rows. If it finds any it stops granting licenses and prints `Schema migration failed; grants disabled`. Remove the duplicates and restart. If the unique key is missing it adds it. The same migration is shipped as `sql/security_hardening_2026_10_02.sql` in the addons repo; see the [addons overview](/docs/addons). ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.allowedJobs` | `{ ['police'] = true }` | Jobs that may read and remove other players' licenses. | | `Config.LicenseCheckDistance` | `5.0` (not in `config.lua`) | Max distance in meters between an allowed-job player and the target. Add the key to `config.lua` to change it. | ### Who may read another player's licenses [#who-may-read-another-players-licenses] A client request for another player passes when one of these is true: - The caller asks about themselves. - The caller's group is `admin` or `superadmin`. - The caller's job is in `Config.allowedJobs`, the job's `onDuty` is not `false`, and the target is within `Config.LicenseCheckDistance`. ## Server events [#server-events] Local events for other server scripts. Clients cannot trigger them, except `removeLicense`. ### esx_license:addLicense [#esx_licenseaddlicense] ```lua TriggerEvent('esx_license:addLicense', playerId, licenseType, cb) ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | number | Server ID of an online player. | | `licenseType` | string | A `type` from the `licenses` table. | | `cb` | function or nil | Called with `false` if the player already owns it, otherwise with the insert result. | ```lua title="server/main.lua" TriggerEvent('esx_license:addLicense', source, 'weapon', function(result) if result then TriggerClientEvent('esx:showNotification', source, 'Weapon license issued') end end) ``` <Callout type="warn"> If `licenseType` is not in the `licenses` table, the license is not added, a warning is printed and `cb` is never called. </Callout> ### esx_license:checkLicense [#esx_licensechecklicense] ```lua TriggerEvent('esx_license:checkLicense', playerId, licenseType, cb) ``` `cb(hasLicense)` receives a boolean. Nothing happens if the player is offline. ```lua title="server/main.lua" TriggerEvent('esx_license:checkLicense', source, 'drive', function(hasLicense) if not hasLicense then return TriggerClientEvent('esx:showNotification', source, 'You need a driving license') end -- continue end) ``` ### esx_license:getLicenses [#esx_licensegetlicenses] ```lua TriggerEvent('esx_license:getLicenses', playerId, cb) ``` `cb(licenses)` receives an array of `{ type, label }` for an online player. ### esx_license:getLicense [#esx_licensegetlicense] ```lua TriggerEvent('esx_license:getLicense', licenseType, cb) ``` `cb({ type = licenseType, label = label })`. `label` is `nil` when the type does not exist. This reads the catalog; it does not check a player. ### esx_license:getLicensesList [#esx_licensegetlicenseslist] ```lua TriggerEvent('esx_license:getLicensesList', function(list) for _, license in ipairs(list) do print(license.type, license.label) end end) ``` Returns the cached catalog, loaded on start and refreshed on `esx:sqlCatalogReady`. ### esx_license:removeLicense (net event) [#esx_licenseremovelicense-net-event] ```lua TriggerServerEvent('esx_license:removeLicense', targetId, licenseType) ``` | Name | Type | Description | | --- | --- | --- | | `targetId` | number | Server ID of the player who loses the license. | | `licenseType` | string | License type. | <Callout type="info" title="Security"> This is the only event clients can send. The server checks that the caller's job is in `Config.allowedJobs`, the license type exists, and the caller may read the target (see the rules above). Otherwise the caller gets "Your job is not allowed to remove the license". </Callout> To remove a license from server code, delete the row yourself: ```lua title="server/main.lua" local xTarget = ESX.Player(targetId) MySQL.update('DELETE FROM user_licenses WHERE type = ? AND owner = ?', { 'weapon', xTarget.getIdentifier(), }) ``` ## Client callbacks [#client-callbacks] Call with `ESX.TriggerServerCallback`, `ESX.AwaitServerCallback` or `xLib.callback`. | Callback | Arguments | Returns | | --- | --- | --- | | `esx_license:checkLicense` | `targetId`, `licenseType` | `boolean` | | `esx_license:getLicenses` | `targetId?` | array of `{ type, label }` (empty when denied) | | `esx_license:getLicense` | `licenseType` | `{ type, label }` | | `esx_license:getLicensesList` | none | array of `{ type, label }` | `targetId` defaults to the caller when omitted. Reading someone else uses the permission rules above. ```lua title="client/main.lua" ESX.TriggerServerCallback('esx_license:checkLicense', function(hasLicense) if hasLicense then OpenGunShop() else ESX.ShowNotification('You need a weapon license') end end, GetPlayerServerId(PlayerId()), 'weapon') ``` ## Common issues [#common-issues] - **addLicense does nothing**: the type is missing in `licenses`, or the player is offline. Check the server console for `Missing license type in db`. - **Grants disabled after update**: duplicate rows in `user_licenses`. Remove them and restart. - **Police cannot see licenses**: the officer is too far away (5 m), off duty, or the job is not in `Config.allowedJobs`. ## Related [#related] - [esx_dmvschool](/docs/addons/esx_dmvschool) - [esx_weaponshop](/docs/addons/esx_weaponshop) - [esx_policejob](/docs/addons/esx_policejob) - [Database schema](/docs/guides/database-schema) # esx_lscustom (/docs/addons/esx_lscustom) esx_lscustom brings the Los Santos Customs workshop to ESX Legacy. Players drive into one of the workshops, press the interaction key and customize performance, body parts, wheels, colors, lights and more in a NUI with a cart, a camera mode and live vehicle stats. Everything in the cart is priced and checked on the server before money is taken. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_lscustom` (version `1.0`) | | Side | Client + server, NUI in `html/` (no build step) | | Dependencies | `esx_lib`, `es_extended`, `oxmysql` | | Reads | `vehicles` table from [esx_vehicleshop](/docs/addons/esx_vehicleshop) for base prices, `owned_vehicles` for saving | | Optional | `esx_addonaccount` with `society_mechanic` (mechanic-only mode) | | SQL file | None | How it works: 1. The player sits in a vehicle within `Config.DrawDistance` of a zone and presses `E`. 2. The client sends `esx_lscustom:startModing` with the current vehicle properties. The server stores this snapshot as the session's reference. 3. The player previews parts. Nothing is paid until checkout. 4. On checkout, the `esx_lscustom:buyCart` callback recomputes every cart line from `Config.Menus` and the vehicle price, checks that the vehicle props only differ in paid fields, charges the money and saves the result. 5. On cancel or disconnect the original props are restored. ## Installation [#installation] <Steps> <Step> ### Install the vehicle catalog [#install-the-vehicle-catalog] Prices depend on the `vehicles` table. Install [esx_vehicleshop](/docs/addons/esx_vehicleshop) or create that table with `model` and `price` columns yourself. </Step> <Step> ### Ensure the resource [#ensure-the-resource] ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ensure esx_vehicleshop ensure esx_lscustom ``` </Step> </Steps> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.DrawDistance` | `10.0` | Distance at which the zone hint appears. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale. | | `Config.IsMechanicJobOnly` | `false` | Only the `mechanic` job can use the workshop. Payment comes from `society_mechanic`. | | `Config.Currency` | `'$'` | Currency symbol in messages and UI. | | `Config.Workshop.UseNui` | `true` | NUI workshop. `false` uses the ESX default menu flow. | | `Config.Workshop.EnableCamera` | `true` | Camera controls in the NUI. | | `Config.Workshop.EnableStats` | `true` | Live handling stats. | | `Config.Workshop.PurchaseCooldown` | `1500` | ms between checkouts per player. | | `Config.Workshop.Ownership.RequireOwned` | `false` | Only accept vehicles owned by the buyer (or mechanic customer vehicles). | | `Config.Workshop.Ownership.SaveOwnedVehicles` | `true` | Save paid changes to `owned_vehicles`. | | `Config.Workshop.Ownership.AllowMechanicCustomerVehicles` | `false` | Mechanics may tune and save vehicles they do not own. | | `Config.Workshop.StatGains` | per mod | Display-only stat gains, e.g. `modEngine = { accel = 12, speed = 8 }`. | | `Config.Zones` | `ls1` ... `ls5` | Workshop locations with `Pos`, `Name` (blip) and `Hint`. | | `Config.Colors` | list | Paint color groups shown in the respray menu. | | `Config.DefaultWheelsPriceMultiplier` | `2` | Wheel price in percent when a wheel menu has no `price`. | | `Config.Menus` | large table | Menu tree and prices. | ### Prices in `Config.Menus` [#prices-in-configmenus] Every purchasable entry has a `modType` and a `price`. The price is a **percentage of the vehicle price**: ```lua title="config.lua" modEngine = { label = TranslateCap('engine'), parent = 'upgrades', modType = 11, price = { 13.95, 32.56, 65.12, 139.53 }, -- one value per level }, ``` - Performance mods (engine, brakes, transmission, suspension, armor) take one percentage per level. Removing a mod (level `-1`) is free. - Turbo and xenon toggles use a single percentage. - Cosmetic parts, paint and wheels use a single percentage. To make tuning cheaper or more expensive overall, scale these numbers. The workshop zones themselves look like this: ```lua title="config.lua" Config.Zones = { ls1 = { Pos = vector3(-337.38, -136.92, 38.57), Name = TranslateCap('blip_name'), Hint = TranslateCap('press_custom'), }, } ``` ## Developer API [#developer-api] The resource has no exports. | Name | Type | Description | | --- | --- | --- | | `esx_lscustom:startModing` | Server net event | `(props, netId)`. Starts a session for the vehicle the player is in. Ignored unless the player is within 12 m of a zone, in that vehicle, and matching model and plate. | | `esx_lscustom:stopModing` | Server net event | `(plate)`. Ends the player's session for that plate. | | `esx_lscustom:buyCart` | Server callback | `(payload, netId)` with `payload.cart` and `payload.vehicleProps`. Returns `success, message`. Max 40 cart lines. | | `esx_lscustom:getVehiclesPrices` | Server callback | Returns the cached `{ model, price }` list. | | `esx_lscustom:setVehiclesPrices` | Client event | Server pushes the price list (refreshed every 10 minutes when changed). | | `esx_lscustom:restoreMods` | Client event | `(netId, props)`. Restores a vehicle when its customer disconnects mid-session. | | `esx_lscustom:cancelInstallMod` | Client event | Restores the original props of the current vehicle. | <Callout type="info" title="Client-triggerable"> `startModing` and `buyCart` are callable by clients. The server ignores the client's prices: it recalculates each line from `Config.Menus`, rejects props that changed fields nobody paid for, enforces the distance, the session and a cooldown, and locks per player and plate during checkout. If you add custom mods, add them to `Config.Menus` so they are validated too. </Callout> ## Common issues [#common-issues] - **Everything costs the same for every car.** The model is missing in `vehicles`, so the 50000 fallback is used. Add the model to the table. - **Changes are lost after restart.** The vehicle is not in `owned_vehicles`, the buyer is not the owner, or `SaveOwnedVehicles` is `false`. Your garage script must also apply the stored props. - **"Not enough money" in mechanic-only mode.** Payment comes from `society_mechanic`, not the mechanic's wallet. - **Hint does not appear.** You must be in a vehicle, and with `IsMechanicJobOnly` you need the `mechanic` job. ## Related [#related] - [esx_mechanicjob](/docs/addons/esx_mechanicjob) - [esx_vehicleshop](/docs/addons/esx_vehicleshop) - [esx_garage](/docs/addons/esx_garage) - [esx_addonaccount](/docs/addons/esx_addonaccount) # esx_mechanicjob (/docs/addons/esx_mechanicjob) esx_mechanicjob adds a `mechanic` job with a workshop, harvesting and crafting of repair items, a mobile F6 menu (bill, lockpick, repair, clean, impound, tow, place props) and NPC towing missions that pay into the society account. Use it for a basic mechanic or tow company on an ESX Legacy server. <Callout type="warn"> The official ESX documentation warns that this resource has known issues and that a rewrite is planned. The version described here already has server-side checks and rate limits on its events, but test it on a staging server first. </Callout> ## Overview [#overview] | | | | --- | --- | | Resource | `esx_mechanicjob` (version `1.0`) | | Side | client + server | | Job name | `mechanic` (grades `recrue`, `novice`, `experimente`, `chief`, `boss`) | | Society | `society_mechanic`, registered as a private society | | Dependencies | `esx_lib`, `es_extended`, [esx_society](/docs/addons/esx_society), [esx_billing](/docs/addons/esx_billing) | | Also used | `oxmysql`, [esx_service](/docs/addons/esx_service) (when `MaxInService` is set), `esx_garage` (impound) | | Items | `gazbottle`, `fixtool`, `carotool`, `blowpipe`, `fixkit`, `carokit` | ### Gameplay loop [#gameplay-loop] 1. **Harvest** tools at the `Garage` zone: gas bottles, repair tools and bodywork tools, one every 4 seconds, up to 5 of each. 2. **Craft** at the `Craft` zone: a gas bottle becomes a blowtorch (`blowpipe`), repair tools become a `fixkit`, bodywork tools become a `carokit`. 3. **Use** the kits on vehicles or use the F6 menu actions. 4. **NPC missions**: tow random parked cars from `Config.Towables` back to `VehicleDelivery` for a society payout. The `MechanicActions` marker opens the vehicle list, work and civilian clothes, stock deposit and withdraw, and, for the `boss` grade, the esx_society boss menu. ## Installation [#installation] <Steps> <Step> ### Import the SQL [#import-the-sql] Run `esx_mechanicjob.sql`. It creates the society rows, the `mechanic` job with five grades and default uniforms, and the six items. If you installed an older version whose grades have empty skins, also run `update_mechanic_uniforms.sql`. It only fills `skin_male` and `skin_female` where they are empty or `{}`. </Step> <Step> ### Start it [#start-it] ```bash title="server.cfg" ensure esx_society ensure esx_billing ensure esx_mechanicjob ``` </Step> </Steps> <Callout type="warn" title="txAdmin recipe installs"> The ESX recipe already creates `society_mechanic` and the mechanic items. Importing `esx_mechanicjob.sql` on top of it fails with duplicate key errors. Check which rows already exist and import only the missing ones. </Callout> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language. | | `Config.Controls.mechanicMenu` | `"F6"` | Default key for the `mechanicMenu` command. | | `Config.Controls.toggleNPCJob` | `"F7"` | Default key for the `mechanicjob` command. | | `Config.DrawDistance` | `10.0` | Distance at which markers are drawn. | | `Config.MaxInService` | `-1` | Service limit through esx_service. `-1` disables the service check. | | `Config.EnablePlayerManagement` | `true` | Enables the boss menu, the `slamvan3` vehicle for higher grades, and blocks `recrue` from harvesting and crafting. | | `Config.EnableSocietyOwnedVehicles` | `false` | Uses vehicles stored in the esx_society garage instead of the fixed list. | | `Config.NPCSpawnDistance` | `500.0` | The NPC target vehicle spawns once the player is closer than this to the target zone. | | `Config.NPCNextToDistance` | `25.0` | How close you must get to the NPC target vehicle. | | `Config.NPCJobEarnings` | `{ min = 15, max = 40 }` | Random reward per completed NPC mission. Doubled for grade 3 and higher. | | `Config.ActionVehicleDistance` | `2.0` | How close a vehicle must be for mobile actions. | | `Config.OxInventory` | `ESX.GetConfig().OxInventory` | Read from es_extended. | | `Config.Vehicles` | `{ 'adder', 'asea', 'asterope', 'banshee', 'buffalo', 'sultan', 'baller3' }` | Models used for NPC tow targets. The server only accepts these models. | | `Config.Zones` | see below | Marker positions. | | `Config.Towables` | 40 `vector3` positions | NPC mission pickup points. Each one is turned into a `Towable<n>` zone at load. | `Config.Zones` contains `MechanicActions`, `Garage` (harvest), `Craft`, `VehicleSpawnPoint`, `VehicleDeleter` and `VehicleDelivery`. Each zone has `Pos`, `Size`, `Color` (optional) and `Type` (marker type, `-1` hides it). <Callout type="info"> The default `Garage` harvest zone is in Paleto Bay (`-97.5, 6496.1`), far from the workshop in the city. Move it if you want a compact job loop. </Callout> ## Commands and keybinds [#commands-and-keybinds] | Command | Default key | Description | | --- | --- | --- | | `mechanicMenu` | `F6` | Opens the mobile actions menu (mechanic job only, not while dead). | | `mechanicjob` | `F7` | Starts the NPC towing mission when you sit in a `flatbed`, or cancels it. Cancelling is limited to once every 5 minutes. | Both are registered with `RegisterKeyMapping`, so players can rebind them in the FiveM key settings. ## Usable items [#usable-items] Registered on the server with `ESX.RegisterUsableItem`. Each use removes one item and triggers a client event on the user. | Item | Client event | Effect | | --- | --- | --- | | `blowpipe` | `esx_mechanicjob:onHijack` | Tries to unlock the closest vehicle (about a 66% success chance; about one in three attempts also sets off the alarm). | | `fixkit` | `esx_mechanicjob:onFixkit` | Repairs the closest vehicle. | | `carokit` | `esx_mechanicjob:onCarokit` | Repairs the closest vehicle's bodywork. | ## Server events [#server-events] All handlers check that the caller has the `mechanic` job and apply a per-player rate limit. | Event | Arguments | Notes | | --- | --- | --- | | `esx_mechanicjob:startHarvest` / `stopHarvest` | none | Gas bottles at `Garage`. | | `esx_mechanicjob:startHarvest2` / `stopHarvest2` | none | Repair tools at `Garage`. | | `esx_mechanicjob:startHarvest3` / `stopHarvest3` | none | Bodywork tools at `Garage`. | | `esx_mechanicjob:startCraft` / `stopCraft` | none | Blowtorch at `Craft`. | | `esx_mechanicjob:startCraft2` / `stopCraft2` | none | Repair kit at `Craft`. | | `esx_mechanicjob:startCraft3` / `stopCraft3` | none | Body kit at `Craft`. | | `esx_mechanicjob:startNPCJob` | none | Player must sit in a `flatbed`. Picks a random `Towable` zone. | | `esx_mechanicjob:stopNPCJob` | none | Clears the mission. | | `esx_mechanicjob:npcJobTargetSpawned` | `targetNetId` | Registers the spawned target vehicle. | | `esx_mechanicjob:npcJobTargetReached` | `targetNetId` | Marks the target as reached. | | `esx_mechanicjob:onNPCJobMissionCompleted` | `targetNetId, flatbedNetId` | Pays the reward to `society_mechanic` after distance, model and timing checks (minimum 30 seconds, 60 seconds between rewards). | | `esx_mechanicjob:impoundOwnedVehicle` | `plate, vehicleNetId` | Calls `exports['esx_garage']:impoundVehicle(plate)` if the vehicle is within 8 units. | | `esx_mechanicjob:getStockItem` | `itemName, count` | Withdraws from `society_mechanic`. | | `esx_mechanicjob:putStockItems` | `itemName, count` | Deposits into `society_mechanic`. | Harvest and craft actions are validated on the server: the player must be a mechanic above `recrue` (when player management is on) and stay within 3 units of the zone. The server stops the loop as soon as they walk away. <Callout type="warn" title="Security"> `getStockItem` and `putStockItems` check the job and a rate limit, but not the player's position or grade. Any mechanic with a modified client can withdraw society stock from anywhere on the map. If your society inventory holds valuable items, add a distance check to `Config.Zones.MechanicActions.Pos` in `server/modules/stock.lua`. </Callout> ## Server callbacks [#server-callbacks] | Callback | Returns | | --- | --- | | `esx_mechanicjob:getStockItems` | Items in the `society_mechanic` inventory (mechanic job only). | | `esx_mechanicjob:getPlayerInventory` | `{ items = ... }` of the caller (mechanic job only). | ## Client events [#client-events] | Event | Arguments | Description | | --- | --- | --- | | `esx_mechanicjob:npcJobStarted` | `targetZone` | Server accepted the mission start. | | `esx_mechanicjob:npcJobCompleted` | `targetNetId` | Mission paid out. | | `esx_mechanicjob:npcJobCompletionFailed` | `message` | Completion rejected (wrong place, wrong vehicle, no flatbed, cooldown). | ## Integration tips [#integration-tips] - Bills from the F6 menu are sent with `esx_billing:sendBill` to `society_mechanic`. Paid bills go to the society account, not to the mechanic. - To give mechanics repair kits from a shop instead of crafting, add `fixkit` and `carokit` to an [esx_shops](/docs/addons/esx_shops) store. The usable item handlers work regardless of how the item was obtained. - With ox_inventory, add the six items to ox_inventory's item list as well, otherwise they do not show up. ## Common issues [#common-issues] - **F7 says "you must be in a flatbed".** The NPC mission only starts from a vehicle with the `flatbed` model. - **NPC mission never pays.** The target must be one of `Config.Vehicles`, delivered within 25 units of `VehicleDelivery`, with the flatbed nearby, and at least 30 seconds after the start. - **Boss menu missing.** It only appears for the grade named `boss` with `Config.EnablePlayerManagement = true`. - **Uniform does not change.** The work clothes come from `skin_male` / `skin_female` in `job_grades`. Run `update_mechanic_uniforms.sql` if they are empty. ## Related [#related] - [esx_society](/docs/addons/esx_society) - [esx_billing](/docs/addons/esx_billing) - [esx_lscustom](/docs/addons/esx_lscustom) - [Create a job](/docs/guides/create-a-job) - [Add items](/docs/guides/add-items) # esx_optionalneeds (/docs/addons/esx_optionalneeds) `esx_optionalneeds` adds alcohol to ESX Legacy. It registers a `drunk` status in [esx_status](/docs/addons/esx_status) and a usable `beer` item. While the status is above zero the player walks drunk, gets motion blur and a screen filter; when it drops back to zero the effects fade out. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_optionalneeds` | | Side | Server (usable item) and client (status and effects) | | Manifest dependencies | `esx_lib`, `es_extended` | | Needs at runtime | [esx_status](/docs/addons/esx_status) (not listed in the manifest) | | SQL | `esx_optionalneeds.sql` adds the `beer` item | ## Installation [#installation] <Steps> <Step> ### Import the item [#import-the-item] ```sql title="esx_optionalneeds.sql" INSERT INTO `items` (`name`, `label`, `weight`) VALUES ('beer', 'Beer', 1) ; ``` The `localization/` folder has translated variants. Some of them, such as `en_esx_optionalneeds.sql`, also insert beer into a `shops` table. Only use those if your shop resource uses that table. </Step> <Step> ### Start it after esx_status [#start-it-after-esx_status] ```bash title="server.cfg" ensure esx_status ensure esx_optionalneeds ``` </Step> </Steps> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.TickTime` | `100` | Not used by the current code. | | `Config.UpdateClientTime` | `5000` | Not used by the current code. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language of the "you drank a beer" notification. | The drunk values are hard-coded in `client/main.lua` and `server/main.lua`. ## How it works [#how-it-works] 1. Using `beer` removes one beer, adds `250000` to `drunk` on the client, plays a drinking scenario and shows a notification. 2. On `esx_status:loaded` the client registers `drunk` with default `0`, color `#8F15A5`, visible only while above zero, and a tick that removes `1500` per esx_status tick (1 second by default). 3. A loop checks the status every second and applies a drunk level: | Drunk value | Level | Walk style | | --- | --- | --- | | 1 to 250000 | 0 | `move_m@drunk@slightlydrunk` | | 250001 to 500000 | 1 | `move_m@drunk@moderatedrunk` | | above 500000 | 2 | `move_m@drunk@verydrunk` | Every level also sets the `spectator5` timecycle, motion blur and `SetPedIsDrunk`. When the value reaches `0`, the screen fades and everything is reset. ## Events [#events] ### esx_optionalneeds:onDrink (client, net) [#esx_optionalneedsondrink-client-net] Plays the `WORLD_HUMAN_DRINKING` scenario for one second. Harmless if triggered by anyone. ```lua TriggerClientEvent('esx_optionalneeds:onDrink', playerId) ``` ### Making players drunk from your own script [#making-players-drunk-from-your-own-script] `drunk` is a custom (client-owned) status, so use the client events of esx_status: ```lua title="server/main.lua" ESX.RegisterUsableItem('vodka', function(source) local xPlayer = ESX.Player(source) xPlayer.removeInventoryItem('vodka', 1) TriggerClientEvent('esx_status:add', source, 'drunk', 400000) TriggerClientEvent('esx_optionalneeds:onDrink', source) end) ``` ```lua title="client/main.lua" -- sober up instantly, for example after a hospital visit TriggerEvent('esx_status:set', 'drunk', 0) ``` ## Common issues [#common-issues] - **Beer does nothing**: esx_status is not started, or started after esx_optionalneeds, so `drunk` was never registered. - **"Item beer does not exist"**: the SQL was not imported, or you use an inventory with its own item list (see [ox_inventory](/docs/guides/ox-inventory)). - **Effects stay after relog**: the drunk value is saved with the other custom statuses and continues after reconnect. It decays as usual. ## Related [#related] - [esx_status](/docs/addons/esx_status) - [esx_basicneeds](/docs/addons/esx_basicneeds) - [Add items](/docs/guides/add-items) # esx_pausemenu (/docs/addons/esx_pausemenu) esx_pausemenu replaces the GTA V pause screen with a branded NUI menu for ESX Legacy. When a loaded player presses `ESC` or `P`, it shows their character name, job, cash, bank, play time, ping, location, server time and online player count, plus buttons for the map, game settings, news, Discord, rules and leaving the server. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_pausemenu` (version 1.0.0, added with ESX Legacy 1.16.0) | | Side | Client NUI with one server callback | | Dependencies | `esx_lib`, `es_extended` | | Optional | [esx_scoreboard](/docs/addons/esx_scoreboard) for the online players button | | Database | None | The menu is plain HTML, CSS and JavaScript in `web/`, so there is no build step. It uses `xLib.colors` from [esx_lib](/docs/core/esx_lib), so it follows your ESX theme convars automatically. ## Installation [#installation] ```bash title="server.cfg" ensure esx_lib ensure es_extended ensure esx_scoreboard # optional ensure esx_pausemenu ``` Then set your own links in `config.lua` (at least `Config.Links.discord` and `Config.Links.rules`) and restart the resource. ## How it works [#how-it-works] 1. A client loop disables controls `200` and `199` (the two pause inputs) every frame while the player is loaded. 2. When one of them is released, the client calls the server callback `esx_pausemenu:getData`. 3. The server returns player data and the client opens the NUI with blur, the theme, the clock and the street name. 4. **Map** and **Settings** close the NUI and open the native `FE_MENU_VERSION_MP_PAUSE` frontend. **Resume** or `ESC` closes it. The menu does not open before `ESX.PlayerLoaded` is true, so the character selection screen keeps the normal pause behavior. It closes automatically on `esx:onPlayerLogout`. ## Configuration [#configuration] All keys in `config.lua`: | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `esx:locale` convar, then ESX config, then `"en"` | Language for UI text | | `Config.Brand.kicker` | `"FIVEM ROLEPLAY"` | Small text above the title | | `Config.Brand.title` | `"ESX LEGACY"` | Big title; also the server name fallback | | `Config.Brand.tagline` | `"ROLEPLAY BEYOND LIMITS"` | Line under the title | | `Config.Brand.city` | `"Los Santos"` | City shown in the location block | | `Config.Brand.established` | `"2015"` | "Established" year | | `Config.Brand.communityLine` | `"BUILT BY A COMMUNITY THAT CARES"` | Footer line | | `Config.Links.discord` | `"https://discord.gg/esx-framework"` | Discord button; empty disables it | | `Config.Links.rules` | `"https://esx-framework.org/"` | Rules button; empty disables it | | `Config.Links.store` | `""` | Optional store link in the news dialog | | `Config.News` | `{}` | List of `{ title, body, date }` entries | | `Config.PeopleCommand` | `"scoreboard"` | Command run by the online players chip | | `Config.Controls.pause` | `200` | ESC control id | | `Config.Controls.pauseAlt` | `199` | P control id | | `Config.ScreenBlurMs` | `180` | Blur fade duration in ms | | `Config.Debug` | `false` | Print debug messages | ```lua title="config.lua" Config.News = { { title = "Police recruitment open", body = "Apply on our Discord.", date = "20 OCT 2026" }, { title = "Server restart times", body = "Daily at 06:00 and 18:00.", date = "01 OCT 2026" }, } ``` News text is shown as plain text, and the UI shows up to 20 entries. The server name in the header comes from the `sv_projectName` convar, then `sv_hostname`, then `Config.Brand.title`. GTA color codes like `^1` are stripped. ## Callbacks, events and commands [#callbacks-events-and-commands] ### esx_pausemenu:getData (server callback) [#esx_pausemenugetdata-server-callback] ```lua ESX.TriggerServerCallback('esx_pausemenu:getData', function(data) end) ``` Returns `nil` if the player is not loaded or the rate limit is hit (2 requests burst, refilling 1 every 750 ms). Otherwise it returns: | Field | Type | Description | | --- | --- | --- | | `id` | `number` | Server id | | `name` | `string` | FiveM player name | | `characterName` | `string` | `xPlayer.getName()` | | `sex` | `string?` | `xPlayer.get('sex')` | | `ping` | `number` | Player ping | | `role`, `job` | `string` | Job label (localized "unemployed" for `unemployed`) | | `jobName` | `string` | Job name | | `bank`, `cash` | `number` | Account balances | | `playTime` | `number` | `xPlayer.getPlayTime()` in seconds | | `players`, `maxPlayers` | `number` | Online count and `sv_maxclients` | | `serverName` | `string` | See above | The callback only returns the caller's own data, so it is safe to leave exposed. ### esx_pausemenu:leaveServer (server net event) [#esx_pausemenuleaveserver-server-net-event] Drops the player who triggers it with the localized `leave_reason`. A client can only kick itself, so this is not a security concern. ### /esx_pausemenu (client command) [#esx_pausemenu-client-command] Toggles the menu. Use it to bind the menu to another key or to open it from another script with `ExecuteCommand('esx_pausemenu')`. ## Integration tips [#integration-tips] - **HUDs:** the menu calls `HideHudAndRadarThisFrame()` while open, which only hides native GTA HUD elements. NUI HUDs such as [esx_hud](/docs/addons/esx_hud) react to `esx:pauseMenuActive`, which es_extended fires for the native pause menu only, so they stay underneath the full-screen NUI. - **Theme:** change colors with the `esx:brand-color`, `esx:darkest-color` and related `setr` convars that esx_lib reads; no file edits needed. - **Languages:** UI strings are the `ui_*` keys in `locales/*.lua` (en, fr, de, es, it, nl, pl). Missing languages fall back to English. - **Other pause menus:** do not run esx_pausemenu together with another resource that disables controls 199 and 200, or both will fight over `ESC`. ## Common issues [#common-issues] - **ESC opens the normal GTA menu:** the player is not loaded yet, the resource failed to start (check that esx_lib is started first), or the callback returned `nil`. - **Menu flickers open and closed:** another script also opens the pause menu on ESC. Remove one of them. - **Map closes immediately:** a third-party script calls `SetPauseMenuActive(false)` every frame. ## Related [#related] - [esx_scoreboard](/docs/addons/esx_scoreboard) - [esx_hud](/docs/addons/esx_hud) - [esx_lib](/docs/core/esx_lib) - [Callbacks guide](/docs/guides/callbacks) # esx_policejob (/docs/addons/esx_policejob) esx_policejob turns the `police` job into a playable law-enforcement role: cloakrooms with grade uniforms, an armory where officers buy weapons, a garage for job cars and helicopters, a boss menu, and an F6 menu to search, cuff, drag, fine and impound. Install it when you want the stock ESX police department rather than writing your own. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_policejob` (version `1.0.2`) | | Side | client + server | | Job name | `police` (grades `recruit`, `officer`, `sergeant`, `lieutenant`, `boss`) | | Society | `society_police` (account, datastore and inventory) | | Hard dependencies | `esx_lib`, `es_extended`, `esx_billing`, `esx_vehicleshop` | | Also used | `oxmysql`, [esx_society](/docs/addons/esx_society) (boss menu, armory storage), `esx_garage` (impound) | | Optional | [esx_identity](/docs/core/esx_identity), [esx_license](/docs/addons/esx_license), [esx_service](/docs/addons/esx_service), [esx_status](/docs/addons/esx_status) | On start the server registers the society with `esx_society:registerSociety` and, when `Config.EnableESXService` is on and `Config.MaxInService` is not `-1`, activates a service limit through esx_service. ### What officers can do [#what-officers-can-do] - **Cloakroom**: switch between civilian clothes and the uniform for their grade, add a bulletproof vest or a high-visibility vest, and (optionally) pick a custom ped. - **Armory**: buy the weapons listed for their grade. With armory management enabled they can also deposit and withdraw weapons and items from the shared police storage. - **Garage**: buy job cars or helicopters for their grade, take them out, and store them again. - **F6 menu**: - Citizen interaction: ID card, body search, handcuff, drag, put in vehicle, take out of vehicle, fine, unpaid bills, and license check (when licenses are enabled). - Vehicle interaction: vehicle info, lockpick, impound, search the plate database. - Object spawner: cones, barriers, spike strips, boxes and a cash crate. - **Boss menu**: managed by esx_society at the `BossActions` marker. Money washing is disabled and the uniform editor is enabled. ## Installation [#installation] <Steps> <Step> ### Import the SQL [#import-the-sql] Run `esx_policejob.sql`. It adds the `society_police` rows to `addon_account`, `datastore` and `addon_inventory`, creates the `police` job with five grades, and creates and fills the `fine_types` table. Translated variants live in `localization/<lang>_esx_policejob.sql` (de, en, es, fi, fr, it, nl, pl, sr). Import only one of them. </Step> <Step> ### Start it after its dependencies [#start-it-after-its-dependencies] ```bash title="server.cfg" ensure oxmysql ensure es_extended ensure esx_lib ensure esx_addonaccount ensure esx_addoninventory ensure esx_datastore ensure esx_society ensure esx_billing ensure esx_vehicleshop ensure esx_policejob ``` </Step> <Step> ### Turn on the optional integrations [#turn-on-the-optional-integrations] Set `Config.EnableESXIdentity`, `Config.EnableLicenses`, `Config.EnableESXService` and `Config.EnableESXOptionalneeds` to match the resources you actually run. </Step> </Steps> <Callout type="warn" title="txAdmin recipe installs"> The official ESX recipe (`legacy.sql`) already contains the `police` job, its society rows and the `fine_types` table. If you used the recipe, do not import `esx_policejob.sql` again. It fails with duplicate key and "table already exists" errors. </Callout> ## Configuration [#configuration] All options are in `config.lua`. | Key | Default | Description | | --- | --- | --- | | `Config.DrawDistance` | `10.0` | Distance at which markers are drawn. | | `Config.ActionVehicleDistance` | `2.0` | How close a vehicle must be for vehicle actions in the F6 menu. | | `Config.MarkerType` | `{ Cloakrooms = 20, Armories = 21, BossActions = 22, Vehicles = 36, Helicopters = 34 }` | Marker type per station point. | | `Config.MarkerSize` | `{ x = 1.5, y = 1.5, z = 0.5 }` | Marker size. | | `Config.MarkerColor` | `{ r = 50, g = 50, b = 204 }` | Marker color. | | `Config.EnablePlayerManagement` | `true` | Enables society management (boss menu). | | `Config.EnableArmoryManagement` | `false` | Adds weapon and item deposit/withdraw to the armory. | | `Config.EnableESXIdentity` | `true` | Shows name, date of birth, sex and height from esx_identity on ID cards and in vehicle owner lookups. | | `Config.EnableESXOptionalneeds` | `false` | Shows the drunk level (from esx_status) in the ID card menu. | | `Config.EnableLicenses` | `false` | Shows licenses during body search and enables license checks and revocation through esx_license. | | `Config.EnableHandcuffTimer` | `true` | Uncuffs automatically after `Config.HandcuffTimer`. | | `Config.HandcuffTimer` | `10 * 60000` | Auto-uncuff delay in milliseconds (10 minutes). | | `Config.EnableCustomPeds` | `false` | Lets officers pick a ped model from `Config.CustomPeds` in the cloakroom. | | `Config.EnableESXService` | `false` | Requires officers to be in service (esx_service) before using the armory, garage and F6 menu. | | `Config.MaxInService` | `-1` | Maximum number of officers in service. `-1` means no limit. | | `Config.EnableFinePresets` | `false` | `true` uses the `fine_types` categories, `false` shows a free amount and reason input. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language. | | `Config.OxInventory` | `ESX.GetConfig().OxInventory` | Read from es_extended, do not change. | ### Stations [#stations] `Config.PoliceStations` is a table of stations (the default one is `LSPD`). Each station has: | Field | Description | | --- | --- | | `Blip` | `Coords`, `Sprite`, `Display`, `Scale`, `Colour` for the map blip. | | `Cloakrooms` | List of `vector3` positions. | | `Armories` | List of `vector3` positions. The server only accepts armory actions within 8 units of one of these. | | `Vehicles` / `Helicopters` | List of garages. Each has a `Spawner` position, an `InsideShop` preview position and `SpawnPoints` (`coords`, `heading`, `radius`). | | `BossActions` | List of `vector3` positions for the boss menu. | ### Grade-based tables [#grade-based-tables] These tables are keyed by **grade name** (`recruit`, `officer`, `sergeant`, `lieutenant`, `boss`). If you rename grades in the database, rename the keys too. - `Config.AuthorizedWeapons[grade]`: list of `{ weapon = 'WEAPON_X', price = n, components = { ... } }`. `components` holds one price per component index of the weapon in ESX's weapon list; `nil` hides that component. - `Config.AuthorizedVehicles.car[grade]` and `Config.AuthorizedVehicles.helicopter[grade]`: list of `{ model = 'police3', price = n, props = { ... } }`. `props` is optional and is merged into the stored vehicle properties. - `Config.CustomPeds`: `shared` entries are available to every grade, plus one list per grade. Each entry has `label`, `maleModel`, `femaleModel`. - `Config.Uniforms`: one entry per grade plus `bullet_wear` and `gilet_wear`, each with `male` and `female` skinchanger components. ```lua title="config.lua" -- Let recruits buy a basic cruiser Config.AuthorizedVehicles.car.recruit = { { model = 'police', price = 5000 }, } ``` <Callout type="info"> The server rejects any vehicle whose configured price is `0`. To hand out free cars, use a price of `1` or extend the script. </Callout> ## Server events [#server-events] All of these are network events that the client triggers. Every handler checks that the caller has the `police` job and is not off duty, and most also check distance. Do not call them from other resources unless you need the same behavior. | Event | Arguments | Server checks | | --- | --- | --- | | `esx_policejob:handcuff` | `target` | On-duty police, within 5 units of target. Toggles the cuffs. | | `esx_policejob:drag` | `target` | On-duty police, target is cuffed, within 5 units. | | `esx_policejob:putInVehicle` | `target` | On-duty police, target is cuffed, within 5 units. | | `esx_policejob:OutVehicle` | `target` | On-duty police, target is cuffed, within 8 units. | | `esx_policejob:confiscatePlayerItem` | `target, itemType, itemName, amount` | On-duty police, target cuffed and within 5 units. `itemType` is `item_standard`, `item_account` or `item_weapon`. | | `esx_policejob:getStockItem` | `itemName, count` | On-duty police near an armory. Withdraws from `society_police`. | | `esx_policejob:putStockItems` | `itemName, count` | On-duty police near an armory. Deposits into `society_police`. | | `esx_policejob:impoundOwnedVehicle` | `plate` | Police job, vehicle with that plate within 8 units. Calls `exports['esx_garage']:impoundVehicle(plate)`. | <Callout type="warn" title="Security"> `confiscatePlayerItem` moves items, money and weapons between players, so it is the event cheaters target first. The current version validates job, duty, distance and cuff state on the server. If you fork the script or add your own actions, keep those checks on the server side and never trust a target ID or amount sent by the client. </Callout> ## Server callbacks [#server-callbacks] Callbacks are registered with `xLib.callback.registerCompat`, so they work with both `xLib.callback` and the classic `ESX.TriggerServerCallback`. | Callback | Arguments | Returns | | --- | --- | --- | | `esx_policejob:getOtherPlayerData` | `target, notify` | Table with `name`, `job`, `grade`, `inventory`, `accounts`, `weapons`, plus `dob`, `height`, `sex` (identity), `drunk` (status) and `licenses` (license) when enabled. Empty table unless the target is cuffed and within 5 units. | | `esx_policejob:getFineList` | `category` | Rows from `fine_types` for that category (cached in memory). | | `esx_policejob:getVehicleInfos` | `plate` | `{ plate, owner? }`. | | `esx_policejob:getArmoryWeapons` | none | Weapons stored in the `society_police` datastore. | | `esx_policejob:addArmoryWeapon` | `weaponName, removeWeapon` | `true` on success. | | `esx_policejob:removeArmoryWeapon` | `weaponName` | `true` on success, gives the weapon with 500 ammo. | | `esx_policejob:buyWeapon` | `weaponName, type, componentNum` | `type` `1` buys the weapon, `2` buys a component. Price comes from `Config.AuthorizedWeapons`. | | `esx_policejob:buyJobVehicle` | `vehicleProps, type` | Inserts the car into `owned_vehicles` with `job = 'police'` and a `POL` plate. | | `esx_policejob:storeNearbyVehicle` | `plates` | Plate that was stored, or `false`. | | `esx_policejob:getStockItems` | none | Items in the `society_police` inventory. | | `esx_policejob:getPlayerInventory` | none | `{ items = ... }` of the caller. | ## Client events [#client-events] | Event | Triggered by | Effect | | --- | --- | --- | | `esx_policejob:handcuff` | server | Toggles the cuffed animation and control locks on the target. | | `esx_policejob:unrestrain` | server or local | Removes cuffs. Fired automatically on spawn and when the resource stops. | | `esx_policejob:drag` | server | `(copId)` toggles being dragged by that officer. | | `esx_policejob:putInVehicle` | server | Warps the cuffed player into a free passenger seat of the closest vehicle (searching from the back). | | `esx_policejob:OutVehicle` | server | Pulls the cuffed player out of the vehicle. | ## Keybinds [#keybinds] | Name | Default | Action | | --- | --- | --- | | `police:quickactions` | `F6` | Opens the police actions menu. | | `police:interact` | `E` | Uses the marker you are standing in (cloakroom, armory, garage, boss menu). | Both are registered through `xLib.addKeybind`, so players can rebind them under **Settings → Key Bindings → FiveM**. ## Integration tips [#integration-tips] Check whether someone is cuffed before letting them use your own features: ```lua title="server/main.lua" RegisterNetEvent('myphone:open', function() local src = source if Player(src).state.isHandcuffed then return -- cuffed players cannot use the phone end TriggerClientEvent('myphone:show', src) end) ``` ```lua title="client/main.lua" if LocalPlayer.state.isHandcuffed then ESX.ShowNotification('You cannot do that while cuffed.') return end ``` Fines are sent with `esx_billing:sendBill` to `society_police`, so paid fines land in the police society account. To pay officers a commission or log fines, hook into [esx_billing](/docs/addons/esx_billing) rather than this resource. ## Common issues [#common-issues] - **"Off duty" notification everywhere.** The script respects `job.onDuty`. A player whose duty flag is `false` cannot use the armory, garage or F6 menu. - **Nothing happens when buying a car.** Check that the grade has entries in `Config.AuthorizedVehicles`, the price is above `0`, the player has enough cash, and they stand within 35 units of the garage `InsideShop` or `Spawner`. - **ID card shows no name, date of birth or height.** `Config.EnableESXIdentity` is `true` by default; if you do not run esx_identity, set it to `false`. - **Impound does nothing.** Impounding needs `esx_garage` to export `impoundVehicle`. Without it the call silently fails. - **Duplicate SQL errors.** See the txAdmin recipe note above. ## Related [#related] - [esx_society](/docs/addons/esx_society) - [esx_billing](/docs/addons/esx_billing) - [esx_service](/docs/addons/esx_service) - [esx_license](/docs/addons/esx_license) - [Create a job](/docs/guides/create-a-job) # esx_property (/docs/addons/esx_property) esx_property is the official housing resource for ESX Legacy. Players buy properties from a list of 95 premade houses and apartments, enter them in their own routing bucket, give keys to friends, place furniture, store vehicles in a garage and watch a CCTV camera. Admins and an optional real estate job can create, edit and sell properties in-game. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_property` (version `2.0`) | | Side | Client + server | | Dependencies | `esx_lib`, `es_extended`, `oxmysql` | | Optional | `ox_inventory` (stash), `esx_datastore` + `skinchanger` (wardrobe), `esx_society` + `esx_addonaccount` (real estate job), `esx_context`, `esx_notify`, `esx_progressbar` (raids), IPL or shell resources | | SQL file | None. Tables are created at startup | | Storage | `esx_properties`, `esx_property_migrations` | Features in short: - **Buying and selling.** Buying takes the price from the bank. Selling back pays 60 % of the price. - **Keys.** The owner can give and remove keys. Players with keys can enter and use the property. - **Locking.** Locked doors keep visitors out. `Config.CanAlwaysExit` and `Config.OwnerCanAlwaysEnter` control edge cases. Visitors can knock. - **Furniture.** Furniture stores sell props by category, paid from the bank, placed and moved with keyboard controls. - **Garage.** Store owned vehicles inside a property and take them out again. - **CCTV.** A camera above the door with zoom and night vision. Optional Discord webhook for screenshots. - **Raids.** On-duty `police` (and optionally admins) can break in, optionally consuming an item. - **Interiors.** IPL interiors are on by default. Shell interiors need `Config.Shells = true` and a shell pack. ## Installation [#installation] <Steps> <Step> ### Ensure the resource [#ensure-the-resource] ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ensure esx_datastore ensure esx_society ensure ox_inventory ensure esx_property ``` Only `oxmysql`, `esx_lib` and `es_extended` are required. Start optional resources before `esx_property`. </Step> <Step> ### First start: JSON to SQL migration [#first-start-json-to-sql-migration] On startup the resource creates `esx_properties` and `esx_property_migrations` (both must be InnoDB). If the migration `properties_json_to_sql_v1` has not run yet, it reads `properties.json`, imports every property into SQL in one transaction and keeps the JSON file unchanged. The import refuses to run if `esx_properties` already has rows without a migration marker, or if `properties.json` is missing or empty. While storage is not ready, all property actions are rejected. </Step> <Step> ### Optional setup done automatically [#optional-setup-done-automatically] The resource inserts a shared `property` datastore if the `datastore` table exists. With `Config.PlayerManagement.Enabled` it also creates the real estate job, its grades and the society account, and registers the society with esx_society. </Step> </Steps> ## Configuration [#configuration] ### General [#general] | Key | Default | Description | | --- | --- | --- | | `Config.ForSaleBlips` | `false` | Blips for properties for sale (can clutter the map). | | `Config.OwnedBlips` | `true` | Blips for owned properties. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale. | | `Config.Shells` | `false` | Enable shell interiors. | | `Config.SaveInterval` | `5` | Fallback save interval in minutes. Changes are also saved after a short debounce. | | `Config.MaxFurniturePerProperty` | `500` | Limit for new furniture purchases. | | `Config.MaxVehiclesPerProperty` | `100` | Limit for new garage deposits. | | `Config.CanAlwaysExit` | `true` | `false` allows players to be locked in. | | `Config.OwnerCanAlwaysEnter` | `true` | `false` means the owner needs the door unlocked. | | `Config.MaxNameLength` | `20` | Max length of property names. | | `Config.CanCustomiseInventoryAndWardrobePositions` | `true` | Owners can move storage and wardrobe points. | | `Config.WipeCustomNameOnSell` | `true` | Reset the custom name on sale. | | `Config.WipeFurnitureOnSell` | `true` | Remove furniture on sale. | | `Config.AllowedGroups` | `{ "admin" }` | ESX groups with admin access. | | `Config.OxInventory` | `ESX.GetConfig().OxInventory` | Uses ox_inventory stashes. | ### Prices, raids, garage, logs [#prices-raids-garage-logs] | Key | Default | Description | | --- | --- | --- | | `Config.ZonePriceOptions.Enabled` | `true` | Suggest prices by zone in the creator. | | `Config.ZonePriceOptions.Default` | `250000` | Base price for suggestions. | | `Config.ZonePrices` | `[0] = 10` ... `[5] = 1` | Multiplier per zone class (posh to scum). | | `Config.Raiding.Enabled` | `true` | Allow raids. | | `Config.Raiding.CanAdminsRaid` | `true` | Admins may raid too. | | `Config.Raiding.ItemRequired` | `{ RemoveItem = true, name = "bread", label = "Bread", ItemCount = 1 }` | Item needed to raid. Remove the table to disable. Change `bread` to a real tool. | | `Config.Raiding.Animation` | `{ type = "Scenario", Scenario = "WORLD_HUMAN_HAMMERING" }` | Raid animation. | | `Config.Garage.Enabled` | `true` | Property garages. | | `Config.Garage.OwnedVehiclesOnly` | `true` | Only owned vehicles can be stored. | | `Config.Garage.MySQLquery` | ``"UPDATE `owned_vehicles` SET `stored` = ? WHERE `plate` = ?"`` | Query run when storing or retrieving. Adapt it to your garage schema. | | `Config.Logs.Webhook` | `""` | Discord webhook for logs (server only). | | `Config.Logs.LogLevel` | `1` | `0` none, `1` major, `2` major + minor, `3` all. | ### Furniture and CCTV [#furniture-and-cctv] | Key | Default | Description | | --- | --- | --- | | `Config.Furniture.Enabled` | `true` | Furniture system. | | `Config.Furniture.RotationSpeed` / `MovementSpeed` / `MovementZspeed` | `0.4` / `0.01` / `0.05` | Placement speeds. | | `Config.Furniture.Controls` | control IDs | Arrow keys move, Alt/Shift rotate, Enter confirms, Backspace exits. | | `Config.FurnitureStores` | Ikea, Electronics Store | Stores and the categories they sell. | | `Config.FurnitureCatagories` | list | Props per category with prices. | | `Config.CCTV.Enabled` | `true` | CCTV system. | | `Config.CCTV.PictureWebook` | `""` (server) | Discord webhook for CCTV screenshots. Leave the client string unchanged. | | `Config.CCTV.HeightAboveDoor` / `FOV` | `2.5` / `80.0` | Camera placement. | | `Config.CCTV.MaxLeftRotation` / `MaxRightRotation` / `MaxUpRotation` / `MaxDownRotation` | `80` / `-50` / `10` / `-45` | Rotation limits. | | `Config.CCTV.MaxZoom` / `MinZoom` / `RotateSpeed` | `30` / `100` / `0.3` | Zoom and speed. | ### Real estate job (`Config.PlayerManagement`) [#real-estate-job-configplayermanagement] | Key | Default | Description | | --- | --- | --- | | `Enabled` | `false` | Enable the real estate job. | | `job` / `joblabel` | `"realestateagent"` / `"Estate Agent"` | Job name and label. | | `society` | `"society_realestateagent"` | Society account. | | `SalePercentage` | `0.25` | Share of the price paid to the selling agent's bank. The rest goes to the society. | | `jobRanks` | trainee (0), agent (1), boss (2) | Grades created automatically. | | `Permissions` | e.g. `CreateProperty = 0`, `DeleteProperty = 2` | Minimum grade per action. | | `Locations` | office entrance, exit, management menu | Real estate office points. | `Config.Interiors` lists the IPL and shell interiors with their spawn, wardrobe and storage positions. `Config.WardrobeInteraction` is a client function you can replace to use your own clothing script. ## Commands [#commands] | Command | Who | Description | | --- | --- | --- | | `/property:create` | Admins, or real estate agents with the `CreateProperty` grade | Opens the property creator. | | `/property:admin` | `Config.AllowedGroups` | Admin menu: manage, teleport to or delete any property. | | `/property:save` | `Config.AllowedGroups` | Saves all properties now. | | `/property:refresh` | `Config.AllowedGroups` | Resends property data to all players. | | `/getoffset` | Client command | Prints your offset inside a property to F8 (for wardrobe and storage positions). | Command names come from the locale file and can differ if you change it. ## Exports [#exports] ### Server [#server] | Export | Returns | | --- | --- | | `GetProperties()` | The full properties table, indexed by property ID. | | `GetOwnedProperties()` | Array of owned properties. | | `GetNonOwnedProperties()` | Array of properties for sale. | | `GetPlayerProperties(identifier)` | Array of properties owned by that identifier. | | `GetPropertyKeys(propertyId)` | Table of key holders, `{ [identifier] = true }`. | | `DoesPlayerHaveKeys(propertyId, identifier)` | `true` / `false` (see note below). | | `ForceSaveProperties()` | Saves immediately and logs the calling resource. | ```lua title="server/main.lua" ESX.RegisterCommand('myhouses', 'user', function(xPlayer) local owned = exports.esx_property:GetPlayerProperties(xPlayer.getIdentifier()) for _, property in ipairs(owned) do xPlayer.showNotification(property.setName ~= '' and property.setName or property.Name) end end, false, { help = 'List your properties' }) ``` A property table contains among others `Name`, `setName`, `Price`, `Interior`, `Entrance`, `Owner`, `Owned`, `Locked`, `Keys`, `garage`, `cctv`, `furniture` and `positions`. Treat it as read-only. Changes from other resources are not saved. <Callout type="warn" title="Edge cases"> `GetPropertyKeys` and `DoesPlayerHaveKeys` index `Properties[propertyId]` without a nil check, so an invalid ID throws an error. Validate the ID first. </Callout> ### Client [#client] `exports.esx_property:GetProperties()` returns the property data the server synced to this player. For properties without access, the server strips owner, keys, furniture and garage data. ## Events and callbacks [#events-and-callbacks] All interaction (buy, sell, lock, keys, furniture, garage, CCTV, raids, admin edits) goes through server callbacks named `esx_property:<action>`, for example `esx_property:buyProperty`, `esx_property:toggleLock`, `esx_property:GiveKey` and `esx_property:StoreVehicle`. Every callback is wrapped by a rate limiter, refuses to run while storage is not ready, validates property IDs and checks ownership, keys, admin groups or real estate grade on the server. The client-triggerable net events are `esx_property:enter`, `esx_property:leave`, `esx_property:SetVehicleOut` and `esx_property:server:createProperty`. They are rate limited too. `enter` checks access and distance to the entrance, and `createProperty` requires admin or the `CreateProperty` grade and validates every field. Client events sent by the server include `esx_property:syncProperties`, `esx_property:syncProperty`, `esx_property:receivePropertyOffer`, `esx_property:giveKeyAccess`, `esx_property:RemoveKey` and the furniture events (`syncFurniture`, `loadFurniture`, `placeFurniture`, `removeFurniture`, `editFurniture`). ## Common issues [#common-issues] - **"properties.json is missing or empty; SQL migration was not completed".** The first start needs the shipped `properties.json`. Restore it, or insert at least one row and the migration marker manually if you start from scratch. - **"SQL property IDs must be contiguous".** IDs in `esx_properties` must be 1..n without gaps. Do not delete rows by hand. Use the admin menu. - **Storage marker does nothing.** Storage needs ox_inventory. - **Wardrobe is empty.** Install esx_datastore and save outfits through [esx_clotheshop](/docs/addons/esx_clotheshop). - **Vehicles stay "out" in your garage script.** Adjust `Config.Garage.MySQLquery` to your schema. - **Shell interiors are black or missing.** Enable `Config.Shells` and install the shell pack the interiors refer to. ## Related [#related] - [esx_society](/docs/addons/esx_society) - [esx_datastore](/docs/addons/esx_datastore) - [esx_garage](/docs/addons/esx_garage) - [ox_inventory with ESX](/docs/guides/ox-inventory) - [Admin permissions](/docs/guides/admin-permissions) # esx_rpchat (/docs/addons/esx_rpchat) esx_rpchat adds the classic roleplay chat commands to an ESX Legacy server: `/me` and `/do` for nearby players, a global OOC channel, `/twt` and `/anontwt` "tweets", and `/msg` private messages. Every command is rate limited on the server, and proximity messages are filtered by distance and routing bucket. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_rpchat` | | Side | Server commands, small client handler | | Dependencies | `esx_lib`, `es_extended`, the default `chat` resource | | Database | None | ## Installation [#installation] ```bash title="server.cfg" ensure chat ensure esx_lib ensure es_extended ensure esx_rpchat ``` ## Commands [#commands] | Command | Who sees it | Prefix (en locale) | Color | | --- | --- | --- | --- | | (plain message) | Everyone | `OOC \| Name` | grey | | `/me <text>` | Players within `ProximityDistance` | `me \| Name` | red | | `/do <text>` | Players within `ProximityDistance` | `do \| Name` | blue | | `/twt <text>` | Everyone | `[Twitter] (@Name)` | light blue | | `/anontwt <text>` | Everyone | `[Twitter] (@Anonymous)` | light blue | | `/msg <id> <text>` | Target and sender | `PM from Name[id]` / `PM SEND TO Name[id]` | orange | - `/me` describes an action ("/me opens the trunk"); `/do` describes the scene or answers a `/me` ("/do The trunk is empty"). - `/me` and `/do` text is cut to `ProximityMaxLength` characters (UTF-8 aware). `/msg` uses the same limit. - `/msg` ignores empty messages, messages to yourself and invalid ids. It uses FiveM names, not character names. - None of the commands can be used from the server console. <Callout type="info"> OOC works through the legacy `chatMessage` event: esx_rpchat cancels every non-command message and re-broadcasts it with its prefix. If another chat resource also handles `chatMessage`, messages can appear twice. </Callout> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language for prefixes and help text | | `Config.OnlyFirstname` | `false` | Show only `firstName` instead of the full name | | `Config.EnableESXIdentity` | `true` | Use the character name (`xPlayer.getName()`); `false` uses the FiveM name | | `Config.ProximityDistance` | `20.0` | Radius in meters for `/me` and `/do` | | `Config.ProximityMaxLength` | `256` | Max characters kept for `/me`, `/do` and `/msg` | | `Config.OocCooldown` | `3000` | Milliseconds between OOC messages per player | | `Config.TwtCooldown` | `10000` | Milliseconds between `/twt` | | `Config.AnonTwtCooldown` | `15000` | Milliseconds between `/anontwt` | | `Config.MeCooldown` | `1000` | Milliseconds between `/me` | | `Config.DoCooldown` | `1000` | Milliseconds between `/do` | The server also reads `Config.MsgCooldown` for `/msg`. It is not in the shipped `config.lua`, so it falls back to `1000`. Add it if you want a different value. Setting any cooldown to `0` disables that limit. A rate-limited player gets a red `SYSTEM` message like "Please wait 3 seconds before sending another message." ## Events [#events] ### esx_rpchat:sendProximityMessage (client) [#esx_rpchatsendproximitymessage-client] The server sends `/me` and `/do` to nearby players through this event: ```lua -- server xLib.triggerClientEvent('esx_rpchat:sendProximityMessage', targets, title, message, color) ``` The target list comes from `ESX.OneSync.GetPlayersInArea` in the sender's routing bucket, so players in other instances (apartments, garages) do not see the message. The client handler also accepts an older 4-argument form `(playerId, title, message, color)` and then checks the distance to that player locally. You can reuse the 3-argument form from your own server scripts to send any styled chat line to a list of players: ```lua title="server/main.lua" local targets = { source, otherId } xLib.triggerClientEvent('esx_rpchat:sendProximityMessage', targets, 'radio | Dispatch', '10-4, units en route', { 0, 200, 0 }) ``` ## Integration tips [#integration-tips] - **Logging:** `/anontwt` is anonymous to players but not to the server. If you need moderation, add a log call in the `anontwt` command in `server/main.lua`. - **Localization:** prefixes contain chat color codes (`^0`, `^4`, `^5`). Edit `locales/<lang>.lua` to restyle them. - **Custom chat UIs:** messages are sent through `chat:addMessage`, so any chat resource that implements it works. ## Common issues [#common-issues] - **OOC messages show twice:** another resource also re-broadcasts `chatMessage`. Disable one of them. - **/me not visible to a nearby player:** the player is in a different routing bucket, or OneSync is off (`ESX.OneSync.GetPlayersInArea` needs OneSync). - **Names show as FiveM names:** the player has no character name yet, or `EnableESXIdentity` is `false`. ## Related [#related] - [esx_identity](/docs/core/esx_identity) - [esx_chat_theme](/docs/core/esx_chat_theme) - [OneSync functions](/docs/es_extended/server/onesync) - [Commands](/docs/es_extended/commands) # esx_scoreboard (/docs/addons/esx_scoreboard) esx_scoreboard is a player list for ESX Legacy that players open with a key (default `Z`). It shows every online player with server id, character name, job, grade and ping, a counter per job, a search bar, and a list of ongoing activities such as robberies or races that other resources push in through server exports. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_scoreboard` (version 1.0.0, added with ESX Legacy 1.16.0) | | Side | Server data cache, client NUI (Svelte) | | Dependencies | `esx_lib`, `es_extended` | | Database | None | | Used by | [esx_pausemenu](/docs/addons/esx_pausemenu) (online players button) | The server keeps an in-memory cache of players and job counts. It updates it on `esx:playerLoaded`, `esx:setJob`, `esx:jobDataRefreshed` and `playerDropped`, and runs a full reconcile every minute as a safety net. Data is only sent to players who currently have the scoreboard open, and the list is paged, so it scales to large servers. ## Installation [#installation] ```bash title="server.cfg" ensure esx_lib ensure es_extended ensure esx_scoreboard ``` The UI ships prebuilt in `web/dist`. To change the Svelte source, run `npm install` and `npm run build` in `web` (Node.js 18 or newer). ## Usage [#usage] - Press `Z` or run `/scoreboard` to toggle it. Press `ESC` to close it. - Search filters by player name, job label or server id. - While it is open, camera look and attack controls are disabled. ## Configuration [#configuration] All keys are in `config/main.lua`. ### Display [#display] | Key | Default | Description | | --- | --- | --- | | `Config.OpenKey` | `"Z"` | Default key for the `scoreboard` key mapping | | `Config.ServerName` | `"ESX Server"` | Name in the header | | `Config.MaxPlayers` | `nil` | Slots shown in the header; `nil` reads `sv_maxclients` | | `Config.LogoUrl` | `""` | HTTPS logo URL; empty shows the ESX placeholder | | `Config.Jobs` | 7 jobs | Badge color per job, see below | | `Config.ActivityTypes` | 6 types | Label and icon per activity type | | `Config.Debug` | `false` | Debug logging | ```lua title="config/main.lua" Config.Jobs = { police = { label = "Police", color = "#3B82F6" }, ambulance = { label = "EMS", color = "#EF4444" }, -- mechanic, taxi, realtor, cardealer, banker ... } Config.ActivityTypes = { robbery = { label = "Robbery", icon = "💰" }, -- heist, drug, race, hostage, shootout } ``` Jobs that are not listed in `Config.Jobs` are still counted; they just use the default color. ### Performance and limits [#performance-and-limits] | Key | Default | Description | | --- | --- | --- | | `Config.SummaryInterval` | `10000` | ms between summary broadcasts (counts, jobs, activities) to open scoreboards | | `Config.PageRefreshInterval` | `15000` | ms between refreshes of the visible player page | | `Config.PageRefreshBatchSize` | `25` | Open scoreboards refreshed before the server thread yields | | `Config.PageRefreshBatchDelay` | `50` | ms between refresh batches | | `Config.PingRefreshInterval` | `30000` | ms between ping updates | | `Config.FullReconcileInterval` | `60000` | ms between full rebuilds of the player cache | | `Config.OpenCooldown` | `2000` | Minimum ms between open requests per player | | `Config.PageRequestCooldown` | `1500` | Minimum ms between page, search or sort requests per player | | `Config.ActivityDebounce` | `1000` | Debounce for activity broadcasts | | `Config.DefaultPageSize` | `50` | Rows per page | | `Config.MaxPageSize` | `100` | Hard cap for rows per page | | `Config.MaxPageCacheEntries` | `256` | Cached page payloads | | `Config.MaxPlayerQueryCacheEntries` | `64` | Cached search or sort results | | `Config.MaxSearchLength` | `48` | Max search text length accepted | | `Config.MaxActivities` | `64` | Activities kept; the oldest is dropped first | | `Config.MaxActivityPlayers` | `16` | Player entries stored per activity | | `Config.UpdateInterval` | `Config.SummaryInterval` | Legacy alias for older configs | ## Server exports [#server-exports] Use these from your own server scripts (robberies, heists, races) to show what is going on in the city. ### AddActivity [#addactivity] ```lua local id = exports.esx_scoreboard:AddActivity(activityType, label, location, players) ``` | Name | Type | Description | | --- | --- | --- | | `activityType` | `string` | Key such as `robbery`; only letters, digits, `_` and `-` are kept | | `label` | `string?` | Display text; defaults to the label from `Config.ActivityTypes` | | `location` | `string?` | Location text | | `players` | `table?` | Array of server ids or strings, capped at `MaxActivityPlayers` | Returns the numeric activity id, or `-1` if the type is invalid. ### UpdateActivity [#updateactivity] ```lua local ok = exports.esx_scoreboard:UpdateActivity(id, { label = '...', location = '...', players = { ... } }) ``` Only the fields you pass are changed. Returns `true` when the activity was found. ### RemoveActivity [#removeactivity] ```lua local ok = exports.esx_scoreboard:RemoveActivity(id) ``` Returns `true` when the activity was found and removed. ### GetActiveActivities [#getactiveactivities] ```lua local list = exports.esx_scoreboard:GetActiveActivities() ``` Returns a copy of all activities. Each entry has `id`, `type`, `label`, `location`, `startTime` (`os.time()`) and `players`. ### Example: store robbery [#example-store-robbery] ```lua title="server/robbery.lua" local activeRobberies = {} RegisterNetEvent('myrobbery:start', function(storeName) local src = source -- validate distance, cooldown and police count here activeRobberies[src] = exports.esx_scoreboard:AddActivity('robbery', storeName, 'Grove Street', { src }) end) local function endRobbery(src) local id = activeRobberies[src] if id then exports.esx_scoreboard:RemoveActivity(id) activeRobberies[src] = nil end end AddEventHandler('playerDropped', function() endRobbery(source) end) ``` Activities are not removed automatically. Always remove them when the event ends, including when the player disconnects. ## Network events [#network-events] | Event | Direction | Notes | | --- | --- | --- | | `esx_scoreboard:server:open` | client to server | Open and register as active viewer; rate limited by `OpenCooldown` | | `esx_scoreboard:server:requestData` | client to server | Same as `open` | | `esx_scoreboard:server:requestPage` | client to server | `{ page, pageSize, search, sortBy, sortAsc }`; only for active viewers, rate limited | | `esx_scoreboard:server:close` | client to server | Stop receiving updates | | `esx_scoreboard:client:receiveSummary` | server to client | Counts, jobs, activities, server info | | `esx_scoreboard:client:receivePage` | server to client | One page of players | | `esx_scoreboard:client:receiveActivities` | server to client | Activity list | The client-triggerable events only return public data (names, jobs, ping) and are rate limited, so they cannot be abused for more than reading the player list. ## Common issues [#common-issues] - **"ESX.GetJobs() unavailable after 10 attempts":** es_extended was not ready. Make sure it starts before esx_scoreboard. - **Changing `OpenKey` has no effect:** FiveM saves key mappings per player. Existing players must rebind it themselves. - **Players missing right after restart:** the cache is rebuilt from `GetPlayers()` once ESX jobs are available; wait for the first reconcile or reopen the list. - **Old robberies stay on the board:** your script never called `RemoveActivity`. ## Related [#related] - [esx_pausemenu](/docs/addons/esx_pausemenu) - [esx_policejob](/docs/addons/esx_policejob) - [esx_lib](/docs/core/esx_lib) - [Server functions](/docs/es_extended/server/functions) # esx_service (/docs/addons/esx_service) `esx_service` tracks which players of a job are on duty. Job resources such as [esx_taxijob](/docs/addons/esx_taxijob) and [esx_mechanicjob](/docs/addons/esx_mechanicjob) use it to cap the number of players in service, show how many colleagues are active and send a notification to everyone on duty. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_service` | | Side | Server (state, callbacks) and client (notification) | | Dependencies | `esx_lib`, `es_extended` | | SQL | none | | Config | none | | State | In memory, reset on restart. Counts are mirrored to `GlobalState[serviceName]`. | ## Installation [#installation] ```bash title="server.cfg" ensure esx_service # job resources that use it ensure esx_taxijob ``` A service name is the job name, for example `taxi`. A player can only enter the service of their own job. ## Server API [#server-api] All server events are local and protected: `GetInvokingResource()` must be set, so calls coming straight from a client are blocked and logged as `Blocked external call`. ### esx_service:activateService [#esx_serviceactivateservice] Creates a service and sets its player limit. Call it once on resource start. ```lua TriggerEvent('esx_service:activateService', name, max) ``` | Name | Type | Description | | --- | --- | --- | | `name` | string | Service (job) name. | | `max` | number or nil | Maximum players on duty. `-1` or a non-number means unlimited. | ```lua title="server/main.lua" TriggerEvent('esx_service:activateService', 'taxi', Config.MaxInService) ``` A service that was never activated is created on first use with no limit. ### esx_service:disableService [#esx_servicedisableservice] Takes a player off duty. ```lua TriggerEvent('esx_service:disableService', name, playerId) ``` | Name | Type | Description | | --- | --- | --- | | `name` | string | Service name. | | `playerId` | number | Server ID of the player. | ### esx_service:notifyAllInService [#esx_servicenotifyallinservice] Sends an advanced notification to everyone on duty in a service, except the sender. The sender's mugshot is used as the icon. ```lua TriggerEvent('esx_service:notifyAllInService', notification, name, sender) ``` | Name | Type | Description | | --- | --- | --- | | `notification` | table | `{ title, subject, msg, iconType }` passed to `ESX.ShowAdvancedNotification`. | | `name` | string | Service name. | | `sender` | number or nil | Server ID of the sender. | ```lua title="server/main.lua" TriggerEvent('esx_service:notifyAllInService', { title = 'Dispatch', subject = 'Backup', msg = 'Officer needs assistance at Legion Square', iconType = 1, }, 'police', source) ``` ### GlobalState count [#globalstate-count] ```lua local onDuty = GlobalState.taxi or 0 ``` The value updates on every enable, disable and disconnect. ## Server callbacks [#server-callbacks] Call these from the client with `ESX.TriggerServerCallback`, `ESX.AwaitServerCallback` or `xLib.callback`. See [Callbacks](/docs/guides/callbacks). | Callback | Arguments | Returns | Check | | --- | --- | --- | --- | | `esx_service:enableService` | `name` | `canTakeService`, `maxInService`, `inServiceCount` | Caller's job must equal `name`; count must be below the limit. | | `esx_service:disableService` | `name` | `boolean` | Caller's job must equal `name`. | | `esx_service:isInService` | `name` | `boolean` | Caller only. | | `esx_service:isPlayerInService` | `name`, `targetId` | `boolean` | Caller and target must both have job `name`. | | `esx_service:getInServiceList` | `name` | table `{ [serverId] = true }` | Caller's job must equal `name`. | ```lua title="client/main.lua" ESX.TriggerServerCallback('esx_service:enableService', function(canTakeService, maxInService, inServiceCount) if canTakeService then ESX.ShowNotification('You are now on duty') else ESX.ShowNotification(('Service full: %s/%s'):format(inServiceCount, maxInService)) end end, 'taxi') ``` ```lua title="client/main.lua" -- go off duty ESX.TriggerServerCallback('esx_service:disableService', function() end, 'taxi') -- check own duty state local onDuty = ESX.AwaitServerCallback('esx_service:isInService', 'taxi') ``` ## Client event [#client-event] `esx_service:notifyAllInService` (net, server to client) shows the notification. It is triggered by the server event above; you do not call it yourself. ## Integration tips [#integration-tips] - esx_service is separate from the `onDuty` field of the ESX job object. If you also use `xPlayer.setJob(job, grade, onDuty)`, keep both in sync yourself. - Players are removed from every service when they disconnect, but not when they change job. Call `esx_service:disableService` when you fire someone. - Duty state does not survive a restart of esx_service. ## Common issues [#common-issues] - **enableService always returns false**: the player's job name differs from the service name you pass. - **Count above the limit**: `activateService` ran after players were already on duty, or with a different `max`. Activate on resource start. - **Old scripts use `TriggerServerEvent('esx_service:disableService', ...)`**: replace it with the callback. ## Related [#related] - [esx_taxijob](/docs/addons/esx_taxijob) - [esx_mechanicjob](/docs/addons/esx_mechanicjob) - [esx_policejob](/docs/addons/esx_policejob) - [Create a job](/docs/guides/create-a-job) # esx_shops (/docs/addons/esx_shops) esx_shops is the official store resource for ESX Legacy. It places 24/7, Rob's Liquor and LTD Gasoline stores on the map and opens a Svelte-based shopping cart UI where players pick items, choose cash or bank and pay. Use it as your general store system or as a template for your own item shops. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_shops` (version `2.0.0`) | | Side | Client + server, NUI (`web/dist`) | | Dependencies | `esx_lib`, `es_extended`, `oxmysql` | | Optional | `esx_addonaccount` (tax collection), `ox_inventory` | | SQL file | None | | Config files | `shared/config/main.lua`, `shared/config/shops.lua` | How a purchase works: 1. The player walks into a marker and presses the interaction key. The client asks the server for the player's tax rate and opens the NUI. 2. On checkout the client sends the cart through the `esx_shops:purchaseItems` callback. 3. The server rate-limits the request, checks the zone, the distance to the shop, the payment method, every item name, quantity and price against `Config.Zones`, and recomputes the total. The client total must match the server total within `Config.PriceTolerance`. 4. The server checks inventory weight, deducts money and adds the items. If adding an item fails, already added items are removed and the money is refunded. 5. The tax part of the price is deposited into `Config.TaxSocietyAccount` when tax collection is on. ## Installation [#installation] <Steps> <Step> ### Add the resource [#add-the-resource] Place `esx_shops` in your resources folder. The repository ships a prebuilt UI in `web/dist`, so you only need Node.js if you change the Svelte source (`cd web && npm install && npm run build`). </Step> <Step> ### Ensure it after its dependencies [#ensure-it-after-its-dependencies] ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ensure esx_addonaccount ensure esx_shops ``` </Step> <Step> ### Prepare the tax account (optional) [#prepare-the-tax-account-optional] With `Config.EnableTaxCollection = true` the tax goes into the shared addon account named in `Config.TaxSocietyAccount` (`society_banker` by default). Create it if it does not exist: ```sql INSERT IGNORE INTO `addon_account` (`name`, `label`, `shared`) VALUES ('society_banker', 'Bank', 1); ``` </Step> </Steps> With `Config.Inventory = 'esx'` the resource registers every configured shop item via `ESX.AddItems` on startup (label, `weight` or `Config.DefaultItemWeight`). You still want proper rows in your `items` table for items you use elsewhere. ## Configuration [#configuration] ### Main config (`shared/config/main.lua`) [#main-config-sharedconfigmainlua] | Key | Default | Description | | --- | --- | --- | | `Config.Debug` | `false` | Prints validation warnings and errors to the console. | | `Config.Inventory` | `'esx'` | `'esx'`, `'ox_inventory'` (or `'ox'`) or `'auto'`. Falls back to ESX if ox_inventory is configured but not started. | | `Config.DefaultItemWeight` | `1` | Weight used when a shop item has no `weight`. | | `Config.DefaultImagePath` | `"nui://esx_shops/web/images"` | Base path for item images. Set to `nil` or `''` to disable auto paths. | | `Config.DefaultImageFormat` | `"png"` | File extension for auto-generated image paths. | | `Config.TaxRate` | `0.19` | Tax share included in every price (19 %). | | `Config.EnableTaxCollection` | `true` | Deposit the tax into the society account. `false` only shows it in the UI. | | `Config.TaxSocietyAccount` | `'society_banker'` | Shared addon account that receives tax. | | `Config.EnableTaxExemptions` | `true` | Lets jobs in `TaxExemptJobs` pay the net price. | | `Config.TaxExemptJobs` | `{ 'police', 'ambulance' }` | Job names that do not pay tax. | | `Config.DrawDistance` | `7.5` | Marker draw distance in meters. | | `Config.MarkerSize` | `{x = 1.1, y = 0.7, z = 1.1}` | Marker scale. | | `Config.MarkerType` | `29` | Marker type. | | `Config.MarkerColor` | `{r = 50, g = 200, b = 50, a = 200}` | Marker color. | | `Config.MovementThreshold` | `5.0` | Meters the player must move before nearby shops are recalculated. | | `Config.SleepNear` / `SleepMedium` / `SleepFar` | `0` / `500` / `1500` | Marker thread sleep in ms (under 50 m, 50-100 m, further). | | `Config.PurchaseCooldownMs` | `500` | Refill interval of the purchase rate limiter. | | `Config.CooldownExpiryMs` | `10000` | Idle time after which limiter entries are cleaned up. | | `Config.PurchaseRateLimitCapacity` | `3` | Token bucket size (burst of purchases allowed). | | `Config.PurchaseRateLimitRefill` | `1` | Tokens added per interval. | | `Config.PurchaseRateLimitIntervalMs` | `Config.PurchaseCooldownMs` | Interval for token refill. | | `Config.MaxQuantityPerItem` | `999` | Maximum quantity per cart line. | | `Config.MaxCartLines` | `25` | Maximum distinct items per purchase. | | `Config.PriceTolerance` | `0.001` | Allowed difference between client and server total. | ### Shop zones (`shared/config/shops.lua`) [#shop-zones-sharedconfigshopslua] `Config.Zones` ships with three zones: `TwentyFourSeven`, `RobsLiquor` and `LTDgasoline`. Each zone looks like this: ```lua title="shared/config/shops.lua" Config.Zones.Hardware = { Items = { { name = 'repairkit', label = 'Repair Kit', price = 250, category = 'tools', weight = 2 }, { name = 'bandage', label = 'Bandage', price = 40, category = 'medical' }, }, Categories = { { id = 'tools', label = 'Tools', icon = 'fa-solid fa-wrench' }, { id = 'medical', label = 'Medical', icon = 'fa-solid fa-kit-medical' }, }, Pos = { vector3(2748.0, 3473.0, 55.7), }, Size = 0.8, -- blip scale Type = 59, -- blip sprite Color = 25, -- blip color ShowBlip = true, ShowMarker = true, } ``` | Field | Type | Description | | --- | --- | --- | | `Items[].name` | `string` | Item name in your inventory. | | `Items[].label` | `string` | Name shown in the UI. | | `Items[].price` | `number` | Gross price per unit (tax included). Must be greater than 0. | | `Items[].category` | `string?` | Category `id` for filtering. | | `Items[].weight` | `number?` | Weight used when the item is auto-registered for the ESX inventory. | | `Items[].image` | `string?` | Custom image URL. Otherwise `DefaultImagePath/<name>.<DefaultImageFormat>`. | | `Items[].limit` | `number?` | Passed to the UI as a per-transaction limit. The server only enforces `Config.MaxQuantityPerItem`. | | `Categories` | `table?` | `{ id, label, icon }` entries, icons are Font Awesome classes. | | `Pos` | `vector3[]` | Every location of this shop. | | `Size`, `Type`, `Color` | `number` | Blip scale, sprite and color. | | `ShowBlip`, `ShowMarker` | `boolean` | Toggle blip and 3D marker. | ### Tax math [#tax-math] Prices include tax. For a 100 item at a `TaxRate` of `0.19`, a normal player pays 100 and 15.97 goes to the society account. A tax-exempt player pays 84.03 and no tax is collected. The UI colors follow the ESX theme convars (`esx:ui:*`) via `xLib.colors.getESXTheme`. ## Developer API [#developer-api] ### Server export `IsItemInShop` [#server-export-isiteminshop] ```lua exports.esx_shops:IsItemInShop(itemName, zone) ``` | Name | Type | Description | | --- | --- | --- | | `itemName` | `string` | Item name to look up. | | `zone` | `string` | Key in `Config.Zones`, e.g. `'TwentyFourSeven'`. | Returns `true` when the item is sold in that zone, otherwise `false`. ```lua title="server/main.lua" if exports.esx_shops:IsItemInShop('lockpick', 'TwentyFourSeven') then print('Lockpicks are sold in 24/7 stores') end ``` ### Shared exports `GetShopItems` / `GetShopCategories` [#shared-exports-getshopitems--getshopcategories] Both are available on client and server and are marked as legacy. They return the `Items` or `Categories` table of a zone, or `{}` if the zone does not exist. ```lua local items = exports.esx_shops:GetShopItems('RobsLiquor') ``` ### Client exports [#client-exports] | Export | Returns | | --- | --- | | `exports.esx_shops:IsShopOpen()` | `true` while the shop UI is open. | | `exports.esx_shops:GetCurrentShop()` | The zone name of the open shop, or `nil`. | ### Client event `esx_shops:forceOpenShop` [#client-event-esx_shopsforceopenshop] Opens the shop UI for a zone if it exists in `Config.Zones`. ```lua title="server/main.lua" TriggerClientEvent('esx_shops:forceOpenShop', playerId, 'TwentyFourSeven') ``` Purchases still require the player to stand within 5 meters of one of that zone's `Pos` entries, so this is only useful near a real shop location (for example from a target or ped script). ### Callbacks and network events [#callbacks-and-network-events] | Name | Type | Notes | | --- | --- | --- | | `esx_shops:purchaseItems` | Server callback | Used by the NUI. Arguments: `purchaseData` (`{ items, total, paymentMethod }`) and `zone`. | | `esx_shops:getTaxRate` | Server callback | Returns the player's tax rate and an optional exemption message. | | `esx_shops:legacyPurchase` | Server net event | Compatibility event: `(zone, items, paymentMethod)`. Answers with the client event `esx_shops:legacyPurchaseResponse(success, message)`. | | `esx_shops:openShop` | Net event | Legacy placeholder. It only validates the argument and does nothing else. | <Callout type="info" title="Client-triggerable"> `esx_shops:purchaseItems` and `esx_shops:legacyPurchase` can be called by any client. Both go through the same server pipeline (rate limit, distance check, server-side prices, inventory space, refund on failure), so clients cannot set prices. Keep `Config.Zones` the only source of prices and do not add your own purchase events that skip `ProcessPurchase`. </Callout> ## Common issues [#common-issues] - **"Not enough money" although the player has cash.** The payment method chosen in the UI decides between `money` and `bank`. - **Tax is not deposited.** `Config.TaxSocietyAccount` does not exist in `addon_account`, or `esx_addonaccount` is not started. Enable `Config.Debug` to see the warning. - **Item not added / inventory full.** With the ESX inventory, every item must be registered and fit the player's max weight. With ox_inventory, `CanCarryItem` must succeed. - **Images missing.** Put a PNG named after the item into `web/images` or set `image` on the item. - **Shop opens but purchases fail far from the marker.** See the distance check above. ## Related [#related] - [esx_addonaccount](/docs/addons/esx_addonaccount) - [esx_weaponshop](/docs/addons/esx_weaponshop) - [Adding items](/docs/guides/add-items) - [ox_inventory with ESX](/docs/guides/ox-inventory) # esx_society (/docs/addons/esx_society) `esx_society` turns an ESX job into a company. It links a job to a shared bank account, adds a boss menu for hiring, firing, promotions, salaries, grade labels and uniforms, and lets bosses launder black money into the company account. Job resources such as [esx_policejob](/docs/addons/esx_policejob), [esx_taxijob](/docs/addons/esx_taxijob) and [esx_mechanicjob](/docs/addons/esx_mechanicjob) register their society with it and open its boss menu. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_society` | | Side | Server (API, validation, money) and client (boss menu) | | Dependencies | `esx_lib`, `es_extended`, `cron`, `esx_addonaccount` | | Uses at runtime | `oxmysql`, `esx_datastore` (society garage), `skinchanger` (uniform saving) | | SQL | `esx_society.sql` (`society_moneywash` table) | A society is a plain record kept in memory: | Field | Type | Description | | --- | --- | --- | | `name` | string | Job name, for example `taxi`. Must match `jobs.name`. | | `label` | string | Display name. | | `account` | string | Shared `esx_addonaccount` account, usually `society_<job>`. | | `datastore` | string | Shared `esx_datastore` store, usually `society_<job>`. Used for the society garage. | | `inventory` | string | Shared `esx_addoninventory` inventory, usually `society_<job>`. | | `data` | table | Free-form options. The official jobs pass `{ type = 'public' }` or `{ type = 'private' }`. esx_society stores this table but does not read it. | Societies are not saved in the database. Every job resource must register its society again on every start. ## Installation [#installation] <Steps> <Step> ### Install the dependencies [#install-the-dependencies] Make sure `oxmysql`, `es_extended`, `esx_lib`, `cron` and [esx_addonaccount](/docs/addons/esx_addonaccount) are installed. Install [esx_datastore](/docs/addons/esx_datastore) and [esx_addoninventory](/docs/addons/esx_addoninventory) too, because the official job resources create society datastores and inventories. </Step> <Step> ### Import the SQL [#import-the-sql] Import `esx_society.sql`. It creates the `society_moneywash` table. On start, esx_society also creates `society_moneywash_operations` and an index `esx_society_employees` on `users (job, job_grade, identifier)` by itself. ```sql title="esx_society.sql" CREATE TABLE `society_moneywash` ( `id` int NOT NULL AUTO_INCREMENT, `identifier` varchar(60) NOT NULL, `society` varchar(60) NOT NULL, `amount` int NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ``` </Step> <Step> ### Start it before every job resource [#start-it-before-every-job-resource] ```bash title="server.cfg" ensure cron ensure esx_addonaccount ensure esx_datastore ensure esx_addoninventory ensure esx_society # job resources after this line ensure esx_policejob ensure esx_taxijob ``` </Step> </Steps> <Callout type="warn" title="InnoDB required for money washing"> On start the money wash module checks that `users`, `society_moneywash` and `addon_account_data` use the InnoDB engine. If one does not, it prints `[esx_society] Moneywash disabled: ...` and every wash request fails with "invalid amount". The rest of the resource keeps working. </Callout> ## Configuration [#configuration] All keys live in `config.lua`. | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language of the menus and notifications. | | `Config.EnableESXIdentity` | `true` | Show `firstname lastname` from the `users` table in the employee list. Set to `false` if you do not use [esx_identity](/docs/core/esx_identity). | | `Config.MaxSalary` | `3500` | Highest salary a boss can set for a grade. Must be a whole number from `0` up to this value. | | `Config.MaxJobGradeLabelLength` | `40` | Grade labels set by bosses are trimmed to this length. | | `Config.JobGradeUpdateCooldown` | `1500` | Minimum delay in ms between two salary or label changes by the same boss for the same job. `0` disables the limit. | | `Config.SocietyGarageDistance` | `12.0` | Max distance in meters from a garage zone for the society garage events. | | `Config.SocietyGarageZones` | `mechanic` and `taxi` zones | Per-society list of `vector3` points where the society garage may be used. | | `Config.BossGrades` | `{ ['boss'] = true }` | Grade **names** (`job_grades.name`) that get boss access. Add more names to give access to several grades. | | `Config.EnableUniformManagement` | `false` | Show uniform management in every boss menu. You can also enable it per job with the `uniforms` option. | | `Config.UniformSaveCooldown` | `30000` | Minimum delay in ms between two uniform saves for the same job. | | `Config.UniformDrawableMax` | `2000` | Highest accepted drawable ID for clothing components. Keep it high for custom clothing packs. | | `Config.UniformTextureMax` | `255` | Highest accepted texture ID (components ending in `_2`). | | `Config.UniformPropMax` | `255` | Highest accepted prop ID for `helmet_1`, `glasses_1`, `watches_1`, `bracelets_1` and `ears_1`. Props may also be `-1` (none). | | `Config.UniformComponentLimits` | `{}` | Per-component overrides, for example `helmet_1 = { min = -1, max = 250 }`. | | `Config.UniformComponents` | 30 skin keys | The skinchanger components that make up a uniform: `tshirt_1` … `ears_2`. A saved uniform must contain every one of them. | ```lua title="config.lua" Config.BossGrades = { ['boss'] = true, ['chef'] = true, -- give a second grade access to the boss menu } ``` ## How it works [#how-it-works] - **Boss check**: a player has boss access to a society when `xPlayer.getJob().name == society.name` and the grade name is in `Config.BossGrades`. Every server event and callback repeats this check. The client menu is only a view. - **Account**: balance, deposits and withdrawals go through the shared [esx_addonaccount](/docs/addons/esx_addonaccount) account named in `society.account`. - **Salaries and labels** are written to `job_grades` and then reloaded with `ESX.RefreshJob(job)` (or `ESX.RefreshJobs()` as fallback), so changes apply without a restart. - **Employees** are listed in pages of 50: online players first, then offline players from the `users` table. - **Money washing** is queued in `society_moneywash_operations` and paid by a [cron](/docs/core/cron) task every day at 03:00 (`TriggerEvent('cron:runAt', 3, 0, WashMoneyCRON)`). - **Society payouts**: when es_extended's `Config.EnableSocietyPayouts` is `true`, the paycheck module calls `esx_society:getSociety` and pays salaries from the society account. See [Money and accounts](/docs/guides/money-and-accounts). ## Server API [#server-api] ### registerSociety (export and event) [#registersociety-export-and-event] Registers a job as a society. Call it once when your job resource starts. ```lua exports.esx_society:registerSociety(name, label, account, datastore, inventory, data) TriggerEvent('esx_society:registerSociety', name, label, account, datastore, inventory, data) ``` | Name | Type | Description | | --- | --- | --- | | `name` | string | Job name. | | `label` | string | Display label. | | `account` | string | Shared addon account name. | | `datastore` | string | Shared datastore name. | | `inventory` | string | Shared addon inventory name. | | `data` | table | Free options, for example `{ type = 'private' }`. | Returns nothing. Registering the same name twice prints a warning and keeps the first entry. ```lua title="server/main.lua" exports.esx_society:registerSociety('taxi', 'Taxi', 'society_taxi', 'society_taxi', 'society_taxi', { type = 'public' }) ``` The event is a local server event (`AddEventHandler`). Clients cannot trigger it. ### GetSociety (export) [#getsociety-export] ```lua local society = exports.esx_society:GetSociety(name) ``` | Name | Type | Description | | --- | --- | --- | | `name` | string | Society (job) name. | Returns the society table or `nil`. ```lua title="server/main.lua" local society = exports.esx_society:GetSociety('police') if society then TriggerEvent('esx_addonaccount:getSharedAccount', society.account, function(account) print(('%s has $%s'):format(society.label, account.money)) end) end ``` ### esx_society:getSociety (event) [#esx_societygetsociety-event] Callback style lookup, used by es_extended's paycheck module. ```lua TriggerEvent('esx_society:getSociety', name, cb) ``` | Name | Type | Description | | --- | --- | --- | | `name` | string | Society name. | | `cb` | function | Receives the society table or `nil`. | ### esx_society:getSocieties (event) [#esx_societygetsocieties-event] ```lua TriggerEvent('esx_society:getSocieties', function(societies) for _, society in ipairs(societies) do print(society.name, society.account) end end) ``` Returns the array of all registered societies. ## Client API [#client-api] ### esx_society:openBossMenu [#esx_societyopenbossmenu] Opens the boss menu (ESX context menu). Use it from a marker or target zone in your job resource. ```lua TriggerEvent('esx_society:openBossMenu', society, close, options) ``` | Name | Type | Description | | --- | --- | --- | | `society` | string | Society name. Must equal the player's job name. | | `close` | function or nil | Passed to `ESX.OpenContext` as the close handler of the main menu. Called when the player closes it. | | `options` | table or nil | Menu entries to show. Missing keys use the defaults below. | | Option | Default | Entry | | --- | --- | --- | | `checkBal` | `true` | Check society balance | | `withdraw` | `true` | Withdraw money | | `deposit` | `true` | Deposit money | | `wash` | `true` | Wash black money | | `employees` | `true` | Employee list and recruiting | | `salary` | `true` | Salary per grade | | `grades` | `true` | Rename grade labels | | `uniforms` | `Config.EnableUniformManagement` | Save the worn outfit as a grade uniform | ```lua title="client/main.lua" TriggerEvent('esx_society:openBossMenu', 'taxi', function() ESX.CloseContext() end, { wash = false, uniforms = true }) ``` The menu first asks the server through the `esx_society:isBoss` callback. Non-bosses see nothing. ## Network events (client to server) [#network-events-client-to-server] These are `RegisterServerEvent` handlers, so any client can send them. All of them validate on the server. <Callout type="info" title="Security"> Each event below is reachable from a modified client. The checks listed are what protects you. If you fork esx_society, keep them. </Callout> | Event | Parameters | Server checks | | --- | --- | --- | | `esx_society:checkSocietyBalance` | `society` | Boss access to that society. Shows the balance as a notification. | | `esx_society:withdrawMoney` | `society`, `amount` | Society exists, amount is a positive number (rounded), boss access, account has enough money. | | `esx_society:depositMoney` | `society`, `amount` | Society exists, positive amount, player's **job** equals the society (any grade), player has the cash. | | `esx_society:washMoney` | `society`, `amount` | Positive amount, boss access, enough `black_money`, wash module ready. | | `esx_society:putVehicleInGarage` | `society`, `vehicle` | Same job, within `Config.SocietyGarageDistance` of a `Config.SocietyGarageZones` point, valid plate, the player is in or next to (10 m) a vehicle with that plate and model. Deletes the entity and stores it in the society datastore key `garage`. | | `esx_society:removeVehicleFromGarage` | `society`, `vehicle` | Same job and garage zone checks, valid plate. Removes the entry from `garage`. | ```lua title="client/main.lua" TriggerServerEvent('esx_society:depositMoney', 'taxi', 500) ``` <Callout type="warn"> `depositMoney` only requires the job, not a boss grade. Every employee can pay into the company account, even though the boss menu shows the button only to bosses. </Callout> ## Server callbacks [#server-callbacks] Registered with `xLib.callback.registerCompat`. Call them with `ESX.TriggerServerCallback`, `ESX.AwaitServerCallback` or `xLib.callback`. See [Callbacks](/docs/guides/callbacks). | Callback | Arguments | Returns | Access | | --- | --- | --- | --- | | `esx_society:isBoss` | `job` | `boolean` | Caller's own job and boss grade. | | `esx_society:getSocietyMoney` | `society` | `number` (0 when denied) | Boss of that society. | | `esx_society:getEmployees` | `society`, `cursor?` | `employees`, `nextCursor` | Boss of that society. One request per player at a time. | | `esx_society:getOnlinePlayers` | `society` | array of `{ source, identifier, name, job = { name, grade, grade_name } }` | Boss of that society. Cached for 1 second. | | `esx_society:getJob` | `job` | job table with `grades` sorted by grade, or `false` | No check. Returns public job data. | | `esx_society:setJob` | `identifier`, `job`, `grade`, `actionType` | `boolean` | Boss. See below. | | `esx_society:setJobSalary` | `job`, `grade`, `salary` | `boolean` | Boss of that job, valid grade, salary 0 to `Config.MaxSalary`, cooldown. | | `esx_society:setJobLabel` | `job`, `grade`, `label` | `boolean` | Boss of that job, valid grade, label trimmed to `Config.MaxJobGradeLabelLength`, cooldown. | | `esx_society:setJobUniform` | `job`, `grade`, `skin` | `boolean` | Boss of that job, complete and in-range skin, sex matches identity, `Config.UniformSaveCooldown`. `grade = -1` saves for all grades. | | `esx_society:getVehiclesInGarage` | `society` | array of stored vehicles | Same job, inside a garage zone. | ### esx_society:setJob rules [#esx_societysetjob-rules] `actionType` must be `'hire'`, `'promote'` or `'fire'`. - **hire / promote**: `job` must be the boss's own job and `grade` must exist and be lower than or equal to the boss's grade. Hiring needs the target online. - **fire**: `job` must be `'unemployed'` and `grade` must be `0`. - Offline targets (promote or fire) are updated in the `users` table only if they currently work for the boss's job. ```lua title="client/main.lua" local ok = ESX.AwaitServerCallback('esx_society:setJob', targetIdentifier, 'taxi', 2, 'promote') if ok then ESX.ShowNotification('Promoted') end ``` ### Reading the balance in a HUD [#reading-the-balance-in-a-hud] ```lua title="client/main.lua" local job = ESX.PlayerData.job ESX.TriggerServerCallback('esx_society:getSocietyMoney', function(money) print(('Company balance: $%s'):format(money)) end, job.name) ``` ## Money washing in detail [#money-washing-in-detail] 1. The boss enters an amount. The server checks boss access and the player's `black_money`. 2. Inside `WithPlayerPersistence`, the amount is removed from `black_money` and a row is written to `society_moneywash_operations` with state `queued` in one transaction. If that fails, the money is refunded. 3. Every day at 03:00 the cron task adds each queued amount to the society account and marks it `paid`. There is no washing fee: 1,000 black money becomes 1,000 in the society account. Old rows in `society_moneywash` are migrated into the new table on start. ## Integration tips [#integration-tips] - Create the SQL rows first. A society needs `society_<job>` in `addon_account` (shared = 1). The official job SQL files also add the same name to `datastore` and `addon_inventory`. - Register the society at the top of your server file, after `esx_society` started. With `ensure` order this is guaranteed. - To give a second grade boss rights, add its `job_grades.name` to `Config.BossGrades`. The check uses the name, not the grade number. - Uniforms are stored in `job_grades.skin_male` and `skin_female` and are read by job scripts that dress players from the grade skin. ## Common issues [#common-issues] - **Menu shows nothing**: the grade name is not in `Config.BossGrades`, or the job name passed to `openBossMenu` differs from the player's job. The server log shows `esx_society: <identifier> attempted open a society boss menu!`. - **"Society has no shared account"** in the console: the `addon_account` row for `society_<job>` is missing or not marked `shared = 1`. - **Salary change does nothing**: the value is above `Config.MaxSalary`, not a whole number, equal to the current value, or sent within `Config.JobGradeUpdateCooldown`. - **Washed money never arrives**: the `cron` resource is not running, the server was offline at 03:00, or the wash module was disabled because a table is not InnoDB. Queued entries stay queued and are paid on the next run. - **Employee list empty for offline players**: with `Config.EnableESXIdentity = true` the query selects `firstname` and `lastname`. Without esx_identity those columns may not exist; set the option to `false`. ## Related [#related] - [esx_addonaccount](/docs/addons/esx_addonaccount) - [esx_datastore](/docs/addons/esx_datastore) - [Create a job](/docs/guides/create-a-job) - [Money and accounts](/docs/guides/money-and-accounts) - [Addons overview](/docs/addons) # esx_status (/docs/addons/esx_status) `esx_status` is the engine behind player needs in ESX Legacy. It keeps a list of named values per player (for example `hunger`, `thirst`, `drunk`), saves them in `users.status`, decays them over time and broadcasts them to the client every tick. [esx_basicneeds](/docs/addons/esx_basicneeds) and [esx_optionalneeds](/docs/addons/esx_optionalneeds) build on it, and HUDs such as [esx_hud](/docs/addons/esx_hud) read from it. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_status` | | Side | Server (authoritative values, saving) and client (status objects, ticks, optional bars) | | Dependencies | `esx_lib`, `es_extended` (`oxmysql` is loaded by the manifest) | | SQL | `esx_status.sql` adds `users.status` | | UI | Optional NUI bars (`html/ui.html`), off by default | ### Two kinds of statuses [#two-kinds-of-statuses] | Kind | Examples | Who owns the value | | --- | --- | --- | | **Authoritative** | Statuses listed in `Config.AuthoritativeStatuses` (default `hunger`, `thirst`) | The server. It decays them and pushes the result to the client with `esx_status:sync`. | | **Custom** | Everything else, for example `drunk` | The client. It ticks them and sends them to the server every `Config.UpdateInterval`. | Since version 1.1 hunger and thirst are decided on the server. Change them with the server events. Client-side changes only last until the next sync. ## Installation [#installation] <Steps> <Step> ### Import the SQL [#import-the-sql] ```sql title="esx_status.sql" ALTER TABLE `users` ADD COLUMN `status` LONGTEXT NULL; ``` </Step> <Step> ### Start it before resources that register statuses [#start-it-before-resources-that-register-statuses] ```bash title="server.cfg" ensure esx_status ensure esx_basicneeds ensure esx_optionalneeds ``` </Step> </Steps> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.StatusMax` | `1000000` | Value that equals 100 percent. Values are clamped between `0` and this number on the server. | | `Config.TickTime` | `1000` | Client tick length in ms. Also the time unit for `decay` in `AuthoritativeStatuses`. | | `Config.UpdateInterval` | `30000` | How often (ms) the server syncs authoritative statuses and the client sends custom statuses. | | `Config.SyncSlices` | `30` | The server spreads one sync round over this many batches to avoid spikes. | | `Config.MinUpdateInterval` | `10000` | Minimum time (ms) between two accepted `esx_status:update` messages from one client. | | `Config.LoadRetryInitial` | `5000` | First retry delay (ms) when loading statuses fails. Doubles on each failure. | | `Config.LoadRetryMax` | `60000` | Upper limit for the retry delay. | | `Config.Display` | `false` | Show the built-in esx_status bars. Keep `false` when you use another HUD. | | `Config.AuthoritativeStatuses` | `hunger = { default = 1000000, decay = 100 }`, `thirst = { default = 1000000, decay = 75 }` | Statuses the server owns. `default` is the start value for new players, `decay` the amount removed per `Config.TickTime`. | With the defaults, hunger drops from full to zero in about 2 hours 47 minutes (1,000,000 / 100 seconds) and thirst in about 3 hours 42 minutes. ```lua title="config.lua" Config.AuthoritativeStatuses = { hunger = { default = 1000000, decay = 100 }, thirst = { default = 1000000, decay = 75 }, stress = { default = 0, decay = 0 }, -- server-owned, changed only by your scripts } ``` ## Status data shape [#status-data-shape] Every status entry sent between server and client looks like this: | Field | Type | Description | | --- | --- | --- | | `name` | string | Unique name, 1 to 64 characters. | | `val` | number | Current value, `0` to `Config.StatusMax`. | | `percent` | number | `val / Config.StatusMax * 100`. | A player can hold at most 64 statuses. The loaded list is also mirrored to `ESX.PlayerData.variables.status` on the client. ## Server API [#server-api] All server events below are local (`AddEventHandler`). Only other server scripts can trigger them. ### esx_status:set [#esx_statusset] Sets a status to an exact value. ```lua TriggerEvent('esx_status:set', playerId, name, value) ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | number | Server ID of the player. | | `name` | string | Status name. | | `value` | number | New value. Clamped to `0` … `Config.StatusMax`. | ```lua title="server/main.lua" -- fill hunger and thirst after a revive TriggerEvent('esx_status:set', source, 'hunger', 1000000) TriggerEvent('esx_status:set', source, 'thirst', 1000000) ``` ### esx_status:add [#esx_statusadd] Adds to a status. Use a negative value to remove. ```lua TriggerEvent('esx_status:add', playerId, name, value) ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | number | Server ID of the player. | | `name` | string | Status name. | | `value` | number | Amount to add (negative to subtract). The result is clamped. | ```lua title="server/main.lua" ESX.RegisterUsableItem('sandwich', function(source) local xPlayer = ESX.Player(source) xPlayer.removeInventoryItem('sandwich', 1) TriggerEvent('esx_status:add', source, 'hunger', 250000) end) ``` `set` and `add` only change a status the player already has. After the change the server sends `esx_status:sync` to the client right away. ### esx_status:getStatus [#esx_statusgetstatus] Reads one status of a player. ```lua TriggerEvent('esx_status:getStatus', playerId, name, cb) ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | number | Server ID. | | `name` | string | Status name. | | `cb` | function | Receives `{ name, val, percent }`. | ```lua title="server/main.lua" TriggerEvent('esx_status:getStatus', source, 'thirst', function(status) if status.percent < 10 then TriggerClientEvent('esx:showNotification', source, 'You are very thirsty') end end) ``` <Callout type="warn"> The callback is not called when the player is not loaded or does not have that status. Do not wait on it forever. </Callout> ## Client API [#client-api] ### esx_status:registerStatus [#esx_statusregisterstatus] Creates a status object on the client. Register custom statuses in a handler for `esx_status:loaded` so the saved value is applied. ```lua TriggerEvent('esx_status:registerStatus', name, default, color, visible, tickCallback) ``` | Name | Type | Description | | --- | --- | --- | | `name` | string | Status name. | | `default` | number | Start value when nothing is saved. | | `color` | string | CSS color for the built-in bar, for example `'#CFAD0F'`. | | `visible` | function | `function(status) return boolean end`. Decides if the bar is shown. Called when `Config.Display` is on, so always pass a function. | | `tickCallback` | function | `function(status) end`, called every `Config.TickTime`. Usually `status.remove(x)`. | ```lua title="client/main.lua" AddEventHandler('esx_status:loaded', function() TriggerEvent('esx_status:registerStatus', 'stress', 0, '#E74C3C', function(status) return status.val > 0 end, function(status) status.remove(500) end ) end) ``` If a status with the same name exists, the new object replaces it and keeps the current value. ### Status object [#status-object] `esx_status:getStatus` and `esx_status:getAllStatus` return status objects with these members: | Member | Description | | --- | --- | | `name`, `val`, `default`, `color` | Fields. | | `set(val)` | Set the value (no clamping). | | `add(val)` | Add, capped at `Config.StatusMax`. | | `remove(val)` | Subtract, floored at `0`. | | `getPercent()` | Value as percent. | | `onTick()` | Runs the tick callback. | | `_set(key, value)`, `_get(key)` | Raw field access. | ### esx_status:unregisterStatus [#esx_statusunregisterstatus] ```lua TriggerEvent('esx_status:unregisterStatus', name) ``` Removes the status from the client list. It stops ticking and is no longer sent to the server. ### esx_status:getStatus (client) [#esx_statusgetstatus-client] ```lua TriggerEvent('esx_status:getStatus', name, cb) ``` ```lua title="client/main.lua" TriggerEvent('esx_status:getStatus', 'hunger', function(status) print(('Hunger: %.1f%%'):format(status.getPercent())) end) ``` ### esx_status:getAllStatus [#esx_statusgetallstatus] ```lua TriggerEvent('esx_status:getAllStatus', function(statuses) for i = 1, #statuses do print(statuses[i].name, statuses[i].val) end end) ``` ### esx_status:set / add / remove (client) [#esx_statusset--add--remove-client] ```lua TriggerEvent('esx_status:set', name, val) TriggerEvent('esx_status:add', name, val) TriggerEvent('esx_status:remove', name, val) ``` These change the local status object. They are also net events, so the server can send them with `TriggerClientEvent('esx_status:add', playerId, 'drunk', 250000)`. For custom statuses the new value reaches the server with the next update. For authoritative statuses (hunger, thirst) the change is overwritten by the next server sync, so use the server events instead. ### esx_status:setDisplay [#esx_statussetdisplay] ```lua TriggerEvent('esx_status:setDisplay', 0.5) ``` Sets the opacity of the built-in bars (`0.0` hides them). Only useful with `Config.Display = true`. ## Events you can listen to [#events-you-can-listen-to] | Event | Side | Payload | When | | --- | --- | --- | --- | | `esx_status:loaded` | client | none | Saved statuses arrived from the server. Register custom statuses here. | | `esx_status:onTick` | client | array of `{ name, val, percent }` | Every `Config.TickTime` after loading. Ideal for HUDs and effects. | | `esx_status:load` | client (from server) | full status list | Sent by the server after `esx:playerLoaded` or on request. Internal. | | `esx_status:sync` | client (from server) | changed entries | Server pushes authoritative values. Internal. | ```lua title="client/hud.lua" AddEventHandler('esx_status:onTick', function(statuses) for _, status in ipairs(statuses) do if status.name == 'thirst' then SendNUIMessage({ action = 'thirst', value = status.percent }) end end end) ``` ## Network events (client to server) [#network-events-client-to-server] | Event | Payload | Protection | | --- | --- | --- | | `esx_status:requestLoad` | none | Ignored while loading; rate limited with the load retry delay. | | `esx_status:update` | array of `{ name, val }` | Accepted at most every `Config.MinUpdateInterval`. Entries are validated (max 64, names 1-64 chars, finite numbers, clamped). Authoritative statuses in the payload are ignored. | <Callout type="warn" title="Custom statuses are client-controlled"> A modified client can send any value for custom statuses. Do not base money, items or permissions on them. If a status matters, add it to `Config.AuthoritativeStatuses` and change it only through the server events. </Callout> ## Saving [#saving] The server loads `users.status` on `esx:playerLoaded` and writes it back on `esx:playerDropped`. During the session values live in the xPlayer variable `status`. Authoritative values are advanced from elapsed time whenever they are read, synced or saved. ## Common issues [#common-issues] - **Hunger or thirst jumps back after eating**: your item script changes them on the client. Use `TriggerEvent('esx_status:add', source, 'hunger', amount)` on the server. - **Status bars do not show**: `Config.Display` is `false` by default. Enable it or use a HUD. - **Custom status always starts at the default**: it was registered before `esx_status:loaded` fired and then never received the saved value, or the player never stayed long enough for an update. Register inside `esx_status:loaded`. - **`users.status` column missing**: import `esx_status.sql`. Loading fails and is retried with backoff. - **Changing an unknown status on the server does nothing**: `set` and `add` only touch statuses that already exist for the player. ## Related [#related] - [esx_basicneeds](/docs/addons/esx_basicneeds) - [esx_optionalneeds](/docs/addons/esx_optionalneeds) - [esx_hud](/docs/addons/esx_hud) - [Add items](/docs/guides/add-items) # esx_taxijob (/docs/addons/esx_taxijob) esx_taxijob adds a `taxi` job: drivers spawn a cab at the depot, bill player passengers from an F6 menu, and run NPC fares that pick up a pedestrian and drive them to a random destination for a payout. It is the simplest official job addon and a good template for your own delivery-style jobs. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_taxijob` (version `1.0`) | | Side | client + server | | Job name | `taxi` (grades `recrue`, `novice`, `experimente`, `uber`, `boss`) | | Society | `society_taxi`, registered as a public society | | Dependencies (fxmanifest) | `esx_lib`, `es_extended` | | Also required at runtime | `oxmysql`, [esx_society](/docs/addons/esx_society), [esx_addonaccount](/docs/addons/esx_addonaccount), [esx_addoninventory](/docs/addons/esx_addoninventory), [esx_datastore](/docs/addons/esx_datastore), [esx_billing](/docs/addons/esx_billing) | | Optional | [esx_service](/docs/addons/esx_service) when `Config.MaxInService` is not `-1` | | OneSync | Required. Vehicles are spawned server-side with `ESX.OneSync.SpawnVehicle`. | ### Depot markers [#depot-markers] | Zone | Purpose | | --- | --- | | `TaxiActions` | Stock deposit and withdraw, and the boss menu for the `boss` grade. | | `Cloakroom` | Switch between work and civilian clothes (from `job_grades` skins). | | `VehicleSpawner` | Take out a cab. | | `VehicleSpawnPoint` | Where the cab appears. | | `VehicleDeleter` | Return the cab. | ## Installation [#installation] <Steps> <Step> ### Import the SQL [#import-the-sql] Run `esx_taxijob.sql`. It creates the `society_taxi` account, datastore and inventory, and the `taxi` job with five grades and uniforms. Skip this if your database came from the ESX txAdmin recipe, which already contains these rows. </Step> <Step> ### Start it [#start-it] ```bash title="server.cfg" ensure esx_society ensure esx_billing ensure esx_taxijob ``` </Step> </Steps> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.DrawDistance` | `10.0` | Distance at which markers are drawn. | | `Config.NPCJobEarnings` | `{ min = 300, max = 600 }` | Random total per NPC fare. Doubled for grade 3 and higher. | | `Config.MinimumDistance` | `3000` | Minimum distance between pickup and drop-off, in GTA units. The server rejects shorter missions. | | `Config.MaxInService` | `-1` | Service limit through esx_service. `-1` disables it. | | `Config.EnablePlayerManagement` | `true` | Enables the boss menu and the F6 mobile menu. | | `Config.EnableSocietyOwnedVehicles` | `false` | Take cabs from the esx_society vehicle garage instead of spawning `Config.AuthorizedVehicles`. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language. | | `Config.OxInventory` | `ESX.GetConfig().OxInventory` | Read from es_extended. | | `Config.AuthorizedVehicles` | `{ { model = 'taxi', title = 'Taxi', icon = 'fas fa-car' } }` | Cabs offered at the spawner. Grades below 3 must drive one of these for NPC fares. | | `Config.Zones` | see table above | `Pos`, `Size`, `Color`, `Type` and `Rotate` per marker. `VehicleSpawnPoint` also has a `Heading`. | | `Config.JobLocations` | list of `vector3` | Possible NPC drop-off points. The server only accepts destinations within 5 units of one of them. | ```lua title="config.lua" -- Offer a second cab model Config.AuthorizedVehicles = { { model = 'taxi', title = 'Taxi', icon = 'fas fa-car' }, { model = 'stretch', title = 'Limousine', icon = 'fas fa-car' }, } ``` ## How NPC fares work [#how-npc-fares-work] 1. Open F6 and choose **Start NPC job** while driving an authorized cab (or any car at grade 3+). 2. The client finds a pedestrian, and when they get in, sends `esx_taxijob:startMission` with the pickup and destination. 3. The server stores the mission only if the destination is one of `Config.JobLocations`, the player is within 80 units of the pickup, and the trip is at least `Config.MinimumDistance` long. 4. At the destination the client sends `esx_taxijob:success`. The server pays only if the player is within 18 units of the stored drop-off, still in an authorized vehicle, and at least 10 seconds have passed since the last payout. With the `society_taxi` account available the payout is split: 30% to the driver as cash and 70% to the society. Without it, the driver gets the full amount. ## Commands and keybinds [#commands-and-keybinds] | Command | Default key | Description | | --- | --- | --- | | `taximenu` | `F6` | Opens the mobile taxi menu: bill the closest player, start or stop NPC fares. | Depot markers use the `E` key (control 38). ## Server events [#server-events] | Event | Arguments | Server checks | | --- | --- | --- | | `esx_taxijob:startMission` | `pickupCoords, dropoffCoords` | Taxi job, authorized vehicle, valid drop-off, distance rules. | | `esx_taxijob:success` | none | Active mission, near drop-off, authorized vehicle, 10 second cooldown. | | `esx_taxijob:getStockItem` | `itemName, count` | Taxi job, within 8 units of `TaxiActions`. | | `esx_taxijob:putStockItems` | `itemName, count` | Taxi job, within 8 units of `TaxiActions`. | `esx_taxijob:success` pays money, so it is the event an exploiter would spam. The mission state lives on the server, and the payout requires a stored mission, so calling it without a real fare only logs a warning. ## Server callbacks [#server-callbacks] | Callback | Arguments | Returns | | --- | --- | --- | | `esx_taxijob:SpawnVehicle` | `model, props` | `true` if a cab was spawned. Requires the taxi job, the player within 15 units of `VehicleSpawner`, and a clear spawn point. | | `esx_taxijob:getStockItems` | none | Items in `society_taxi`. | | `esx_taxijob:getPlayerInventory` | none | `{ items = ... }` of the caller. | ## Integration tips [#integration-tips] - Player fares from the F6 menu are sent through `esx_billing:sendBill` to `society_taxi`, so the society receives the money once the passenger pays. - To adjust the 30/70 split, edit the `esx_taxijob:success` handler in `server/main.lua`. There is no config key for it. - Add more drop-off points by appending `vector3` positions to `Config.JobLocations`. Keep them on roads so the AI passenger can walk away. ## Common issues [#common-issues] - **Cab does not spawn.** The spawn point is blocked by another vehicle, OneSync is off, or the player is not near the spawner. - **"Must be in a taxi" for a car that looks like a taxi.** Only exact models from `Config.AuthorizedVehicles` count for grades 0 to 2. - **Fare never pays.** The destination must be within 18 units when you arrive. Stopping far from the marker or switching vehicles cancels the payout. ## Related [#related] - [esx_society](/docs/addons/esx_society) - [esx_billing](/docs/addons/esx_billing) - [esx_service](/docs/addons/esx_service) - [Create a job](/docs/guides/create-a-job) # esx_vehicleshop (/docs/addons/esx_vehicleshop) esx_vehicleshop is the official car dealership for ESX Legacy. It turns Premium Deluxe Motorsport into a showroom where players buy vehicles that are saved to `owned_vehicles`, and optionally into a player-run `cardealer` job that orders stock, sells or rents cars to other players, and gets paid daily rent. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_vehicleshop` (version `1.0`) | | Side | Client + server | | Dependencies | `esx_lib`, `es_extended`, `oxmysql` | | Optional | `esx_society`, `esx_addonaccount`, `esx_addoninventory` (job mode), `esx_license` (license check), `cron` (rent) | | SQL file | `esx_vehicleshop.sql` | | Client export | `GeneratePlate` | Two modes, controlled by `Config.EnablePlayerManagement`: - **Direct sales (`false`, default).** Anyone can enter the showroom, browse `vehicles` by category and buy with **cash**. The vehicle spawns outside, the player is warped in, and a row is added to `owned_vehicles` with a server-generated plate. - **Car dealer job (`true`).** Players with the `cardealer` job buy stock with society money into `cardealer_vehicles`, then sell or rent it to nearby players, return it to the provider for 75 % of the price, and manage the society stash through the boss actions marker. The society `cardealer` is registered with [esx_society](/docs/addons/esx_society). Reselling at the `ResellVehicle` marker pays `Config.ResellPercentage` of the catalog price and deletes the vehicle from `owned_vehicles`. ## Installation [#installation] <Steps> <Step> ### Import the SQL [#import-the-sql] `esx_vehicleshop.sql` creates the tables `cardealer_vehicles`, `vehicle_sold`, `owned_vehicles`, `rented_vehicles`, `vehicle_categories` and `vehicles` (with the default catalog), plus the `cardealer` job, its grades and the `society_cardealer` addon account and inventory. If `owned_vehicles` already exists (for example from another garage script), remove that `CREATE TABLE` from the file before importing. All of `cardealer_vehicles`, `owned_vehicles`, `rented_vehicles` and `vehicle_sold` must use InnoDB, otherwise dealer transactions are disabled at startup. </Step> <Step> ### Ensure the resource [#ensure-the-resource] ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ensure cron ensure esx_addonaccount ensure esx_addoninventory ensure esx_society ensure esx_license ensure esx_vehicleshop ``` Only the first three are required in direct sales mode. </Step> </Steps> On first start the resource also creates `vehicle_rental_payments` (daily rent receipts), converts a legacy `varchar` `vehicle_sold.date` to `DATETIME` when all values are valid, and adds the `idx_vehicle_sold_date_plate` index. ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.DrawDistance` | `10` | Marker draw distance. | | `Config.MarkerColor` | `{r = 120, g = 120, b = 240}` | Marker color. | | `Config.EnablePlayerManagement` | `false` | Enables the `cardealer` job mode. Needs esx_addonaccount, esx_billing and esx_society. | | `Config.ResellPercentage` | `50` | Percent of the catalog price paid when reselling. | | `Config.RentalPriceMultiplier` | `0.05` | Daily rent = base price × multiplier. | | `Config.MinimumRentalPrice` | `1` | Lowest daily rent. | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale. | | `Config.LicenseEnable` | `false` | Require the `drive` license before opening the shop menu (direct mode, client-side check). | | `Config.PlateLetters` | `3` | Letters in generated plates. | | `Config.PlateNumbers` | `3` | Digits in generated plates. | | `Config.PlateUseSpace` | `true` | Space between letters and digits. Letters + numbers + space must not exceed 8. | | `Config.OxInventory` | `ESX.GetConfig().OxInventory` | Uses an ox_inventory stash for the society stock. | | `Config.Blip` | `{ show = true, Sprite = 326, Display = 4, Scale = 0.8 }` | Dealership blip. | ### Zones [#zones] `Config.Zones` defines six points. `Type = -1` hides the marker. | Zone | Default position | Purpose | | --- | --- | --- | | `ShopEntering` | `vector3(-33.7, -1102.0, 25.4)` | Opens the shop / dealer menu. | | `ShopInside` | `vector3(-47.5, -1097.2, 25.4)` | Preview spot. Server checks for sales use 30 m around it. | | `ShopOutside` | `vector3(-28.6, -1085.6, 25.5)` | Spawn point of bought vehicles. | | `BossActions` | `vector3(-32.0, -1114.2, 25.4)` | Boss menu and stock (job mode). | | `GiveBackVehicle` | `vector3(-18.2, -1078.5, 25.6)` | Return a rented vehicle. | | `ResellVehicle` | `vector3(-44.6, -1080.7, 25.6)` | Sell an owned vehicle back. | ### Catalog tables [#catalog-tables] ```sql INSERT INTO `vehicle_categories` (`name`, `label`) VALUES ('electric', 'Electric'); INSERT INTO `vehicles` (`name`, `model`, `price`, `category`) VALUES ('Neon', 'neon', 150000, 'electric'); ``` Vehicles whose `category` has no matching row in `vehicle_categories` are not shown (the query uses an inner join). ## Rentals [#rentals] In job mode a dealer can rent a stock car to a player. The rent is computed on the server (`getRentalPrice`). Every day at 22:00 the `cron` resource triggers `PayRent()`: rent is taken from each renter's **bank** and added to `society_cardealer`. If a renter cannot pay, the rental is removed ("evicted"). Each charge is written to `vehicle_rental_payments`, so a restart does not charge twice on the same day. ## Developer API [#developer-api] ### Client export `GeneratePlate` [#client-export-generateplate] ```lua local plate = exports.esx_vehicleshop:GeneratePlate() ``` Returns a random plate in the configured format that is not in `owned_vehicles` (checked with the `esx_vehicleshop:isPlateTaken` callback). It yields, so call it from a thread. The shop itself generates plates on the server and ignores client plates. ```lua title="client/main.lua" CreateThread(function() local plate = exports.esx_vehicleshop:GeneratePlate() print(('Free plate: %s'):format(plate)) end) ``` ### Server callbacks [#server-callbacks] | Callback | Arguments | Who | Notes | | --- | --- | --- | --- | | `esx_vehicleshop:buyVehicle` | `model`, `plate` | Anyone within 30 m of `ShopInside` | Plate argument is ignored, price comes from `vehicles`. | | `esx_vehicleshop:resellVehicle` | `plate`, `model` (hash) | Owner near `ResellVehicle` | Checks ownership, model and plate. | | `esx_vehicleshop:giveBackVehicle` | `plate` | Renter near `GiveBackVehicle` | Vehicle with that plate must be within 8 m. | | `esx_vehicleshop:buyCarDealerVehicle` | `model` | `cardealer` near showroom | Pays from `society_cardealer`. | | `esx_vehicleshop:getCommercialVehicles` | none | `cardealer` | Current stock. | | `esx_vehicleshop:getRentedVehicles` | none | `cardealer` near boss marker | Active rentals. | | `esx_vehicleshop:getSoldVehicles` | `page` | `cardealer` | Sales log, 50 per page. | | `esx_vehicleshop:getStockItems` / `getPlayerInventory` | none | `cardealer` / player | Society stash and own inventory. | | `esx_vehicleshop:isPlateTaken` | `plate` | Anyone | `true` if the plate exists in `owned_vehicles`. | | `esx_vehicleshop:retrieveJobVehicles` | `type` | Player | Own vehicles of `type` for the current job. | ### Net events (client to server) [#net-events-client-to-server] | Event | Arguments | Guard | | --- | --- | --- | | `esx_vehicleshop:setVehicleOwnedPlayerId` | `playerId, vehicleProps, model, label` | `cardealer` near showroom, model must be in stock. Sells a stock car to `playerId`. | | `esx_vehicleshop:rentVehicle` | `vehicle, plate, rentPrice, playerId` | Same guard. `plate` and `rentPrice` are ignored, the server computes both. | | `esx_vehicleshop:returnProvider` | `vehicleModel` | `cardealer` near boss or resell marker. | | `esx_vehicleshop:getStockItem` / `putStockItems` | `itemName, count` | `cardealer` near boss or resell marker. | | `esx_vehicleshop:setJobVehicleState` | `plate, state` | Only sets `stored = 0` for the player's own job vehicle near them. | | `esx_vehicleshop:getVehiclesAndCategories` | none | Sends the catalog to the caller. | The server broadcasts `esx_vehicleshop:updateVehiclesAndCategories(vehicles, categories, vehiclesByModel)` to clients. <Callout type="warn" title="Security notes"> All money-moving events and callbacks are guarded by job, distance and database transactions. Two gaps remain: the `drive` license check (`Config.LicenseEnable`) runs only on the client, and `isPlateTaken` can be called by anyone. If the license must be enforced, add a server-side `esx_license:checkLicense` call to your fork of `buyVehicle`. </Callout> ## Common issues [#common-issues] - **"Dealer transactions disabled" in the console.** One of the dealer tables is MyISAM. Convert it with `ALTER TABLE <name> ENGINE=InnoDB;`. - **Shop menu is empty.** The `vehicles` table is empty or categories do not match `vehicle_categories.name`. - **Plate length warning.** `PlateLetters + PlateNumbers` (+1 with space) is above 8. - **Rent is never charged.** The `cron` resource is not started, or `society_cardealer` is missing in `addon_account`. - **Job menu does not show.** `Config.EnablePlayerManagement` is off or the player does not have the `cardealer` job. ## Related [#related] - [esx_garage](/docs/addons/esx_garage) - [esx_society](/docs/addons/esx_society) - [esx_license](/docs/addons/esx_license) - [cron](/docs/core/cron) - [Database schema](/docs/guides/database-schema) # esx_weaponshop (/docs/addons/esx_weaponshop) esx_weaponshop is the official weapon store for ESX Legacy. It adds Ammu-Nation locations and a hidden black market dealer, opens a NUI catalog, and lets players buy weapons, a weapon license, ammo, attachments and tints. All purchases are validated and charged on the server. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_weaponshop` (version `1.0`) | | Side | Client + server, NUI (`web/dist`) | | Dependencies | `esx_lib`, `es_extended`, `esx_license`, OneSync (`/onesync`) | | Optional | `ox_inventory` (detected from `ESX.GetConfig().OxInventory`) | | SQL file | `esx_weaponshop.sql` (adds the `weapon` license) | | Config files | `shared/config/main.lua`, `shops.lua`, `upgrades.lua`, `weapon_images.lua` | In-game flow: - **Legal shops** (`Legal = true`, default `GunShop`) charge **cash**. With `Config.LicenseEnable`, players without the `weapon` license get a menu to buy it for `Config.LicensePrice` first. - **Illegal shops** (`Legal = false`, default `BlackWeashop`) charge **black_money**, never need a license and have their blip disabled by default. - Players can buy ammo, components and tints for a weapon they own and that the zone sells (when `Config.WeaponShopUpgrades.Enabled` is on). ## Installation [#installation] <Steps> <Step> ### Import the SQL [#import-the-sql] Run `esx_weaponshop.sql`. It only inserts the `weapon` license into the `licenses` table (from [esx_license](/docs/addons/esx_license)). ```sql INSERT IGNORE INTO `licenses` (`type`, `label`) VALUES ('weapon', "Weapon licence"); ``` The files in `localization/` still create a legacy `weashops` table. The current version does not read it, prices come from the Lua config. </Step> <Step> ### Ensure the resource [#ensure-the-resource] ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ensure esx_license ensure esx_weaponshop ``` OneSync must be enabled, the manifest declares `/onesync` as a dependency. </Step> </Steps> ## Configuration [#configuration] ### Main config (`shared/config/main.lua`) [#main-config-sharedconfigmainlua] | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Locale. | | `Config.OxInventory` | `ESX.GetConfig().OxInventory` | Uses ox_inventory for weapons and attachments when true. | | `Config.FallbackWeaponImage` | `''` | Image shown when a weapon image cannot be loaded. | | `Config.WeaponImages` | per-weapon map | Image overrides. `weapon_images.lua` fills this with bundled images. | | `Config.InitialWeaponAmmo` | `42` | Ammo given with a weapon bought through the ESX loadout. | | `Config.LicenseEnable` | `true` | Require the `weapon` license in legal shops. | | `Config.LicensePrice` | `5000` | Price of the weapon license (cash). | | `Config.ServerDistanceBuffer` | `3.0` | Extra meters added to `InteractionDistance` for the server distance check. | | `Config.PurchaseCooldown` | `1500` | Minimum ms between purchases of the same type per player. | | `Config.LicenseCallbackTimeout` | `5000` | Timeout for esx_license calls in ms. | | `Config.ClientCallbackTimeout` | `5000` | Timeout when asking the client for its ammo state. | | `Config.DrawDistance` | `10.0` | Marker draw distance. | | `Config.Size` | `{ x = 1.5, y = 1.5, z = 0.5 }` | Marker size. | | `Config.Color` | `{ r = 0, g = 128, b = 255 }` | Marker color. | | `Config.Type` | `1` | Marker type. | | `Config.InteractionDistance` | `2.0` | Distance to open the shop. | | `Config.InteractionKeyLabel` | `'E'` | Key label shown in the UI. | ### Shop zones (`shared/config/shops.lua`) [#shop-zones-sharedconfigshopslua] ```lua title="shared/config/shops.lua" Config.Zones = { GunShop = { Legal = true, Blip = { Enabled = true, Sprite = 110, Color = 81, Display = 4, Scale = 0.7, ShortRange = true }, Items = { { name = 'WEAPON_PISTOL', price = 500 }, { name = 'WEAPON_BAT', price = 50 }, }, Locations = { vector3(-662.1, -935.3, 20.8), }, }, } ``` | Field | Description | | --- | --- | | `Legal` | `true` = cash + license check, `false` = black money, no license. | | `Blip` | `Enabled`, `Sprite`, `Color`, `Display`, `Scale`, `ShortRange`. | | `Items` | `{ name, price }` per weapon. Only weapons listed here can be bought or upgraded in this zone. | | `Locations` | `vector3` list of counters. | ### Upgrades (`shared/config/upgrades.lua`) [#upgrades-sharedconfigupgradeslua] | Key | Default | Description | | --- | --- | --- | | `Config.WeaponShopUpgrades.Enabled` | `true` | Master switch. With ox_inventory enabled, upgrades also require ox_inventory to be started. | | `Ammo.Enabled` | `true` | Sell ammo. | | `Ammo.PricePerRound` | `5` | Price per round. | | `Ammo.DefaultAmount` / `MinAmount` / `MaxAmount` | `30` / `1` / `250` | Amount limits per purchase. | | `Ammo.MaxTotal` | `250` | Maximum ammo a weapon may hold after buying. | | `Ammo.QuickAmounts` | `{ 30, 60, 120, 250 }` | Buttons in the UI. | | `Components.Enabled` | `true` | Sell attachments. | | `Components.DefaultPrice` | `750` | Price when a component has no entry in `Prices`. | | `Components.Blacklisted` | `{ clip_default = true }` | Components that are never sold. | | `Components.Prices` | e.g. `suppressor = 1250`, `scope = 1500` | Per-component prices. | | `Components.OxItemsByName` / `OxItemsByHash` | mapping | ESX component name or hash to ox_inventory attachment item. | | `Tints.Enabled` | `true` | Sell weapon tints. | | `Tints.DefaultPrice` | `350` | Price when a tint index has no entry. | | `Tints.Prices` | `[0] = 0` ... `[7] = 900` | Price per tint index. | | `Tints.Swatches` | hex colors | Colors shown in the UI. | ## Developer API [#developer-api] The resource has no exports. Its server callbacks are used by the NUI: | Callback | Arguments | Description | | --- | --- | --- | | `esx_weaponshop:buyWeapon` | `weaponName`, `zone` | Buys a weapon from a zone. | | `esx_weaponshop:buyLicense` | none | Buys the `weapon` license near a legal shop. | | `esx_weaponshop:buyUpgrade` | `data` (`{ weaponName, action, ... }`), `zone` | `action` is `'ammo'`, `'component'` or `'tint'`. | The server also sends `esx_weaponshop:requestWeaponAmmoState` to the client and accepts the answer through the net event `esx_weaponshop:receiveWeaponAmmoState` (only from the same player, matched by request id). <Callout type="info" title="Client-triggerable"> All three callbacks are client-callable. The server validates the zone, the weapon name, the distance to a zone location (`InteractionDistance + ServerDistanceBuffer`), the license, the price from config, the payment and the inventory, and refunds when adding the weapon fails. Requests are rate-limited per player and action. Never move prices to the client. </Callout> To check the weapon license from your own scripts, use [esx_license](/docs/addons/esx_license): ```lua title="server/main.lua" TriggerEvent('esx_license:checkLicense', playerId, 'weapon', function(hasLicense) if not hasLicense then TriggerClientEvent('esx:showNotification', playerId, 'You need a weapon license.') end end) ``` ## Common issues [#common-issues] - **Resource does not start.** `esx_license` is a hard dependency and OneSync must be on. - **License menu never appears.** `Config.LicenseEnable` is `false`, or the zone has `Legal = false`. - **Purchase fails silently.** The player is too far from a `Locations` entry, is spamming (cooldown), lacks cash / black money, or cannot carry the weapon. - **Upgrades unavailable.** `Config.WeaponShopUpgrades.Enabled` is off, or `Config.OxInventory` is true and ox_inventory is not started. The player must own the weapon. - **Changed prices in `weashops` have no effect.** Edit `Config.Zones` instead. ## Related [#related] - [esx_license](/docs/addons/esx_license) - [esx_shops](/docs/addons/esx_shops) - [Weapons config](/docs/es_extended/config/weapons) - [ox_inventory with ESX](/docs/guides/ox-inventory) # esx_weather (/docs/addons/esx_weather) esx_weather is the ESX Legacy weather and time sync. It splits the map into zones (Los Santos, Sandy Shores and Paleto Bay by default), gives each zone its own weather that rotates on a timer, and lets admins change a zone's weather from an in-game panel. Every player sees the weather of the zone closest to them, with a smooth transition when they drive into another zone. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_weather` (version 1.0.0, added with ESX Legacy 1.16.0) | | Side | Server state, client sync, NUI admin panel | | Dependencies | `esx_lib`, `es_extended` | | Database | None; persistence uses resource KVP | Run only one weather sync. Remove vSync, cd_easytime, qb-weathersync or similar resources before using esx_weather, or they will overwrite each other every tick. ## Installation [#installation] ```bash title="server.cfg" ensure esx_lib ensure es_extended ensure esx_weather ``` ## How it works [#how-it-works] 1. On start the server gives each zone a random weather from `ValidTypes` (or `defaultType` when `dynamic` is off), then restores saved weather from KVP if `persist` is on. 2. With `dynamic = true`, every `cycleTimeSeconds` all zones get a new random weather and the server broadcasts the list. 3. Each client checks once per second which zone center is closest and applies that zone's weather with `SetWeatherTypeOvertimePersist` over `transitionTimeSeconds`. 4. For time, the server sends its real clock (`os.date`) on start and when a player loads. When the client enters a zone, it sets the clock to that time plus the zone's hour offset. GTA then advances the clock at `secondsPerGameMinute` real seconds per game minute. ## Configuration [#configuration] All keys are in `config/main.lua`. | Key | Default | Description | | --- | --- | --- | | `Config.debug` | `false` | Debug prints | | `Config.panelCommand` | `"weatherpanel"` | Command that opens the admin panel | | `Config.AdminGroups` | `{ admin = true, moderator = true }` | ESX groups allowed to use the panel | | `Config.Zones` | 3 zones | Zone name to `vector2` center, see below | | `Config.Weather.ValidTypes` | all 17 types | Weather types used for random rotation and accepted from the panel | | `Config.Weather.transitionTimeSeconds` | `30` | Seconds to blend into a new weather | | `Config.Weather.cycleTimeSeconds` | `1800` (30 min) | Seconds between automatic rotations | | `Config.Weather.dynamic` | `true` | Rotate weather automatically; `false` keeps `defaultType` until changed in the panel | | `Config.Weather.defaultType` | `CLEAR` | Start weather when `dynamic` is `false`; must be in `ValidTypes` | | `Config.Weather.persist` | `true` | Save zone weather (and the rotation timer) in KVP across restarts | | `Config.Weather.persistKey` | `"esx_weather_zones"` | KVP key used for persistence | | `Config.Time.secondsPerGameMinute` | `15` | Real seconds per in-game minute (a full day takes 6 hours) | | `Config.Time.Zones` | LS `0`, Sandy `1`, Paleto `2` | Hour offset per zone | Default zone centers: ```lua title="config/main.lua" Config.Zones = { [Shared.Enum.Zone.LOS_SANTOS] = vector2(-33.6997, -1102.0000), [Shared.Enum.Zone.SANDY_SHORES] = vector2(1728.2000, 3709.3000), [Shared.Enum.Zone.PALETO_BAY] = vector2(-220.2753, 6408.9116), } ``` The weather types are `CLEAR`, `EXTRASUNNY`, `CLOUDS`, `OVERCAST`, `RAIN`, `CLEARING`, `THUNDER`, `SMOG`, `FOGGY`, `XMAS`, `SNOW`, `SNOWLIGHT`, `BLIZZARD`, `HALLOWEEN`, `NEUTRAL`, `RAIN_HALLOWEEN` and `SNOW_HALLOWEEN`. <Callout type="warn" title="Remove snow and Halloween types for normal play"> The default `ValidTypes` list contains every type, so random rotation can produce `XMAS`, `BLIZZARD` or `HALLOWEEN` in the middle of summer. Most servers trim the list to `CLEAR`, `EXTRASUNNY`, `CLOUDS`, `OVERCAST`, `RAIN`, `CLEARING`, `THUNDER`, `SMOG` and `FOGGY`. </Callout> The server validates the config on start and throws a clear error if, for example, `Zones` is empty, `cycleTimeSeconds` is not positive, or `defaultType` is not in `ValidTypes`. ### Adding a zone [#adding-a-zone] Zones are matched by name, so a new zone needs an entry in three places: ```lua title="shared/enum/Zone.lua" Shared.Enum.Zone = { LOS_SANTOS = "Los Santos", SANDY_SHORES = "Sandy Shores", PALETO_BAY = "Paleto Bay", CAYO_PERICO = "Cayo Perico", } ``` ```lua title="config/main.lua" Config.Zones[Shared.Enum.Zone.CAYO_PERICO] = vector2(4840.0, -5174.0) Config.Time.Zones[Shared.Enum.Zone.CAYO_PERICO] = 0 ``` ## Admin panel [#admin-panel] `/weatherpanel` opens a NUI panel with the zone you are in, its current weather and a selector for every zone. The client hides the panel from non-admins, and the server re-checks the group on every change, so the panel cannot be abused by a modified client. ## Client exports [#client-exports] ### ToggleWeatherSync [#toggleweathersync] ```lua exports.esx_weather:ToggleWeatherSync(state) ``` ### ToggleTimeSync [#toggletimesync] ```lua exports.esx_weather:ToggleTimeSync(state) ``` | Name | Type | Description | | --- | --- | --- | | `state` | `boolean` | `false` pauses syncing on this client, `true` resumes it; anything else throws an error | ```lua title="client/heist_intro.lua" exports.esx_weather:ToggleWeatherSync(false) exports.esx_weather:ToggleTimeSync(false) SetWeatherTypeNowPersist('EXTRASUNNY') NetworkOverrideClockTime(12, 0, 0) -- play the scene exports.esx_weather:ToggleWeatherSync(true) exports.esx_weather:ToggleTimeSync(true) ``` When you resume, esx_weather only re-applies weather or time if the zone value differs from what it last applied. If you changed weather or time manually while sync was off, the change can stay until the zone's weather changes or the player enters another zone. ## Events [#events] | Event | Direction | Description | | --- | --- | --- | | `esx_weather:server:setZoneWeather` | client to server | `(zone, weatherType)` from the panel. Admin group check, zone and type validation, 1 s rate limit | | `esx_weather:client:weather:setZones` | server to client | Full zone weather table | | `esx_weather:client:weather:setZone` | server to client | `(zone, weatherType)` for a single zone | | `esx_weather:client:time:setTime` | server to client | `{ hours, minutes, seconds }` base time | esx_weather has no server exports. The internal `Modules.Weather.setZone` function is not exposed to other resources, so changing a zone's weather from another script means editing esx_weather itself (for example by adding your own `exports('SetZoneWeather', Modules.Weather.setZone)` in `server/main.lua`). ## Common issues [#common-issues] - **Weather flickers between two states:** another weather sync resource is running. Remove it. - **Players see different times:** time is set when a player enters a zone, from the server time sent at login plus elapsed real seconds, while GTA's own clock runs faster. Clocks can drift between players who have not changed zones for a while. - **Snow or Halloween weather appears:** trim `Config.Weather.ValidTypes`. - **Panel does not open:** your ESX group is not in `Config.AdminGroups`, or the zone weather has not been received yet (wait a few seconds after joining). - **Changes in config are ignored after restart:** `persist = true` restores the saved weather. Delete the KVP or change `persistKey` to start fresh. ## Related [#related] - [esx_adminmenu](/docs/addons/esx_adminmenu) - [Admin permissions](/docs/guides/admin-permissions) - [esx_lib](/docs/core/esx_lib) - [Addons overview](/docs/addons) # ESX Legacy Addons (/docs/addons) ESX Legacy Addons is the official collection of optional resources that sit on top of `es_extended`: jobs, shops, vehicles, housing, HUD, chat and admin tools. Install the whole folder for a ready-to-play roleplay server, or pick single addons and respect their dependencies. ## Overview [#overview] | Fact | Value | |------|-------| | Repository | [esx-framework/ESX-Legacy-Addons](https://github.com/esx-framework/ESX-Legacy-Addons) | | Folder | `[esx_addons]` (39 resources) | | Required by all | `esx_lib`, `es_extended` (most also need `oxmysql`) | | Installed by | The txAdmin ESX recipe, or manually | Every addon lists its hard dependencies in `fxmanifest.lua`. FiveM refuses to start a resource whose dependencies are missing, and prints `Couldn't start resource ... dependency ... not found` in the console. ## All addons [#all-addons] | Addon | What it does | Category | |-------|--------------|----------| | [esx_accessories](/docs/addons/esx_accessories) | Shops for masks, helmets, glasses and ear accessories, saved per player | player | | [esx_accessorymanager](/docs/addons/esx_accessorymanager) | Radial menu to take off and restore clothing and props | player | | [esx_addonaccount](/docs/addons/esx_addonaccount) | Extra money accounts, such as society funds and property black money | economy | | [esx_addoninventory](/docs/addons/esx_addoninventory) | Shared and personal item storage for societies and properties | economy | | [esx_adminmenu](/docs/addons/esx_adminmenu) | Admin menu with player actions, bans and action logs | admin | | [esx_ambulancejob](/docs/addons/esx_ambulancejob) | EMS job: revive, heal, pharmacy, job vehicles | jobs | | [esx_animations](/docs/addons/esx_animations) | Emote menu with animations, scenarios and walk styles | player | | [esx_banking](/docs/addons/esx_banking) | Banks and ATMs with PIN codes, transfers and history | economy | | [esx_barbershop](/docs/addons/esx_barbershop) | Paid hair, beard and makeup changes | player | | [esx_basicneeds](/docs/addons/esx_basicneeds) | Hunger and thirst, food and drink items | player | | [esx_billing](/docs/addons/esx_billing) | Invoices and fines between players and jobs | economy | | [esx_boat](/docs/addons/esx_boat) | Boat shop, boat license and boat garages | vehicles | | [esx_clotheshop](/docs/addons/esx_clotheshop) | Clothing stores and saved outfits | player | | [esx_cruisecontrol](/docs/addons/esx_cruisecontrol) | Cruise control and seatbelt | vehicles | | [esx_datastore](/docs/addons/esx_datastore) | JSON data storage per player or per society | economy | | [esx_death](/docs/addons/esx_death) | Death state, death screen, respawn and revive | player | | [esx_dmvschool](/docs/addons/esx_dmvschool) | Theory and practical driving tests | vehicles | | [esx_drugs](/docs/addons/esx_drugs) | Harvest, process and sell cannabis for black money | economy | | [esx_garage](/docs/addons/esx_garage) | Garages and impound lots with a React UI | vehicles | | [esx_hud](/docs/addons/esx_hud) | Default HUD: status bars, money, speedometer, mileage | player | | [esx_joblisting](/docs/addons/esx_joblisting) | Job center where players pick a non-whitelisted job | jobs | | [esx_jobs](/docs/addons/esx_jobs) | Simple civilian jobs: miner, lumberjack, fisherman and more | jobs | | [esx_license](/docs/addons/esx_license) | Stores licenses like driving and weapon licenses | player | | [esx_lscustom](/docs/addons/esx_lscustom) | Los Santos Customs vehicle tuning | vehicles | | [esx_mechanicjob](/docs/addons/esx_mechanicjob) | Mechanic job: repairs, towing, crafting | jobs | | [esx_optionalneeds](/docs/addons/esx_optionalneeds) | Drunk status and a beer item | player | | [esx_pausemenu](/docs/addons/esx_pausemenu) | Custom pause menu with character and job info | player | | [esx_policejob](/docs/addons/esx_policejob) | Police job: armory, handcuffs, fines, vehicles | jobs | | [esx_property](/docs/addons/esx_property) | Buy, furnish and share houses, real estate job | world | | [esx_rpchat](/docs/addons/esx_rpchat) | `/me`, `/do`, OOC, `/twt` and private messages | player | | [esx_scoreboard](/docs/addons/esx_scoreboard) | Player list with job counts and live activities | player | | [esx_service](/docs/addons/esx_service) | On/off duty system with per-job limits | jobs | | [esx_shops](/docs/addons/esx_shops) | 24/7, liquor and LTD stores with a cart UI | economy | | [esx_society](/docs/addons/esx_society) | Job bank accounts, boss menu, salaries, money washing | economy | | [esx_status](/docs/addons/esx_status) | Status engine behind hunger, thirst and drunk | player | | [esx_taxijob](/docs/addons/esx_taxijob) | Taxi job with NPC fares | jobs | | [esx_vehicleshop](/docs/addons/esx_vehicleshop) | Car dealership, car dealer job, rentals | vehicles | | [esx_weaponshop](/docs/addons/esx_weaponshop) | Ammu-Nation and black market weapon shops | economy | | [esx_weather](/docs/addons/esx_weather) | Per-zone weather and time sync with admin panel | world | ## Installing addons [#installing-addons] <Steps> <Step> ### Get the resources [#get-the-resources] The [txAdmin ESX recipe](/docs/getting-started/installation) clones the repository and copies `[esx_addons]` into `resources/` for you. For a manual setup, download the repository from GitHub and copy either the whole `[esx_addons]` folder or single addon folders into your `resources` directory. </Step> <Step> ### Import the SQL [#import-the-sql] Most addons ship a `<name>.sql` file that creates their tables and inserts jobs, items or society accounts. Import it into your ESX database once. If you installed with the txAdmin recipe, skip this step: `legacy.sql` already contains these rows, and re-importing causes duplicate key errors. Some addons (`esx_addonaccount`, `esx_billing`, `esx_banking`, `esx_license`, `esx_garage`, `esx_vehicleshop`, `esx_property`, `esx_death`, `esx_adminmenu`, `esx_society`) also create or migrate their own tables on startup. </Step> <Step> ### Ensure them in server.cfg [#ensure-them-in-servercfg] Start addons after the framework. Folder names in brackets start every resource inside them: ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ensure [core] ensure [esx_addons] ``` To run only a few addons, ensure them by name and list storage addons before the jobs that use them: ```bash title="server.cfg" ensure esx_addonaccount ensure esx_addoninventory ensure esx_datastore ensure esx_society ensure esx_billing ensure esx_policejob ``` </Step> <Step> ### Configure [#configure] Every addon has a `config.lua` (some also a `config/` folder). Change locations, prices and job settings there and restart the resource. See the addon page for every key and its default. </Step> </Steps> ## Dependencies between addons [#dependencies-between-addons] The table combines the hard dependencies from `fxmanifest.lua` with soft dependencies found in the code (exports, events and callbacks into another addon). Soft dependencies do not block startup, but the related feature breaks without them. | Addon | Hard dependencies (fxmanifest) | Also uses at runtime | |-------|-------------------------------|----------------------| | esx_society | `esx_addonaccount`, `cron` | `esx_datastore` (society garage) | | esx_policejob | `esx_billing`, `esx_vehicleshop` | `esx_society`, `esx_addoninventory`, `esx_datastore`, `esx_license`, `esx_service`, `esx_status`, `esx_skin` | | esx_ambulancejob | `esx_death`, `esx_skin`, `esx_vehicleshop` | `esx_society` | | esx_mechanicjob | `esx_society`, `esx_billing` | `esx_addonaccount`, `esx_addoninventory`, `esx_service`, `esx_skin` | | esx_taxijob | — | `esx_society`, `esx_billing`, `esx_addonaccount`, `esx_addoninventory`, `esx_datastore`, `esx_service`, `esx_skin` | | esx_vehicleshop | — | `esx_addonaccount`, `esx_addoninventory`, `esx_billing`, `esx_license`, `esx_society` | | esx_property | `oxmysql` | `esx_addonaccount`, `esx_datastore`, `esx_society`, `esx_skin`, `esx_context` | | esx_weaponshop | `esx_license`, OneSync | — | | esx_dmvschool | `esx_license` | — | | esx_boat | `esx_vehicleshop` | `esx_license` | | esx_basicneeds | `esx_status` | — | | esx_optionalneeds | — | `esx_status` | | esx_hud | `esx_status`, `oxmysql` | — | | esx_billing | — | `esx_addonaccount` (society bills) | | esx_accessories | `esx_skin`, `esx_datastore` | — | | esx_clotheshop, esx_barbershop | `esx_skin` | `esx_datastore` (clotheshop outfits) | | esx_jobs | OneSync | `esx_addonaccount` (vehicle deposit) | The chain most servers hit is **job → esx_society → esx_addonaccount / esx_datastore / esx_addoninventory**. Every society needs a row in `addon_account` (money), and jobs with a stash also need rows in `addon_inventory` and `datastore`. If a job's boss menu shows no money or the armory is empty, those rows are usually missing. <Callout type="info" title="esx_phone references"> Several job addons still trigger `esx_phone` events for dispatch messages. `esx_phone` is not part of the repository; without it those calls do nothing and no error is shown. </Callout> ## Security hardening SQL [#security-hardening-sql] The repository root contains two SQL files related to the October 2026 security audit. ### security_indexes.sql [#security_indexessql] This file is **superseded**. It only contains comments and a `SELECT` that prints a note pointing to `sql/security_hardening_2026_10_02.sql`. Running it changes nothing. The comments also warn against blindly adding indexes such as `(identifier, id)` on `billing` or `(owner, plate)` on `owned_vehicles`, because InnoDB secondary indexes already include the primary key. ### sql/security_hardening_2026_10_02.sql [#sqlsecurity_hardening_2026_10_02sql] This migration enforces data integrity for the economy tables that `esx_billing`, `esx_license`, `esx_addonaccount` and `esx_banking` rely on. It runs as a temporary stored procedure and is safe to run more than once. Each step is skipped when it is already applied. It first **refuses to run** (raises an error) when it finds: - duplicate `user_licenses` rows for the same `owner` and `type` - duplicate `addon_account_data` rows for the same account and owner - `addon_account_data` rows with a `NULL` account name or an empty-string owner - `billing`, `users` or `addon_account_data` tables that are not InnoDB Then it: | Change | Purpose | |--------|---------| | Creates `billing_payments` | Payment journal with a unique `operation_id`, so a bill cannot be paid twice | | Unique key on `user_licenses (owner, type)` | A player can hold each license type only once | | Index on `billing (identifier)` | Fast lookup of a player's bills | | Generated column `addon_account_data.owner_key` plus unique key `(account_name, owner_key)` | One row per account and owner, including shared accounts where `owner` is `NULL` | | Index on `banking (time)` | Fast transaction history queries | Finally, it lists `billing_payments` rows still in state `claimed` (payments that started but were never applied). Review them manually; the script never refunds or replays them. <Callout type="warn" title="Before you run it"> Back up the database and run it in a maintenance window. DDL statements commit immediately, so there is no rollback. It needs MySQL 8 or MariaDB 10.6+ and a client that understands `DELIMITER` (HeidiSQL, the `mysql` CLI). Resolve duplicate rows yourself: the script never merges or deletes financial data. Recent versions of billing, license, addonaccount and banking apply the same schema changes on startup, so the manual script is optional. </Callout> ## Common issues [#common-issues] - **`Couldn't start resource esx_xxx`**: a hard dependency is missing or starts later. Check the dependency table above. - **Duplicate entry errors on SQL import**: the rows already exist, usually from the txAdmin `legacy.sql`. Skip that import. - **Boss menu shows $0 or no society found**: the `society_<job>` row is missing in `addon_account` / `addon_account_data`, or `esx_society` started before `esx_addonaccount`. - **Hardening script stops with "Duplicate ..."**: find the duplicates with a `GROUP BY ... HAVING COUNT(*) > 1` query, merge balances by hand, then run it again. ## Related [#related] - [Installation](/docs/getting-started/installation) - [server.cfg](/docs/getting-started/server-cfg) - [esx_society](/docs/addons/esx_society) - [Create a job](/docs/guides/create-a-job) - [Database schema](/docs/guides/database-schema) # cron (/docs/core/cron) cron is a tiny server-side scheduler shipped with ESX Legacy. Other resources register a callback for a time of day (hour and minute) and cron calls it once every day at that time. Official addons use it, for example `esx_society` for salaries and `esx_vehicleshop` for rental payments. ## Overview [#overview] | | | | --- | --- | | Resource | `cron` | | Side | Server only | | Dependencies | None | | Config | None | | Required | Only when a resource uses `cron:runAt` (esx_society and esx_vehicleshop do) | ## cron:runAt [#cronrunat] The only API is a server-side local event. ```lua TriggerEvent("cron:runAt", h, m, cb) ``` | Name | Type | Description | | --- | --- | --- | | `h` | `number` | Hour, 0-23, in server local time. | | `m` | `number` | Minute, 0-59. | | `cb` | `function` | Called as `cb(d, h, m)`. A callable table (with `__call`) also works. | Callback arguments: | Name | Type | Description | | --- | --- | --- | | `d` | `number` | Weekday from `os.date("*t").wday`: 1 = Sunday, 2 = Monday, ..., 7 = Saturday. | | `h` | `number` | The hour you registered. | | `m` | `number` | The minute you registered. | Wrong argument types fail with an `assert` that names the invoking resource. ### Examples [#examples] ```lua title="server/main.lua" -- Every day at 05:10 TriggerEvent("cron:runAt", 5, 10, function(d, h, m) print(("Daily cleanup at %02d:%02d"):format(h, m)) MySQL.update("DELETE FROM my_logs WHERE created_at < NOW() - INTERVAL 30 DAY") end) ``` ```lua title="server/main.lua" -- Every Monday at 18:30 local MONDAY <const> = 2 TriggerEvent("cron:runAt", 18, 30, function(d) if d ~= MONDAY then return end print("Weekly payout") end) ``` ```lua title="server/main.lua" -- Every 6 hours: register several times for _, hour in ipairs({ 0, 6, 12, 18 }) do TriggerEvent("cron:runAt", hour, 0, function() print("Six-hour task") end) end ``` ## How scheduling works [#how-scheduling-works] - cron checks once per minute, aligned to the start of the minute. - On each new minute it computes every job's most recent scheduled time (today, or yesterday if today's time is still ahead) and runs the job if that time is newer than the previous check. A short server hitch therefore does not skip a job. - Jobs do **not** catch up after a restart. A task scheduled for 05:10 does not run if the server was offline at 05:10. - Errors in a callback are caught and printed as `cron job at HH:MM errored, skipping it`; other jobs keep running. ## Common issues [#common-issues] - **Weekday check never matches.** The official README uses `d == 1` for Monday. In Lua `wday` 1 is Sunday; Monday is `2`. - **Task runs at the wrong hour.** The server's timezone differs from yours. Check `os.date()` in the server console. - **Errors after restarting a resource.** Jobs cannot be removed. When you restart a resource that registered a job, the old callback reference stays registered and errors every day, next to the new job the restarted resource registers. The error is harmless, but a full server restart cleans it up. - **Jobs stop after restarting cron.** Jobs live in cron's memory. Restarting `cron` alone drops every job; the resources that registered them must be restarted too. - **Registering from the client.** `cron:runAt` is a server-only local event; it does nothing on the client and cannot be triggered over the network. ## Related [#related] - [Core resources overview](/docs/core) - [Server functions](/docs/es_extended/server/functions) - [esx_society](/docs/addons/esx_society) - [oxmysql guide](/docs/guides/oxmysql) # esx_chat_theme (/docs/core/esx_chat_theme) esx_chat_theme is a visual theme for the default FiveM `chat` resource. It gives chat messages the GTA-style Chalet font with a drop shadow and removes the chat window background. It contains no Lua code and no API; it is a pure style add-on you can keep, edit or remove. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_chat_theme` | | Side | Client (NUI styles for `chat`) | | Dependencies | The cfx `chat` resource (not declared in the manifest) | | Config | None, edit `style.css` | | Required | No | | Game | `common` | ## How it works [#how-it-works] The cfx chat resource scans running resources for a `chat_theme` entry in their `fxmanifest.lua` and loads the listed files into the chat page. esx_chat_theme registers a theme named `esx`: ```lua title="fxmanifest.lua" chat_theme 'esx' { styleSheet = 'style.css', script = 'shadow.js', msgTemplates = { default = '<b>{0}</b><span>{1}</span>' } } ``` | Part | Purpose | | --- | --- | | `style.css` | Fonts (`Font2`, `Font2_cond` loaded from `runtime.fivem.net`), transparent `.chat-window`, message size and spacing | | `shadow.js` | Injects an SVG drop shadow filter (`#svgDropShadowFilter`) that `.msg` uses for readable text on any background | | `msgTemplates.default` | Renders the author (`{0}`) in bold followed by the message (`{1}`) | ## Customization [#customization] Change the look in `style.css`. The `.msg` rule controls most of it: ```css title="style.css" .msg { font-family: Font2, sans-serif; color: #ffffff; font-size: calc(1.7vh); filter: url(#svgDropShadowFilter); line-height: calc(2.7vh * 1.2); margin-bottom: 0; } ``` The font files are loaded from `runtime.fivem.net` at runtime. If that host is unreachable, the chat falls back to `sans-serif`. ## Install order [#install-order] Start `chat` before the theme. The official recipe starts `chat` first and the theme as part of `[core]`: ```bash title="server.cfg" ensure chat ensure oxmysql ensure esx_lib ensure es_extended ensure [core] ``` ## Common issues [#common-issues] - **Theme has no effect.** The `chat` resource is not running, or you use a third-party chat that ignores `chat_theme` entries. - **Styles clash with another theme.** Several `chat_theme` resources can be active at once and their CSS stacks. Stop the one you do not want. ## Related [#related] - [Core resources overview](/docs/core) - [server.cfg](/docs/getting-started/server-cfg) - [Installation](/docs/getting-started/installation) # esx_context (/docs/core/esx_context) esx_context is the mouse-driven context menu of ESX Legacy. Use it for short action lists (an interaction menu, a boss menu) and small forms with text, number or radio inputs. You open it from client code with `ESX.OpenContext`, which is a wrapper around the `esx_context` exports. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_context` | | Side | Client only | | Dependencies | `es_extended` | | UI | Plain HTML page (`index.html`), Font Awesome icons | | Version | 1.16.x (ships with ESX Legacy 1.16.4) | The menu is a single panel. Only one context menu can be open at a time; opening a new one replaces the current one. ## Configuration [#configuration] `config.lua` contains a single global: | Key | Default | Description | | --- | --- | --- | | `PREVIEW_KEYBIND` | `"LMENU"` (left Alt) | Default key for the `previewContext` key mapping, which gives mouse focus to a menu opened in preview mode. Set it to `nil` to skip registering the command and key mapping. | Players can rebind the key under **Settings > Key Bindings > FiveM** ("Preview Active Context"). Debug commands (see [Commands](#commands)) are only registered when `Config.EnableDebug` in es_extended is `true`. ## Functions [#functions] All functions are client side and available on the `ESX` object once you load `@es_extended/imports.lua` or call `exports['es_extended']:getSharedObject()`. Every wrapper returns `false` when `esx_context` is not running. ### ESX.OpenContext [#esxopencontext] Opens a context menu and gives it mouse and keyboard focus. ```lua ESX.OpenContext(position, elements, onSelect, onClose, canClose) ``` | Name | Type | Description | | --- | --- | --- | | `position` | `string` | `"left"`, `"center"` or `"right"` (default `"right"`). | | `elements` | `table` | Array of [elements](#element-fields). | | `onSelect` | `function(menu, element)?` | Runs when the player clicks a selectable element. | | `onClose` | `function(menu)?` | Runs after the menu closes, by key or by `ESX.CloseContext`. | | `canClose` | `boolean?` | `false` blocks closing with Escape/Backspace. Defaults to `true`. | The `menu` argument is the internal menu table: `menu.eles` holds the current elements (including updated `inputValue`s), `menu.position` the position. Before the elements reach the UI, `ESX.OpenContext` strips HTML tags from `title`, `description`, `label` and `inputPlaceholder`. Use plain text. ```lua title="client/main.lua" local function openGarageMenu() local elements = { { unselectable = true, icon = 'fas fa-warehouse', title = 'Garage' }, { icon = 'fas fa-car', title = 'Take out vehicle', name = 'take_out' }, { icon = 'fas fa-square-parking', title = 'Park vehicle', name = 'park' }, { disabled = true, icon = 'fas fa-wrench', title = 'Repair', description = 'Mechanics only' }, } ESX.OpenContext('right', elements, function(menu, element) if element.name == 'take_out' then TriggerServerEvent('my_garage:takeOut') elseif element.name == 'park' then TriggerServerEvent('my_garage:park') end ESX.CloseContext() end) end RegisterCommand('garage', openGarageMenu, false) ``` ### ESX.PreviewContext [#esxpreviewcontext] Shows the menu without taking mouse focus. The player keeps control of the character and can press the preview key (left Alt by default) to focus the menu and click it. ```lua ESX.PreviewContext(position, elements, onSelect, onClose, canClose) ``` The parameters are the same as `ESX.OpenContext`. The preview key only works when an `onSelect` function was passed. <Callout type="warn"> `ESX.PreviewContext` passes elements to the UI without the HTML stripping that `ESX.OpenContext` and `ESX.RefreshContext` apply. </Callout> ### ESX.CloseContext [#esxclosecontext] Closes the open menu, releases focus and calls the `onClose` callback. Does nothing if no menu is open. ```lua ESX.CloseContext() ``` ### ESX.RefreshContext [#esxrefreshcontext] Replaces the elements and/or the position of the open menu without closing it. Callbacks and `canClose` stay the same. ```lua ESX.RefreshContext(elements, position) ``` | Name | Type | Description | | --- | --- | --- | | `elements` | `table?` | New element list. `nil` keeps the current elements. | | `position` | `string?` | New position. `nil` keeps the current position. | ```lua -- Update a live counter in the open menu local elements = { { unselectable = true, icon = 'fas fa-users', title = 'Players online' }, { icon = 'fas fa-hashtag', title = tostring(#GetActivePlayers()) }, } ESX.RefreshContext(elements) ``` ## Element fields [#element-fields] | Field | Type | Description | | --- | --- | --- | | `title` | `string` | Main text of the row. | | `description` | `string?` | Smaller text below the title. | | `icon` | `string?` | Font Awesome 6 free class, e.g. `"fas fa-check"`. Use `""` for no icon. | | `name` | `string?` | Your identifier. Use it in `onSelect` and to read input values. | | `unselectable` | `boolean?` | Rendered as a header row, not clickable. | | `disabled` | `boolean?` | Rendered grayed out, not clickable. | | `input` | `boolean?` | Turns the row into an input field. Input rows never trigger `onSelect`. | | `inputType` | `"text"` \| `"number"` \| `"radio"` | Type of input. | | `inputPlaceholder` | `string?` | Placeholder text. For radio inputs it also acts as the preselected value when `inputValue` is not set. | | `inputValue` | `string` \| `number` | Initial value. Updated with what the player enters. | | `inputMin` | `number?` | Lower bound for `number` inputs (clamped in the UI and in Lua). | | `inputMax` | `number?` | Upper bound for `number` inputs. | | `inputValues` | `table?` | Required for `radio`: array of `{ text = 'Label', value = 'key' }` options. | Any other field you add (for example `value` or `job`) is kept on the element and passed back to `onSelect`. ### Form example [#form-example] Input values are written back to `menu.eles` when the field changes (on blur or Enter for text, on change for number and radio). Read them when the player clicks a submit row: ```lua title="client/main.lua" local function openNameForm() local elements = { { unselectable = true, icon = 'fas fa-id-card', title = 'Register vehicle' }, { icon = '', title = 'Plate text', input = true, inputType = 'text', inputPlaceholder = 'ABC 123', name = 'plate' }, { icon = '', title = 'Seats', input = true, inputType = 'number', inputValue = 2, inputMin = 1, inputMax = 8, name = 'seats' }, { icon = '', title = 'Engine', input = true, inputType = 'radio', name = 'engine', inputValue = 'petrol', inputValues = { { text = 'Petrol', value = 'petrol' }, { text = 'Electric', value = 'electric' }, }, }, { icon = 'fas fa-check', title = 'Submit', name = 'submit' }, } ESX.OpenContext('center', elements, function(menu, element) if element.name ~= 'submit' then return end local values = {} for _, ele in ipairs(menu.eles) do if ele.input then values[ele.name] = ele.inputValue end end if not values.plate or values.plate == '' then ESX.ShowNotification('Enter a plate first', 'error') return end TriggerServerEvent('my_registry:register', values) ESX.CloseContext() end) end ``` <Callout type="error" title="Validate on the server"> Everything in `values` comes from the client. Check lengths, ranges and permissions again in your server event. </Callout> ## Exports [#exports] The `ESX.*` wrappers call these exports. You can use them directly from any client script. | Export | Signature | Notes | | --- | --- | --- | | `Open` | `exports.esx_context:Open(position, elements, onSelect, onClose, canClose)` | Opens with focus. No HTML stripping when called directly. | | `Preview` | `exports.esx_context:Preview(position, elements, onSelect, onClose, canClose)` | Opens without focus. | | `Close` | `exports.esx_context:Close()` | Closes and calls `onClose`. | | `Refresh` | `exports.esx_context:Refresh(elements, position)` | Updates the open menu. | | `focusPreview` | `exports.esx_context:focusPreview()` | Gives focus to a previewed menu (same as the preview key). | ## State bag [#state-bag] While a menu is open, `LocalPlayer.state['context:active']` is `true`. Other resources can check it to avoid opening UI on top of the menu: ```lua if LocalPlayer.state['context:active'] then return end ``` ## Controls [#controls] | Key | Action | | --- | --- | | Mouse click | Select an element | | Escape / Backspace | Close the menu (ignored while typing in an input, and when `canClose` is `false`) | | Left Alt (`previewContext`) | Focus a menu opened with `ESX.PreviewContext` | ## Commands [#commands] | Command | Description | | --- | --- | | `previewContext` | Bound to `PREVIEW_KEYBIND`; focuses a previewed menu. | | `ctx:preview`, `ctx:open`, `ctx:close`, `ctx:form` | Debug commands that open sample menus. Only registered when `Config.EnableDebug = true` in es_extended. | ## Common issues [#common-issues] - **`ESX.OpenContext` returns `false` and nothing shows**: `esx_context` is not started, or it starts after the resource calling it. Ensure it in `server.cfg` after `es_extended`. - **Argument order**: `onSelect` and `onClose` come before `canClose`, and `RefreshContext` takes `elements` first. Older snippets get both wrong. - **Clicking an input row does nothing**: by design. Add a separate submit row. - **HTML in titles disappears**: `ESX.OpenContext` strips tags on purpose. - **Menu stays open after selecting**: selecting does not close the menu. Call `ESX.CloseContext()` in `onSelect` when you are done. - **Mouse cursor stuck after a script error**: if your `onSelect` errors before closing, focus stays on the UI. Press Escape, or wrap risky code and always call `ESX.CloseContext()`. ## Related [#related] - [Client UI functions](/docs/es_extended/client/ui) - [esx_menu_default](/docs/core/esx_menu_default) for keyboard-driven list menus - [esx_menu_dialog](/docs/core/esx_menu_dialog) for single text input - [Core resources overview](/docs/core) # esx_identity (/docs/core/esx_identity) `esx_identity` is the ESX Legacy resource that asks a new player for their character's first name, last name, date of birth, sex and height, validates the input on the server and stores it in the `users` table. Use it on every ESX server; both the single-character flow and [esx_multicharacter](/docs/core/esx_multicharacter) depend on it. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_identity` | | Version | 1.16.3 (fxmanifest, ships with ESX Legacy 1.16.4) | | Side | Client + server, NUI form built with Vue | | Dependencies | `es_extended`, `esx_lib`, `oxmysql`, [esx_skin](/docs/core/esx_skin) (opened after registration) | | Database | Adds `firstname`, `lastname`, `dateofbirth`, `sex`, `height` to `users` | ## Installation [#installation] Run the bundled SQL once if your `users` table does not have the identity columns yet (the official txAdmin recipe already includes them): ```sql title="esx_identity.sql" ALTER TABLE `users` ADD COLUMN `firstname` VARCHAR(16) NULL DEFAULT NULL, ADD COLUMN `lastname` VARCHAR(16) NULL DEFAULT NULL, ADD COLUMN `dateofbirth` VARCHAR(10) NULL DEFAULT NULL, ADD COLUMN `sex` VARCHAR(1) NULL DEFAULT NULL, ADD COLUMN `height` INT NULL DEFAULT NULL; ``` Then ensure it after `es_extended` and `esx_lib`: ```bash title="server.cfg" ensure esx_lib ensure es_extended ensure esx_identity ``` ## How it works [#how-it-works] <Steps> <Step> ### Player connects [#player-connects] Without multicharacter, the server reads the identity columns during `playerConnecting`. With multicharacter, [esx_multicharacter](/docs/core/esx_multicharacter) triggers the form when the player picks an empty slot. </Step> <Step> ### Form or restore [#form-or-restore] If `firstname` is empty, the client receives `esx_identity:showRegisterIdentity` and opens the form. Otherwise the stored identity is applied to the xPlayer and the client gets `esx_identity:alreadyRegistered`. </Step> <Step> ### Server-side validation [#server-side-validation] The `esx_identity:registerIdentity` callback re-checks every field (see [Validation rules](#validation-rules)), formats the names (`jOHN` becomes `John`), saves the row and pushes the data to the client. </Step> <Step> ### Skin creation [#skin-creation] Without multicharacter, the client fires `esx_skin:playerRegistered`, which opens the [esx_skin](/docs/core/esx_skin) creator for players without a saved skin. </Step> </Steps> ## Configuration [#configuration] All keys live in `config.lua`. | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `ESX.GetConfig().Locale` | Language of notifications and the form. | | `Config.EnableCommands` | `ESX.GetConfig().EnableDebug` | Registers `/char` and `/chardel`. | | `Config.DateFormat` | `"DD/MM/YYYY"` | Display and storage format. Choices: `DD/MM/YYYY`, `MM/DD/YYYY`, `YYYY/MM/DD`. See [Common issues](#common-issues). | | `Config.MaxNameLength` | `20` | Names must be shorter than this (byte length). | | `Config.MinHeight` | `120` | Minimum height in centimeters. | | `Config.MaxHeight` | `220` | Maximum height in centimeters. | | `Config.MaxAge` | `100` | Oldest allowed age. The youngest allowed age is fixed at 18. | | `Config.FullCharDelete` | `true` | `/chardel` also resets `bank_savings` and `caution` addon accounts and the `user_ears`, `user_glasses`, `user_helmet`, `user_mask` datastores. | | `Config.EnableDebugging` | `ESX.GetConfig().EnableDebug` | Registers the `xPlayerGet*` debug commands. | ## Validation rules [#validation-rules] The server never trusts the NUI. A registration is rejected with an error notification when: | Field | Rule | | --- | --- | | `firstname`, `lastname` | String, valid locale characters, length between 1 and `MaxNameLength - 1`. | | `sex` | `m`, `M`, `f` or `F` (stored lowercase). | | `dateofbirth` | Matches `DD/MM/YYYY`, is a real calendar date, year between `currentYear - MaxAge` and `currentYear - 18`. | | `height` | Number between `MinHeight` and `MaxHeight`. | A player that is already registered cannot register again (`already_registered`). ## Reading identity data [#reading-identity-data] After loading, the identity is stored on the xPlayer and replicated to the player's state bag. <Tabs items={['Server', 'Client']}> <Tab value="Server"> ```lua title="server/main.lua" local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return end local fullName = xPlayer.getName() -- "John Doe" local firstName = xPlayer.get('firstName') local lastName = xPlayer.get('lastName') local dob = xPlayer.get('dateofbirth') -- "24/12/1995" local sex = xPlayer.get('sex') -- "m" or "f" local height = xPlayer.get('height') -- 180 -- also available to any resource via state bags local state = Player(source).state print(state.firstName, state.lastName) ``` </Tab> <Tab value="Client"> ```lua title="client/main.lua" local data = ESX.GetPlayerData() print(data.firstName, data.lastName, data.dateofbirth, data.sex, data.height) -- or the replicated state bag print(LocalPlayer.state.name) ``` </Tab> </Tabs> ## Events [#events] ### esx_identity:completedRegistration [#esx_identitycompletedregistration] Server event fired by esx_identity when a player finishes the form **in multicharacter mode** only. esx_multicharacter listens to it to create the new character. In single-character mode the identity is saved directly and this event is not fired. ```lua AddEventHandler('esx_identity:completedRegistration', function(source, data) end) ``` | Name | Type | Description | | --- | --- | --- | | `source` | `number` | Player server ID. | | `data.firstname` | `string` | Formatted first name. | | `data.lastname` | `string` | Formatted last name. | | `data.dateofbirth` | `string` | Date in `Config.DateFormat`. | | `data.sex` | `string` | `"m"` or `"f"`. | | `data.height` | `number` | Height in centimeters. | ```lua title="server/main.lua" AddEventHandler('esx_identity:completedRegistration', function(source, data) print(('Player %s registered as %s %s'):format(source, data.firstname, data.lastname)) end) ``` ### esx_identity:showRegisterIdentity [#esx_identityshowregisteridentity] Client event that opens the registration form. It waits for the NUI and the loading screen, then opens the form unless the player is dead. It also triggers `esx_skin:resetFirstSpawn`. It is registered as a net event, so the server can send it: ```lua title="server/main.lua" TriggerClientEvent('esx_identity:showRegisterIdentity', playerId) ``` <Callout type="warn"> Opening the form for a player that is already registered does nothing useful: the server rejects the submission with "You have already registered." Clear the identity first (for example with `/chardel`). </Callout> ### Internal events [#internal-events] | Event | Side | Description | | --- | --- | --- | | `esx_identity:setPlayerData` | Client (secure net event) | Server pushes `firstName`, `lastName`, `dateOfBirth`, `sex`, `height` into `ESX.PlayerData`. | | `esx_identity:alreadyRegistered` | Client (secure net event) | Sent to registered players; fires `esx_skin:playerRegistered` once the loading screen is gone. | | `esx_identity:registerIdentity` | Server callback | Receives the form payload. Do not call it from other resources. | ## Commands [#commands] | Command | Permission | Enabled by | Description | | --- | --- | --- | --- | | `/char` | `user` | `Config.EnableCommands` | Shows your active character name. | | `/chardel` | `admin` | `Config.EnableCommands` | Wipes your identity and skin and reopens the form. | | `/xPlayerGetFirstName` | `user` | `Config.EnableDebugging` | Prints `xPlayer.get('firstName')`. | | `/xPlayerGetLastName` | `user` | `Config.EnableDebugging` | Prints `xPlayer.get('lastName')`. | | `/xPlayerGetFullName` | `user` | `Config.EnableDebugging` | Prints `xPlayer.getName()`. | | `/xPlayerGetSex` | `user` | `Config.EnableDebugging` | Prints `xPlayer.get('sex')`. | | `/xPlayerGetDOB` | `user` | `Config.EnableDebugging` | Prints `xPlayer.get('dateofbirth')`. | | `/xPlayerGetHeight` | `user` | `Config.EnableDebugging` | Prints `xPlayer.get('height')`. | ## Common issues [#common-issues] - **Registration always fails with "Invalid Format (DOB)".** The form sends the date in `Config.DateFormat`, but the server validator only parses `DD/MM/YYYY`. With `YYYY/MM/DD` every submission fails, and with `MM/DD/YYYY` dates with a day above 12 fail. Keep the default format. - **Long names get cut or the query errors.** `MaxNameLength` is 20, but the SQL columns are `VARCHAR(16)`. Either lower `Config.MaxNameLength` to `17` or widen the columns. - **Form never opens.** The client waits for both the NUI `ready` message and `esx:loadingScreenOff`. A custom loading screen that never shuts down blocks the form. Check the F8 console for "Waiting for esx_identity NUI..". - **"identifier-missing" on connect.** The player has no license identifier (for example `sv_lan`). Fix the identifier setup in `es_extended`. - **Height notification says inches.** The English debug string reads "Inches", but the value is stored in centimeters. ## Related [#related] - [esx_multicharacter](/docs/core/esx_multicharacter) - [esx_skin](/docs/core/esx_skin) - [xPlayer functions](/docs/es_extended/server/xplayer) - [Player data](/docs/es_extended/playerdata) - [Database schema](/docs/guides/database-schema) # esx_inventory (/docs/core/esx_inventory) `esx_inventory` is the default slot-based inventory UI for ESX Legacy. It opens with `F2`, shows items, money and weapons from es_extended, and offers a small server API so other resources can show a second "storage" panel (stashes, society stock, trunks). es_extended remains the authority on items and weight; this resource is the presentation layer. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_inventory` | | Side | Client UI (React NUI) + server storage bridge | | Dependencies | `es_extended`, `esx_lib` | | Database | None (slot layout is stored in client KVP `esx_inventory:slots`) | | Version | 1.16.3 (shipped with ESX Legacy 1.16.4) | Features verified in the source: - Slot grid with drag and drop. The layout is saved per player with `SetResourceKvp`. - Use, give to a nearby player and drop items, routed through the validated es_extended events (`esx:useItem`, `esx:giveInventoryItem`, `esx:removeInventoryItem`). - Give the ammo of a selected weapon to a nearby player. - Cash, black money and weapons shown next to items. - Hotbar styling on the first slots with keybinds that use the item in that slot. - Toasts for gained and lost items, even while the inventory is closed. - Optional second panel for storages registered by other resources. <Callout type="warn" title="Not compatible with ox_inventory"> es_extended checks `GetResourceState("ox_inventory") ~= "missing"` and sets `Config.CustomInventory = "ox"`, which disables `EnableDefaultInventory`. esx_inventory then stops with `ESX Default Inventory is disabled in config, please enable it to use this resource.` If you use ox_inventory, do not ensure esx_inventory. See [ox_inventory with ESX](/docs/guides/ox-inventory). </Callout> ## Installation [#installation] esx_inventory is part of the `[core]` folder. Start it after its dependencies: ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ensure esx_inventory ``` The UI ships prebuilt in `web/dist`. You only need to rebuild it if you change `web/src`: ```bash cd esx_inventory/web pnpm install pnpm build ``` ## Configuration [#configuration] All keys live in `config/main.lua`. | Key | Default | Description | | --- | --- | --- | | `Locale` | `GetConvar("esx:locale", "en")` | Language used for UI strings and notifications. | | `PlayerSlots` | `25` | Number of slots in the player grid. Slot indexes saved in KVP are clamped to this range. | | `HotbarSlots` | `5` | Number of first slots styled as hotbar. One keybind is registered per hotbar slot. | | `ItemImageUrl` | `"nui://esx_inventory/web/images/%s.png"` | Image URL template. `%s` is replaced with the item, account or weapon name. | | `NearbyPlayerDistance` | `3.0` | Max distance (meters) for a player to appear in the "give" target list. | | `StorageBurst` | `5` | Rate limiter capacity for storage put/take actions per player. | | `StorageRefillInterval` | `1000` | Interval (ms) after which the storage rate limiter refills `StorageBurst` tokens. | ### Theme convars [#theme-convars] The panel reads the shared ESX UI convars through `xLib.colors.getESXTheme()`. Set them with `setr` so the client can read them: ```bash title="server.cfg" setr esx:ui:primaryColor "#fb9b04" setr esx:ui:logoUrl "https://example.com/logo.png" ``` Supported convars: `esx:ui:primaryColor`, `esx:ui:secondaryColor`, `esx:ui:backgroundColor`, `esx:ui:accentColor`, `esx:ui:logoUrl`. ### Item images [#item-images] Put `itemname.png` files (for example `bread.png`, `WEAPON_PISTOL.png`, `money.png`) into `web/images/`. Missing images fall back to an icon. To use a CDN, change `ItemImageUrl` to something like `"https://cdn.example.com/items/%s.png"`. ## Keybinds [#keybinds] Keybinds are registered with `xLib.addKeybind`, so players can rebind them in **Settings > Key Bindings > FiveM**. | Name | Default key | Action | | --- | --- | --- | | `showinv` | `F2` | Open or close the inventory. | | `hotbar1` ... `hotbar5` | `1` ... `5` | Use the item in hotbar slot 1-5. Weapons are equipped or holstered. | While a hotbar key is held, the matching GTA weapon-wheel shortcut control is disabled so the key does not also switch weapons. The inventory does not open while the player is dead, the pause menu is active, or the player is not loaded yet. ## Client exports [#client-exports] ### ShowInventory [#showinventory] ```lua exports.esx_inventory:ShowInventory() ``` Opens the inventory for the local player. Does nothing if it is already open, the NUI is not ready, the player is dead or the pause menu is open. ```lua title="client/main.lua" RegisterCommand('inv', function() exports.esx_inventory:ShowInventory() end, false) ``` ## Server exports (storage API) [#server-exports-storage-api] A storage is a second panel next to the player inventory. The registering resource owns the data; esx_inventory handles the player side (remove before put, carry check before and after take, refund on failure) and runs one transaction at a time per player. ### RegisterStorage [#registerstorage] ```lua exports.esx_inventory:RegisterStorage(storageId, definition) -- returns boolean ``` | Name | Type | Description | | --- | --- | --- | | `storageId` | `string` | Unique, non-empty storage id. | | `definition.label` | `string` | Title shown on the panel. Required. | | `definition.canAccess` | `fun(xPlayer): boolean` | Access check. Required. | | `definition.getItems` | `fun(xPlayer): table[]` | Returns `{ name, count, label?, weight? }` entries. Required. | | `definition.putItem` | `fun(xPlayer, itemName, count): boolean` | Adds to your storage. Return `true` only after the state changed. Required. | | `definition.takeItem` | `fun(xPlayer, itemName, count): boolean` | Removes from your storage. Return `true` only after the state changed. Required. | | `definition.slots` | `number?` | Max distinct items. Default `30`. | | `definition.maxWeight` | `number?` | Max total weight, `0` disables the check. Default `0`. Weights come from the es_extended item registry. | | `definition.coords` | `vector3?` | Optional location. Transfers are refused when the player is too far away. | | `definition.distance` | `number?` | Allowed distance from `coords`. Default `5.0`. | Returns `false` when the id is invalid, a callback is missing, or another resource already owns the id. The owning resource may call it again to replace its definition; open viewers are refreshed. Only `item_standard` entries are supported in storages: money and weapons cannot be stored through this API. <Callout type="warn" title="Callbacks must not yield"> The callbacks run synchronously in your resource. Do not call `MySQL.query.await`, `Wait` or other yielding functions inside them. Keep the storage contents in memory and persist them separately. </Callout> ```lua title="server/storage.lua" local stock = {} -- [itemName] = count, load it from your database on start local function toList() local list = {} for name, count in pairs(stock) do list[#list + 1] = { name = name, count = count } end return list end local function registerStorages() exports.esx_inventory:RegisterStorage('society_mechanic', { label = 'Mechanic Stock', slots = 30, maxWeight = 200, coords = vector3(-342.29, -133.37, 39.0), distance = 5.0, canAccess = function(xPlayer) return xPlayer.getJob().name == 'mechanic' end, getItems = function() return toList() end, putItem = function(_, itemName, count) stock[itemName] = (stock[itemName] or 0) + count return true end, takeItem = function(_, itemName, count) local current = stock[itemName] or 0 if current < count then return false end stock[itemName] = current - count > 0 and current - count or nil return true end, }) end AddEventHandler('esx_inventory:ready', registerStorages) if GetResourceState('esx_inventory') == 'started' then registerStorages() end ``` ### OpenStorage [#openstorage] ```lua exports.esx_inventory:OpenStorage(playerId, storageId) -- returns boolean ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | `number` | Server id of the player. | | `storageId` | `string` | Id passed to `RegisterStorage`. | Returns `false` if the storage does not exist, the player is not loaded, out of range, `canAccess` returns false, or `getItems` errors. The same function is also bound to the **local server event** `esx_inventory:openStorage`, so `TriggerEvent('esx_inventory:openStorage', playerId, storageId)` works from other server scripts. ```lua title="server/main.lua" RegisterNetEvent('mechanic:openStock', function() local src = source if not exports.esx_inventory:OpenStorage(src, 'society_mechanic') then local xPlayer = ESX.GetPlayerFromId(src) if xPlayer then xPlayer.showNotification('You cannot open this storage.') end end end) ``` ### RefreshStorage [#refreshstorage] ```lua exports.esx_inventory:RefreshStorage(storageId) -- returns boolean ``` Pushes the current `getItems` result to everyone viewing the storage. Call it after you change the contents outside of `putItem` / `takeItem` (for example from an admin command). Viewers who lost access or moved out of range get the panel closed. ### UnregisterStorage [#unregisterstorage] ```lua exports.esx_inventory:UnregisterStorage(storageId) -- returns boolean ``` Removes a storage and closes it for every viewer. Storages are also unregistered automatically when the owning resource stops. ## Events [#events] | Event | Side | Type | Description | | --- | --- | --- | --- | | `esx_inventory:ready` | Server | Local | Fired when esx_inventory starts. Register your storages again here. | | `esx_inventory:openStorage` | Server | Local | `(playerId, storageId)`, same as `OpenStorage`. | | `esx_inventory:storagePut` | Server | Net (internal) | Sent by the UI. Rate limited and validated. | | `esx_inventory:storageTake` | Server | Net (internal) | Sent by the UI. Rate limited and validated. | | `esx_inventory:storageClosed` | Server | Net (internal) | Sent when the player closes the storage panel. | | `esx_inventory:openStorage` / `refreshStorage` / `closeStorage` | Client | Net (internal) | Sent by the server to drive the storage panel. | Do not trigger the internal net events yourself; use the exports. The UI refreshes itself on the es_extended events `esx:setInventory`, `esx:addInventoryItem`, `esx:removeInventoryItem`, `esx:addLoadoutItem`, `esx:removeLoadoutItem`, `esx:setAccountMoney` and `esx:playerLoaded`, and closes on `esx:onPlayerDeath`. ## Common issues [#common-issues] <Accordions> <Accordion title="Error: ESX Default Inventory is disabled in config"> `Config.CustomInventory` is not `false` in es_extended. Most often `ox_inventory` exists in your resources folder (even if not started, the check is `~= "missing"`). Remove it or stop using esx_inventory. </Accordion> <Accordion title="Pressing 1-5 uses items instead of switching weapons"> The hotbar keybinds default to `1`-`5` and suppress the weapon-wheel shortcut while held. Players can unbind or rebind them in the FiveM key binding settings. </Accordion> <Accordion title="Storage does not reappear after restarting esx_inventory"> Storages live in memory of esx_inventory. Register them in an `esx_inventory:ready` handler as shown above so they come back after a restart. </Accordion> <Accordion title="Put or take silently fails"> `putItem`/`takeItem` must return exactly `true`. Any other value, or an error, counts as failure and the player's items are refunded. Also check `slots`, `maxWeight`, `coords`/`distance` and the rate limit (`StorageBurst` actions per `StorageRefillInterval`). </Accordion> </Accordions> ## Related [#related] - [Core resources overview](/docs/core) - [esx_lib](/docs/core/esx_lib) - [Adding items](/docs/guides/add-items) - [ox_inventory with ESX](/docs/guides/ox-inventory) - [es_extended config](/docs/es_extended/config) # esx_lib (/docs/core/esx_lib) esx_lib is the official ESX utility library. It exposes a global `xLib` table that lazily loads modules (callbacks, player cache, points, NUI helpers, streaming, validation, rate limiting and more) into any resource that includes `@esx_lib/imports.lua`. es_extended itself is built on top of it, so every ESX Legacy server needs esx_lib started first. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource name | `esx_lib` (the loader hardcodes this name, do not rename the folder) | | Version | `1.0.0` (`legacyversion '1.16.3'` in fxmanifest) | | Side | Shared (client and server modules) | | Dependencies | None. `oxmysql` is only needed by the server-side `schema` and catalog modules, which run inside es_extended. | | Used by | es_extended, esx_identity, esx_inventory, esx_menu_default, esx_menu_dialog, esx_multicharacter, esx_progressbar, esx_skin, esx_whitelist | | Global | `xLib` (with fields `name`, `side`, `debug`) | Parts of esx_lib are adapted from [ox_lib](https://github.com/overextended/ox_lib) by Linden (LGPL-3.0). Those files carry the original notice in the source. ## Installation and load order [#installation-and-load-order] esx_lib ships in the `[core]` folder of esx_core. Start it before es_extended: ```bash title="server.cfg" ensure oxmysql ensure esx_lib ensure es_extended ``` To use xLib in your own resource, add the import as a shared script and declare the dependency: ```lua title="fxmanifest.lua" fx_version 'cerulean' game 'gta5' lua54 'yes' shared_scripts { '@esx_lib/imports.lua', '@es_extended/imports.lua', } client_scripts { 'client/*.lua' } server_scripts { 'server/*.lua' } dependencies { 'es_extended', 'esx_lib', } ``` ### How lazy loading works [#how-lazy-loading-works] `imports.lua` creates the `xLib` table with an `__index` metamethod. The first time you read `xLib.<name>`, it loads `imports/<name>/shared.lua` plus `imports/<name>/client.lua` or `imports/<name>/server.lua` from esx_lib and runs them inside your resource. Modules you never touch are never loaded. If no module folder exists for a name, xLib returns a proxy that calls `exports.esx_lib:<name>(...)`. This is how functions that must live in one central place (callback validation, pub/sub registry, `triggerClientEvent`) are shared by every resource. <Callout type="warn" title="Global require is replaced"> `imports.lua` sets `require = xLib.require` in your resource. Plain `require('module')` therefore loads files from your resource through `LoadResourceFile`, not from the standard Lua package path. See [require](#require-load-and-loadjson). </Callout> ## Module summary [#module-summary] | Module | Side | Purpose | | --- | --- | --- | | `addKeybind` | Client | Register key mappings with press and release handlers | | `blips` | Client | Create and remove map blips from an options table | | `cache` | Client | Cached `ped`, `vehicle`, `seat`, `weapon`, `playerId`, `serverId`, `coords` | | `callback` | Client, Server | Client/server callbacks with validation, timeouts and rate limits | | `class` | Shared | Minimal class system with `constructor` and `new` | | `colors` | Shared | ESX brand colors and UI theme from convars | | `dui` | Client | DUI (browser texture) class | | `entity` | Client | Closest entity and distance helpers, teleport | | `game` | Client | Spawn and delete vehicles and objects, closest player, ped mugshots | | `interactions` | Client | Shared "Interact" key (`E` by default) with conditional handlers | | `isEnhanced` | Shared | Detects the GTA V Enhanced build | | `markerZone` | Client | Marker plus interaction zone built on `points` | | `math` | Shared | Clamp, rounding, conversion, lerp, color and heading helpers | | `medal` | Client | Trigger a Medal.tv clip through the local Medal client | | `nui` | Client | `SendNUIMessage`, focus and NUI callback helpers | | `onesync` | Server | Players and entities in an area (server-side OneSync) | | `overload` | Shared | Type-based function overloading | | `player` | Server | Player ped, coords and distance checks | | `point` / `points` | Client | Proximity points with `enter`, `leave` and `inside` | | `pubsub` | Client, Server | Server-driven topic subscriptions | | `rateLimiter` | Server | Token bucket rate limiter | | `raycast` | Client | Raycasts from the camera | | `require` | Shared | Module loader, `xLib.load` and `xLib.loadJson` | | `scaleform` | Client | Freemode messages, breaking news, popup warnings | | `schema` | Server | Database column inspection and safe migrations (oxmysql) | | `server` | Server | Aliases for `player` distance checks | | `streaming` | Client | Request models, dicts, assets with timeout | | `string` | Shared | Case conversion, safe matching, `randomHex`, `uuid` | | `table` | Shared | Array and table helpers | | `timeout` | Shared | Cancellable `setTimeout` and `clearTimeout` | | `token` | Server | Single-use, expiring tokens | | `validation` | Shared, Server | Validate numbers, money, strings and player proximity | | `vehiclePlate` | Shared | Normalize, compare and generate plates | | `verify` | Shared | Runtime type checks | | `waitFor` | Shared | Yield until a function returns a value | The `catalog` folder is not a regular module. es_extended loads `imports/catalog/bootstrap.lua` directly (see [SQL catalog](#sql-catalog)). ## Callbacks [#callbacks] `xLib.callback` is the callback system ESX uses internally. `ESX.TriggerServerCallback` and `ESX.RegisterServerCallback` are compat wrappers around it (`xLib.callback.registerCompat`). For new code, use xLib directly or the ESX wrappers described in [Callbacks](/docs/guides/callbacks). ### xLib.callback.register [#xlibcallbackregister] ```lua xLib.callback.register(name, cb) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Unique callback name (max 200 chars). A name owned by another resource raises an error. | | `cb` | `function` | Server: `function(source, ...)`. Client: `function(...)`. Its return values are sent back. | Errors inside `cb` are printed with a stack trace and the caller receives `false`. ### xLib.callback.await [#xlibcallbackawait] ```lua -- client local ... = xLib.callback.await(name, delay, ...) -- server local ... = xLib.callback.await(name, playerId, ...) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Callback name registered on the other side | | `delay` | `number` or `false` | Client only. Throttle in ms: further calls to the same name within this window return immediately without a request. | | `playerId` | `number` | Server only. Target player | | `...` | `any` | Arguments passed to the handler | Yields the current thread and returns the handler's return values. Rejects with `callback '<name>' does not exist` if nothing is registered, or with a timeout error. ### xLib.callback (with function) [#xlibcallback-with-function] ```lua xLib.callback(name, delayOrPlayerId, cb, ...) ``` Same as `await`, but calls `cb(...)` with the results instead of yielding. Omitting `cb` prints a warning and falls back to await. <Tabs items={['Server', 'Client']}> <Tab value="Server"> ```lua title="server/main.lua" xLib.callback.register('myshop:getBalance', function(source, account) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return false end return xPlayer.getAccount(account).money end) ``` </Tab> <Tab value="Client"> ```lua title="client/main.lua" CreateThread(function() -- throttle: at most one request per second local balance = xLib.callback.await('myshop:getBalance', 1000, 'bank') print(('Bank balance: %s'):format(balance)) end) ``` </Tab> </Tabs> ### xLib.callback.registerCompat [#xlibcallbackregistercompat] ```lua xLib.callback.registerCompat(name, cb, owner) ``` Registers a handler with the legacy ESX signature: server `function(source, cb, ...)`, client `function(cb, ...)`. `owner` is the resource whose stop removes the handler. es_extended uses this for `ESX.RegisterServerCallback` and `ESX.RegisterClientCallback`. ### Callback limits [#callback-limits] Server callbacks are protected by per-player budgets. When a player exceeds them, the request is answered with `false`. All values are server convars. | Convar | Default | Meaning | | --- | --- | --- | | `xLib:callbackTimeout` | falls back to `esx:callbackTimeout`, then 300000 ms | Await timeout. Replicated to clients when set. | | `xLib:callbackBurst` | `60` | Request bucket capacity per player | | `xLib:callbackRate` | `30` | Requests refilled per second | | `xLib:callbackByteBurst` | `262144` | Payload byte bucket per player | | `xLib:callbackByteRate` | `65536` | Bytes refilled per second | | `xLib:callbackMaxPayloadBytes` | `65536` | Max packed size of one request | | `xLib:callbackMaxInFlight` | `8` | Concurrent callbacks per player | | `xLib:callbackMaxGlobalInFlight` | `4096` | Concurrent callbacks server-wide | | `xLib:callbackValidationGrace` | `15000` | Delay before an unknown callback name is rejected (capped at half the timeout) | | `xLib:callbackValidationWindow` | `5000` | Window for counting invalid-name validations | | `xLib:callbackValidationMaxRequests` | `30` | Max invalid-name validations per window | The server-side receiver also gives up after the configured timeout, or 30 seconds when none is set. Payloads are limited to 32 arguments, 2048 entries and 8 levels of nesting. ## Client cache [#client-cache] ```lua local cache = xLib.cache ``` | Key | Type | Description | | --- | --- | --- | | `playerId` | `number` | `PlayerId()` | | `serverId` | `number` | `GetPlayerServerId(PlayerId())`, resolved lazily | | `ped` | `number` | Current player ped | | `vehicle` | `number` or `false` | Current vehicle | | `seat` | `number` or `false` | Seat index (`-1` is the driver) | | `weapon` | `number` or `false` | Selected weapon hash, `false` when unarmed | | `coords` | `vector3` | Read live on every access | esx_lib polls every 100 ms and triggers a local event `xLib:cache:<key>` with `(value, previous)` when `ped`, `vehicle`, `seat` or `weapon` change. Other resources update their copy from that event. ```lua title="client/main.lua" AddEventHandler('xLib:cache:vehicle', function(vehicle, previous) if vehicle then print('Entered vehicle', vehicle, 'seat', xLib.cache.seat) else print('Left vehicle', previous) end end) ``` ## Points and marker zones [#points-and-marker-zones] ### xLib.points.create [#xlibpointscreate] ```lua local handle = xLib.points.create(coords, distance, hidden, enter, leave, inside) ``` | Name | Type | Description | | --- | --- | --- | | `coords` | `vector3` | Point position | | `distance` | `number` | Radius that counts as "nearby" | | `hidden` | `boolean?` | Hidden points never trigger | | `enter` | `function` | Called once when the player enters the radius | | `leave` | `function` | Called once when the player leaves | | `inside` | `function?` | Called every frame while nearby, with the current distance | Also available: `xLib.points.remove(handle)` and `xLib.points.hide(handle, hidden)`. Proximity is scanned every 500 ms; the loop only runs every frame while at least one point with `inside` is active. Points are removed automatically when the creating resource stops. `xLib.point` is the class form: `xLib.point:new({ coords, distance, hidden, enter, leave, inside })` with methods `:delete()` and `:toggle(hidden?)`. Its callbacks receive `self`. ### xLib.markerZone.create [#xlibmarkerzonecreate] ```lua local zone = xLib.markerZone.create(data) ``` | Field | Type | Description | | --- | --- | --- | | `coords` | `vector3` or table | Zone center | | `drawDistance` | `number?` | Marker draw radius (alias `distance`), default `10.0` | | `interactDistance` | `number?` | Interaction radius (aliases `actionDistance`, `enterDistance`), default `1.5` | | `exitDistance` | `number?` | Radius to leave interaction, defaults to `interactDistance` | | `marker` | table or `false` | Marker options, `false` disables drawing | | `hidden` | `boolean?` | Start hidden | | `canDraw` / `canInteract` | `function(distance)?` | Extra conditions | | `onEnter` / `onInside` / `onExit` | `function(distance)?` | Interaction radius handlers | | `onEnterPoint` / `onExitPoint` | `function?` | Draw radius handlers | Returns `{ handle, remove(), hide(hidden?), show() }`, or `nil` when `coords` is invalid. ```lua title="client/main.lua" local zone = xLib.markerZone.create({ coords = vector3(25.7, -1347.3, 29.5), interactDistance = 1.5, onEnter = function() ESX.TextUI('[E] Open shop') end, onExit = function() ESX.HideUI() end, }) ``` ## Interactions and keybinds [#interactions-and-keybinds] ### xLib.addKeybind [#xlibaddkeybind] ```lua local keybind = xLib.addKeybind(data) ``` | Field | Type | Description | | --- | --- | --- | | `name` | `string` | Command name (registered as `+name` / `-name`) | | `description` | `string` | Label in the FiveM key bindings menu | | `defaultMapper` | `string?` | Input mapper, default `keyboard` | | `defaultKey` | `string?` | Default key | | `disabled` | `boolean?` | Start disabled | | `onPressed` / `onReleased` | `function(self)?` | Handlers, skipped while the pause menu is open | The returned object has `currentKey`, `isPressed`, `getCurrentKey()`, `isControlPressed()`, `disable(toggle)` and `remove()`. ### xLib.interactions [#xlibinteractions] esx_lib registers one keybind named `esx_interact` (default `E`) and triggers the local event `xLib:interact` when it is pressed. `xLib.interactions` lets many resources share that key: ```lua xLib.interactions.register(name, onPress, condition) xLib.interactions.remove(name) local key = xLib.interactions.getInteractKey() -- e.g. "E" ``` Every registered interaction whose `condition()` returns true runs `onPress()`. Interactions are removed when their resource stops. ## NUI helpers [#nui-helpers] | Function | Description | | --- | --- | | `xLib.nui.send(message, data?)` | Sends a table as-is, or `{ action = message, data = data }` when `message` is a string | | `xLib.nui.focus(hasFocus, hasCursor?, keepInput?)` | `SetNuiFocus`; cursor defaults to `hasFocus` | | `xLib.nui.open(message?, focus?, cursor?, keepInput?)` | Send and focus (skip focus with `focus = false`) | | `xLib.nui.close(message?)` | Send and drop focus | | `xLib.nui.ok(data?)` / `xLib.nui.fail(error, code?)` | Build `{ ok, data }` / `{ ok = false, error, code }` responses | | `xLib.nui.register(name, handler)` | `RegisterNUICallback` wrapper with error handling | `handler(data, reply)` can return a value (sent as response, `nil` becomes `ok()`), or call `reply(response)` later. Handlers that declare two parameters and return `nil` are treated as deferred; they time out after `xLib:nuiDeferTimeout` ms (default `10000`) and reply `fail('timeout', 'timeout')`. ## Streaming [#streaming] All functions accept an optional `cb`. Without it they yield and return the loaded asset; with it they call `cb(asset)`. Requests time out after `xLib:streamingTimeout` ms (default `10000`). | Function | Returns | | --- | --- | | `xLib.streaming.requestModel(modelHash, cb?)` | Model hash or `nil` | | `xLib.streaming.requestModelWithSpinner(modelHash, message?, cb?)` | Model hash or `nil` | | `xLib.streaming.requestStreamedTextureDict(textureDict, cb?)` | Dict name | | `xLib.streaming.requestNamedPtfxAsset(assetName, cb?)` | Asset name | | `xLib.streaming.requestAnimSet(animSet, cb?)` | Anim set | | `xLib.streaming.requestAnimDict(animDict, cb?)` | Anim dict | | `xLib.streaming.requestWeaponAsset(weaponHash, cb?)` | Weapon hash | | `xLib.streaming.requestAudioBank(bankName, cb?)` | Bank name | ## Game and entity helpers (client) [#game-and-entity-helpers-client] | Function | Description | | --- | --- | | `xLib.game.spawnVehicle(model, coords, heading, cb?, networked?)` | Spawns a vehicle (networked by default). Yields and returns the handle when `cb` is omitted. Errors if `coords` is more than 424 units from the player. | | `xLib.game.spawnLocalVehicle(model, coords, heading, cb?)` | Non-networked vehicle | | `xLib.game.spawnObject(model, coords, cb?, networked?)` | Spawns an object | | `xLib.game.spawnLocalObject(model, coords, cb?)` | Non-networked object | | `xLib.game.deleteVehicle(vehicle)` / `deleteObject(object)` | Deletes the entity | | `xLib.game.isVehicleEmpty(vehicle)` | `true` when no ped is inside | | `xLib.game.getPeds(onlyOtherPeds?)` / `getVehicles()` / `getObjects()` | Entity pools | | `xLib.game.getPlayers(onlyOtherPlayers?, returnKeyValue?, returnPeds?)` | Active players | | `xLib.game.getClosestPlayer(coords?)` / `getClosestPed(coords?, modelFilter?)` / `getClosestObject(coords?, modelFilter?)` / `getClosestVehicle(coords?, modelFilter?)` | Returns `entity, distance` | | `xLib.game.getPlayersInArea(coords, maxDistance)` / `getVehiclesInArea(coords, maxDistance)` | Entities in a radius | | `xLib.game.isSpawnPointClear(coords, maxDistance)` | `true` when no vehicle is in the radius | | `xLib.game.getVehicleInDirection()` | Vehicle in front of the player | | `xLib.game.getVehicleProperties(vehicle)` / `setVehicleProperties(vehicle, props)` | Read and apply vehicle mods, colors, extras | | `xLib.game.getPedMugshot(ped, transparent?)` | Returns headshot handle and txd string | | `xLib.entity.closest(entities, isPlayerEntities, coords?, modelFilter?)` | Closest entity and distance | | `xLib.entity.EnumerateWithinDistance(entities, isPlayerEntities, coords?, maxDistance)` | Entities within distance | | `xLib.entity.Teleport(entity, coords, cb?)` | Teleports and waits for collision | The `ESX.Game.*` client functions in es_extended are direct aliases of these (`ESX.Game.SpawnVehicle = xLib.game.spawnVehicle` and so on). See [ESX.Game](/docs/es_extended/client/modules/game). ## Server player and OneSync helpers [#server-player-and-onesync-helpers] `source` may be a number or numeric string. Functions return `nil` (or `false`) for invalid or disconnected players instead of erroring. | Function | Returns | | --- | --- | | `xLib.player.getPed(source)` | Ped or `nil` | | `xLib.player.getCoords(source)` | `vector3` or `nil` | | `xLib.player.distanceToCoords(source, coords)` | Distance or `nil` | | `xLib.player.isNearCoords(source, coords, distance)` | `nearby, actualDistance` | | `xLib.player.distanceToPlayer(source, target)` | Distance or `nil` | | `xLib.player.isNearPlayer(source, target, distance)` | `nearby, actualDistance` | | `xLib.player.findNearbyCoords(source, coordsList, distance)` | `coords, index, actualDistance` | | `xLib.player.isNearAnyCoords(source, coordsList, distance)` | `nearby, index, actualDistance` | | `xLib.onesync.getPlayersInArea(source, maxDistance, ignore?, routingBucket?)` | Players near a player id or `vector3` | | `xLib.onesync.getClosestPlayer(source, maxDistance, ignore?, routingBucket?)` | Closest player | | `xLib.onesync.getPedsInArea` / `getObjectsInArea` / `getVehiclesInArea(coords, maxDistance, modelFilter?)` | Entities in area | | `xLib.onesync.getClosestPed` / `getClosestObject` / `getClosestVehicle(coords, modelFilter?)` | `entity, distance, coords` | `xLib.server.*` (`getPlayerCoords`, `distanceToCoords`, `isPlayerNearCoords`, `distanceToPlayer`, `isPlayerNearPlayer`, `isPlayerNearAnyCoords`) and `xLib.validation.playerNearCoords` / `playerNearPlayer` are aliases of the same checks. Use them to validate client requests, for example that a player is actually standing at a shop before you charge them: ```lua title="server/main.lua" local SHOP <const> = vector3(25.7, -1347.3, 29.5) xLib.callback.register('myshop:buy', function(source, item) if not xLib.player.isNearCoords(source, SHOP, 5.0) then return false end -- process purchase return true end) ``` ## Validation and type checks [#validation-and-type-checks] ### xLib.validation (shared) [#xlibvalidation-shared] | Function | Returns | | --- | --- | | `xLib.validation.number(value, min?, max?, round?)` | Finite number in range, else `nil` | | `xLib.validation.integer(value, min?, max?, mode?)` | Integer (`mode`: `round`, `floor`, `ceil`), else `nil` | | `xLib.validation.count(value, max?)` | Positive item count, else `nil` | | `xLib.validation.money(value, min?, max?)` | Rounded non-negative amount, else `nil` | | `xLib.validation.string(value, options?)` | Trimmed string, else `nil`. Options: `trim`, `allowEmpty`, `minLength`, `maxLength`, `pattern` | | `xLib.validation.oneOf(value, values)` | `true` when `value` is in the array | | `xLib.validation.player(source)` | Server only. `true` for a connected player | ```lua title="server/main.lua" RegisterNetEvent('myshop:deposit', function(amount) local value = xLib.validation.money(amount, 1, 1000000) if not value then return end local xPlayer = ESX.GetPlayerFromId(source) if xPlayer and xPlayer.getMoney() >= value then xPlayer.removeMoney(value, 'Shop deposit') xPlayer.addAccountMoney('bank', value, 'Shop deposit') end end) ``` ### xLib.verify [#xlibverify] ```lua local ok = xLib.verify(value, types, throwError) ``` Checks `value` against one type or an array of types and returns a boolean, or raises an error when `throwError` is `true`. Supported types: Lua types plus `array`, `int`, `uint`, `float`, `char`, `vector3`, `vector4`, `ped`, `vehicle`, `prop`, `playerId`, `class` and `model`. On the server, `model` asks a random connected client to validate the model through the `xLib:validateModel` callback. `xLib.verify.debug(...)` only runs when the `xLib:debug` convar is `true`. ## Rate limiter and tokens (server) [#rate-limiter-and-tokens-server] ### xLib.rateLimiter [#xlibratelimiter] ```lua local limiter = xLib.rateLimiter(options) ``` | Option | Type | Description | | --- | --- | --- | | `capacity` | `number` | Max tokens per key | | `refill` | `number` | Tokens restored per interval | | `interval` | `number` | Interval in ms | | `maxEntries` | `number?` | Max tracked keys (convar `xLib:rateLimiterMaxEntries`, default 4096) | | `staleMs` | `number?` | Idle time before a full bucket is pruned | | `pruneInterval` | `number?` | Min time between prunes | Methods: `limiter:consume(key, cost?)` returns `allowed, retryAfterMs`; `limiter:retryAfter(key, cost?)`, `limiter:reset(key)`, `limiter:clear()`, `limiter:size()`. Buckets keyed by a player id are reset on `playerDropped`. ```lua title="server/main.lua" local limiter = xLib.rateLimiter({ capacity = 5, refill = 1, interval = 2000 }) RegisterNetEvent('myjob:search', function() local allowed, retryAfter = limiter:consume(source) if not allowed then return TriggerClientEvent('esx:showNotification', source, ('Wait %d s'):format(math.ceil(retryAfter / 1000)), 'error') end -- handle search end) ``` ### xLib.token [#xlibtoken] | Function | Description | | --- | --- | | `xLib.token.create({ source?, ttl, data? })` | Returns a token id. `ttl` in ms (default 30000). Max entries via `xLib:tokenMaxEntries` (default 4096). | | `xLib.token.consume(id, source?)` | Returns `data` once; the token is deleted. Fails if `source` does not match. | | `xLib.token.remove(id)` | Deletes a token | | `xLib.token.reset(source)` | Deletes all tokens of a player, returns count | | `xLib.token.clear()` / `xLib.token.size()` | Clear all / count | Tokens of a player are removed on `playerDropped`. ## Pub/sub [#pubsub] The server subscribes players to topics and publishes data only to them. Clients can only listen. Topic names must match `^[%w_:%.%-]+$` (max 200 chars); prefix them with your resource name. | Function | Side | Description | | --- | --- | --- | | `xLib.pubsub.subscribe(src, topic)` | Server | Returns `true` if added | | `xLib.pubsub.unsubscribe(src, topic)` | Server | Returns `true` if removed | | `xLib.pubsub.publish(topic, data)` | Server | Sends to all subscribers, returns the count | | `xLib.pubsub.subscribers(topic)` | Server | Array of server ids | | `xLib.pubsub.isSubscribed(src, topic)` | Server | Boolean | | `xLib.pubsub.clear(topic)` | Server | Drops the topic, returns removed count | | `xLib.pubsub.on(topic, handler)` | Client | `handler(data, topic)` | | `xLib.pubsub.off(topic, handler?)` | Client | Removes one or all handlers | Players are removed from all topics on disconnect, and topics are cleared when the resource that created them stops. <Tabs items={['Server', 'Client']}> <Tab value="Server"> ```lua title="server/main.lua" local TOPIC <const> = 'myshop:247:stock' RegisterNetEvent('myshop:openShop', function() xLib.pubsub.subscribe(source, TOPIC) end) local function updateStock(stock) xLib.pubsub.publish(TOPIC, stock) end ``` </Tab> <Tab value="Client"> ```lua title="client/main.lua" xLib.pubsub.on('myshop:247:stock', function(stock) print(('Stock updated: %d items'):format(#stock)) end) ``` </Tab> </Tabs> ## Shared utilities [#shared-utilities] ### Table, string and math [#table-string-and-math] `xLib.table`: `isArray`, `searchForKey`, `filter`, `deepcopy`, `merge`, `dump`, `sizeOf`, `set`, `indexOf`, `lastIndexOf`, `find`, `findIndex`, `map`, `reverse`, `clone`, `concat`, `join`, `contains`, `sort` (sorted `pairs` iterator), `toArray`, `wipe`. The `ESX.Table.*` functions are direct aliases, see [ESX.Table](/docs/es_extended/shared/table). `xLib.string`: `normalize`, `capitalize`, `toSnake`, `toCamel`, `toPascal`, `escapePattern`, `matchSafe`, `before`, `after`, `contains`, `replace`, `randomHex(length)`, `uuid()`. `xLib.math`: `clamp`, `toNumber`, `tryNumber`, `toInteger`, `toScalars`, `toVector`, `normalToRotation`, `toRGBA`, `hexToRGBA`, `toHex`, `groupDigits`, `round`, `interpolate`, `lerp`, `inverseLerp`, `remap`, plus the ESX-style `Round`, `GroupDigits`, `Trim`, `Random`, `GetHeadingFromCoords` used by [ESX.Math](/docs/es_extended/shared/math). ### Timeout and waitFor [#timeout-and-waitfor] ```lua local id = xLib.timeout.setTimeout(msec, cb) xLib.timeout.clearTimeout(id) local value = xLib.waitFor(cb, errMessage, timeout, interval) ``` `waitFor` calls `cb` until it returns a non-nil value. It errors after `timeout` ms (default `5000`; pass `false` to wait forever) and polls every `interval` ms (default `0`). `ESX.SetTimeout`, `ESX.ClearTimeout` and `ESX.Await` are aliases. See [Timeout](/docs/es_extended/shared/timeout). ```lua title="client/main.lua" local vehicle = xLib.waitFor(function() local veh = GetVehiclePedIsIn(PlayerPedId(), false) if veh ~= 0 then return veh end end, 'Player never entered a vehicle', 10000) ``` ### Vehicle plates [#vehicle-plates] | Function | Description | | --- | --- | | `xLib.vehiclePlate.normalize(plate, options?)` | Trimmed, normalized plate or `nil`. Options: `maxLength`, `uppercase`, `compact`, `allowEmpty`, `pattern`, `coerce` | | `xLib.vehiclePlate.equals(left, right, options?)` | Compares two plates after normalizing | | `xLib.vehiclePlate.matchesVehicle(vehicle, plate, options?)` | Compares against a vehicle's plate | | `xLib.vehiclePlate.generate(options?)` | Random plate. Options: `prefix`, `suffix`, `letters` (3), `numbers` (3), `useSpace` | | `xLib.vehiclePlate.generateUnique(options?, exists?)` | Retries up to `options.attempts` (30) until `exists(plate)` returns false; reserves the plate for 30 s | | `xLib.vehiclePlate.randomLetters(length)` / `randomNumbers(length)` | Random chunks | ### Other shared modules [#other-shared-modules] - `xLib.class(copy?)` creates a class; instances are made with `Class:new(...)`, which calls `constructor`. - `xLib.overload(name, validTypes, cb, obj?)` registers a typed overload, dispatched by argument types. - `xLib.isEnhanced()` returns `true` on the GTA V Enhanced build (server: `gamename` convar `gta5enhanced` or `gta5_enhanced`). - `xLib.colors` exposes `brand`, `darkest`, `dark`, `mid`, `light`, `lightest` and `getESXTheme(defaults?)`. See [convars](#convars). ### require, load and loadJson [#require-load-and-loadjson] ```lua local config = require 'config.shops' -- loads config/shops.lua from your resource local other = require '@other_resource.data' -- loads data.lua from another resource local result = xLib.load('scripts/once') -- runs a file without caching local data = xLib.loadJson('data/items') -- decodes data/items.json ``` Dots become slashes. Search paths are `./?.lua` and `./?/init.lua`. The file must be listed in the target resource's `files` or scripts so `LoadResourceFile` can read it on the client. Circular requires raise an error. ## Other client modules [#other-client-modules] - `xLib.blips.create(data)`, `xLib.blips.createMany(items, defaults?)`, `xLib.blips.remove(blip)`. `data` can be a `vector3` or a table with `coords` or `entity` or `radius`, plus `sprite`, `display`, `scale`, `color`, `alpha`, `shortRange`, `label`, `route`, `routeColor` and more. - `xLib.dui:new({ url, width, height, debug? })` with `remove()`, `setUrl(url)`, `sendMessage(message)`, `sendMouseMove(x, y)`, `sendMouseDown(button)`, `sendMouseUp(button)`, `sendMouseWheel(deltaX, deltaY)`. DUIs are destroyed when the resource stops. - `xLib.scaleform.showFreemodeMessage(title, msg, sec)`, `showBreakingNews(title, msg, bottom, sec)`, `showPopupWarning(title, msg, bottom, sec)`, `showTrafficMovie(sec)`, plus `xLib.scaleform.utils.requestScaleformMovie(movie)` and `runMethod(scaleform, methodName, returnValue?, ...)`. - `xLib.Raycast.FromScreen(depth, ...)`, `xLib.Raycast.Start(depth, ...)`, `xLib.Raycast:Stop()`, `xLib.Raycast:IsActive()`, `xLib.Raycast.GetShapeTestResult(shape)`. - `xLib.medal.triggerClip(publicKey?, eventName?, clipOptions?)` and `xLib.medal.getConfig()`. ## Server-only modules [#server-only-modules] - `xLib.schema` (needs oxmysql): `column(tableName, columnName)`, `identifier(value)`, `ensureColumn(tableName, columnName, definition)`, `widenVarchar(tableName, columnName, length, column?)`, `ensureProperty()`, `ensureMileage()`. es_extended migrations and esx_multicharacter use it to adjust tables safely. - `xLib.triggerClientEvent(eventName, targets, ...)`: `targets` is one server id or an array. For arrays, the payload is packed once and sent to each client, which is cheaper than calling `TriggerClientEvent` in a loop. ### SQL catalog [#sql-catalog] `imports/catalog/bootstrap.lua` is loaded as a server script by es_extended, esx_identity, esx_multicharacter and esx_skin. On start it reads `imports/catalog/data.json` (catalog version `1.16.0`) and creates missing tables, columns and seed rows (jobs, items, licenses, vehicles and more) for ESX core and official addons, using the locale from `Config.SqlCatalogLocale`, `Config.Locale` or the `esx:locale` convar. Other resources call the es_extended export `EnsureSqlCatalog(name, locale)`, which only lets a resource prepare its own catalog. When done, it triggers the server event `esx:sqlCatalogReady` with the list of touched catalogs. If it fails, resource initialization is blocked and the console prints `SQL catalog FAILED`. ## Exports [#exports] Functions defined in esx_lib's own `resource/` scripts are exported automatically. You normally reach them through xLib, but they are callable as `exports.esx_lib:<name>(...)`: | Export | Side | Description | | --- | --- | --- | | `setValidCallback(name, isValid, owner?)` | Both | Internal callback ownership registry | | `isCallbackValid(name)` | Both | `true` if a callback name is registered | | `acquireCallbackBudget` / `releaseCallbackBudget` / `chargeCallbackBytes` | Server | Internal callback rate budget | | `pubsub_subscribe`, `pubsub_unsubscribe`, `pubsub_publish`, `pubsub_subscribers`, `pubsub_isSubscribed`, `pubsub_clear` | Server | Pub/sub registry | | `triggerClientEvent(eventName, targets, ...)` | Server | Packed multi-target event | | `getMedalConfig()` / `triggerMedalClip(publicKey, eventName, clipOptions)` | Client | Medal.tv integration | ## Events [#events] | Event | Side | Type | Payload | | --- | --- | --- | --- | | `xLib:cache:<key>` | Client | Local | `value, previous` for `ped`, `vehicle`, `seat`, `weapon` | | `xLib:interact` | Client | Local | Fired when the `esx_interact` key is pressed | | `xLib:validateCallback` | Both | Net (internal) | Callback validation | | `esx:sqlCatalogReady` | Server | Local | `catalogs` table, fired by the catalog bootstrap | ## Configuration [#configuration] `config.lua` only configures the Medal.tv integration. esx_lib's NUI page posts clip requests to the Medal desktop client at `http://localhost:12665`, so nothing happens for players who do not run Medal. | Key | Default | Description | | --- | --- | --- | | `Config.Medal.enabled` | `true` | Allow `xLib.medal.triggerClip` | | `Config.Medal.publicKey` | ESX public key | Medal API public key | | `Config.Medal.eventName` | `'Death'` | Default event name | | `Config.Medal.clipOptions.duration` | `30` | Clip length in seconds | | `Config.Medal.clipOptions.captureDelayMs` | `0` | Delay before capture | | `Config.Medal.clipOptions.alertType` | `'Default'` | `Default`, `Disabled`, `SoundOnly` or `OverlayOnly` | ### Convars [#convars] | Convar | Default | Description | | --- | --- | --- | | `xLib:debug` | `false` | Debug output, enables `xLib.verify.debug` | | `xLib:nuiDeferTimeout` | `10000` | Deferred NUI callback timeout (ms) | | `xLib:streamingTimeout` | `10000` | Streaming request timeout (ms) | | `xLib:rateLimiterMaxEntries` | `4096` | Default max keys per rate limiter | | `xLib:tokenMaxEntries` | `4096` | Max active tokens | | `esx:brand-color` | `#FB9B04` | `xLib.colors.brand` | | `esx:darkest-color` / `esx:dark-color` / `esx:mid-color` / `esx:light-color` / `esx:lightest-color` | `#161616` / `#252525` / `#383838` / `#969696` / `#F2F2F2` | Palette | | `esx:ui:primaryColor`, `esx:ui:secondaryColor`, `esx:ui:backgroundColor`, `esx:ui:accentColor`, `esx:ui:logoUrl` | palette values | Theme returned by `xLib.colors.getESXTheme()` | Callback convars are listed in [Callback limits](#callback-limits). ```bash title="server.cfg" setr esx:ui:primaryColor "#3B82F6" set xLib:callbackTimeout 60000 ``` ## Version reporting [#version-reporting] On the server, esx_lib publishes the `version` and `legacyversion` metadata of every ESX resource (names starting with `esx_` or `es_`, or that depend on es_extended or esx_lib) as server info convars named `<resource>-version`. This is what server lists and support tools read to see which ESX version you run. It runs 2 seconds after start and again for each ESX resource that starts later. ## Common issues [#common-issues] - **`attempt to index a nil value (global 'xLib')`**: `@esx_lib/imports.lua` is missing from `shared_scripts`, or esx_lib is not started. - **Renamed folder**: the loader reads files from a resource literally named `esx_lib`. Keep the name. - **`callback '<name>' does not exist`**: the name is not registered on the receiving side, or its resource stopped. On the server this is reported after the validation grace period (up to 15 s). - **`callback '<name>' is already owned by resource '<x>'`**: two resources registered the same callback name. Prefix names with your resource. - **Callback returns `false` under load**: the player hit a callback budget. Check the [callback limits](#callback-limits) and avoid calling server callbacks every frame; use the client `delay` argument. - **`require` cannot find a file on the client**: the file is not in the resource's `files` list, so `LoadResourceFile` cannot read it. - **`SQL catalog FAILED`**: oxmysql is not connected or the database user cannot create or alter tables. ## Related [#related] - [ESX Legacy core resources](/docs/core) - [Callbacks guide](/docs/guides/callbacks) - [ESX shared functions](/docs/es_extended/shared) - [Client callback module](/docs/es_extended/client/modules/callback) - [OneSync server functions](/docs/es_extended/server/onesync) # esx_loadingscreen (/docs/core/esx_loadingscreen) esx_loadingscreen is the loading screen players see while they connect to an ESX Legacy server. It shows a logo, a progress bar driven by the game's real load progress and rotating tips. It has no Lua config; you customize it by editing the files in `web/`. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_loadingscreen` | | Side | Client (loadscreen NUI) | | Dependencies | None | | Config | None, edit `web/script.js`, `web/style.css`, `web/logo.png` | | Required | No, any loading screen resource works | The manifest declares `loadscreen "web/index.html"` and `loadscreen_manual_shutdown "yes"`. Manual shutdown means the game does not close the loading screen on its own; a script has to call `ShutdownLoadingScreenNui()`. ## How it works [#how-it-works] <Steps> <Step> ### Progress [#progress] FiveM sends `loadProgress` messages with a `loadFraction` between 0 and 1 to the page. The bar animates to that value and switches the status text through "Connecting", "Handshaking", "Downloading resources", "Loading scripts", "Spawning player" and "Finalizing". </Step> <Step> ### Completion [#completion] When `loadFraction` reaches 1, the page posts the NUI callback `loadingComplete`. The client script waits 2500 ms (`SHUTDOWN_DELAY`) and calls `ShutdownLoadingScreenNui()`. </Step> <Step> ### Spawn [#spawn] es_extended (without multicharacter) and esx_multicharacter also call `ShutdownLoadingScreen()` and `ShutdownLoadingScreenNui()` when the player spawns or the character selection opens, and fire the client event `esx:loadingScreenOff`. </Step> </Steps> ## Customization [#customization] | What | Where | | --- | --- | | Logo | Replace `web/logo.png` (keep the file name, it is listed in `fxmanifest.lua`) | | Tips | `tips` array in `web/script.js` (rotates every 6000 ms) | | Loading texts | `loadingTexts` array in `web/script.js` | | Status labels | `statuses` array in `web/script.js` | | Colors, layout | `web/style.css` | | Font | Poppins from Google Fonts, linked in `web/index.html` | ```js title="web/script.js" const tips = [ "Read the rules at discord.gg/yourserver", "Press F1 to open the help menu", "Use /report to contact staff", ] ``` <Callout type="info"> The default tips mention features such as F1 menus and `/report` that ESX does not ship. Replace them with tips that are true on your server. </Callout> If you add new files (a background video, extra images), add them to the `files` list in `fxmanifest.lua`, otherwise the client never downloads them. ## Events [#events] | Event | Side | Description | | --- | --- | --- | | `esx:loadingScreenOff` | Client (local, fired by es_extended / esx_multicharacter) | The loading screen was shut down. Use it to show your HUD. | ```lua title="client/main.lua" AddEventHandler("esx:loadingScreenOff", function() -- show your HUD here end) ``` ## Common issues [#common-issues] - **Two loading screens fight each other.** Only start one resource with a `loadscreen` entry. - **Changes do not show up.** Loading screen files are cached by the client. Restart the server and clear the FiveM cache (`%localappdata%/FiveM/FiveM.app/data/cache`) to test. - **Black screen after loading.** The loading screen closed but the player did not spawn. Check es_extended and esx_multicharacter errors in the F8 console. ## Related [#related] - [Core resources overview](/docs/core) - [esx_multicharacter](/docs/core/esx_multicharacter) - [Client events](/docs/es_extended/events/client) - [Troubleshooting](/docs/getting-started/troubleshooting) # esx_menu_default (/docs/core/esx_menu_default) esx_menu_default is the classic ESX list menu: a vertical list of buttons and sliders that players navigate with the arrow keys and Enter. It does not take mouse focus, so the player can keep moving while the menu is open. You open it with `ESX.UI.Menu.Open('default', ...)` from a client script. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_menu_default` | | Side | Client only | | Menu type | `default` | | Dependencies | `es_extended`, `esx_lib` (loaded via `@esx_lib/imports.lua`) | | UI | React build in `web/build`, Font Awesome icons | <Callout type="warn"> The manifest only declares `es_extended` as a dependency, but the resource loads `@esx_lib/imports.lua`. Start `esx_lib` before `esx_menu_default` or the resource fails with `xLib` errors. </Callout> The resource has no `config.lua`. Menus are stacked: when you open a second menu, it is shown on top, and closing it brings back the previous one. ## ESX.UI.Menu.Open [#esxuimenuopen] ```lua ESX.UI.Menu.Open(menuType, namespace, name, data, submit, cancel, change, close) ``` | Name | Type | Description | | --- | --- | --- | | `menuType` | `string` | `'default'` for this resource. | | `namespace` | `string` | Usually `GetCurrentResourceName()`. Menus in this namespace are closed when the resource stops. | | `name` | `string` | Unique menu name inside the namespace. | | `data` | `table` | Menu data, see below. | | `submit` | `function(data, menu)?` | Enter pressed on a usable element. | | `cancel` | `function(data, menu)?` | Backspace pressed. | | `change` | `function(data, menu)?` | Highlight moved or a slider value changed. | | `close` | `function()?` | Runs after `menu.close()`. | Returns the `menu` object (see [Menu object](#menu-object)). ### Menu data [#menu-data] | Field | Type | Description | | --- | --- | --- | | `title` | `string` | Menu header. HTML is rendered. | | `align` | `string?` | `top-left`, `top-right`, `bottom-left`, `bottom-right`, `center`, `left-center` or `right-center`. Defaults to `right-center`. | | `elements` | `table` | Array of [elements](#element-fields). | ### Element fields [#element-fields] | Field | Type | Description | | --- | --- | --- | | `label` | `string` | Text of the row. HTML is rendered. | | `name` | `string?` | Your identifier, used to branch in callbacks. | | `description` | `string?` | Second line below the label. HTML is rendered. | | `icon` | `string` \| `table` | Font Awesome icon name, e.g. `'burger'` (solid set), or `{ 'fab', 'discord' }` for another set. | | `type` | `'slider'?` | Omit for a button. | | `value` | `number?` | Current slider value. For sliders with `options` it is the zero-based index into `options`. | | `min` | `number?` | Slider minimum (default `0`). Ignored when `options` is set. | | `max` | `number?` | Slider maximum (default `0`). Ignored when `options` is set. | | `options` | `string[]?` | Text values for a slider. The slider cycles through them. | | `unselectable` | `boolean?` | Skipped by arrow navigation, Enter does nothing. Good for headers. | | `usable` | `boolean?` | `false` keeps the row selectable but blocks submit, change and slider steps. | | `disableRightArrow` | `boolean?` | Hides the arrow icon on a button. | Sliders wrap around: pressing Right on the maximum jumps to the minimum and vice versa. Any extra fields you add (for example `price`) are passed back to your callbacks unchanged. ## Callback data [#callback-data] `submit` and `change` receive: | Field | Description | | --- | --- | | `data.current` | The highlighted element (a copy with `selected = true`). | | `data.elements` | All elements, each with a `selected` flag. | | `data._namespace`, `data._name` | Menu identifiers. | `cancel` receives only `_namespace` and `_name` in `data`. Before `change` runs, the menu writes each element's `value` and `selected` state back into `menu.data.elements`, so `menu.data` always matches what the player sees. ## Example [#example] ```lua title="client/main.lua" local function openShopMenu() local elements = { { label = 'Food', unselectable = true, icon = 'utensils' }, { label = 'Bread', name = 'bread', price = 5, type = 'slider', value = 1, min = 1, max = 20, icon = 'bread-slice' }, { label = 'Water', name = 'water', price = 3, type = 'slider', value = 1, min = 1, max = 20, icon = 'bottle-water' }, { label = 'Payment', name = 'payment', type = 'slider', value = 0, options = { 'Cash', 'Bank' }, icon = 'wallet' }, { label = 'Close', name = 'close', icon = 'xmark', disableRightArrow = true }, } ESX.UI.Menu.Open('default', GetCurrentResourceName(), 'shop', { title = '24/7 Shop', align = 'top-left', elements = elements, }, function(data, menu) local item = data.current if item.name == 'close' then return menu.close() end if item.name == 'payment' then return end local payment = menu.data.elements[4].value == 0 and 'money' or 'bank' TriggerServerEvent('my_shop:buy', item.name, item.value, payment) end, function(data, menu) menu.close() end) end RegisterCommand('shop', openShopMenu, false) ``` ## Menu object [#menu-object] `ESX.UI.Menu.Open` returns a menu object. The same object is passed as `menu` to every callback. | Member | Description | | --- | --- | | `menu.close()` | Closes this menu and runs the `close` callback. | | `menu.refresh()` | Re-sends `menu.data` to the UI. Call it after changing data. | | `menu.update(query, newData)` | Merges `newData` into every element matching all `query` fields. | | `menu.setElement(index, key, value)` | Sets one field of the element at `index`. | | `menu.setElements(elements)` | Replaces the element list. | | `menu.setTitle(title)` | Changes the title. | | `menu.removeElement(query)` | Removes elements that match any field in `query`. | | `menu.data` | The data table you passed in. | | `menu.type`, `menu.namespace`, `menu.name` | Identifiers. | ```lua menu.update({ name = 'bread' }, { label = 'Bread (sold out)', usable = false }) menu.refresh() ``` <Callout type="info"> The setters only change Lua data. Nothing changes on screen until you call `menu.refresh()`. </Callout> ## Other menu functions [#other-menu-functions] These are generic `ESX.UI.Menu` helpers that work for every menu type (`default`, `dialog`, `list`). | Function | Description | | --- | --- | | `ESX.UI.Menu.Close(type, namespace, name, cancel)` | Closes a specific menu. With `cancel = true` the `cancel` callback runs instead of `close`. | | `ESX.UI.Menu.CloseAll(cancel)` | Closes every open menu. | | `ESX.UI.Menu.GetOpened(type, namespace, name)` | Returns the menu object or `nil`. | | `ESX.UI.Menu.IsOpen(type, namespace, name)` | Alias of `GetOpened`; truthy when open. | | `ESX.UI.Menu.GetOpenedMenus()` | Returns the internal list of open menus (may contain `nil` holes). | | `ESX.UI.Menu.RegisterType(type, open, close)` | Registers a custom menu type. Used by the menu resources themselves. | ## Controls [#controls] All keys are FiveM key mappings, so players can rebind them under **Settings > Key Bindings > FiveM**. | Default key | Key mapping | Action | | --- | --- | --- | | Enter | `+menu_default_enter` | Submit the highlighted element (200 ms debounce) | | Backspace | `+menu_default_backspace` | Cancel | | Arrow Up | `+menu_default_top` | Move up (skips unselectable rows, wraps) | | Arrow Down | `+menu_default_down` | Move down | | Arrow Left / Right | `+menu_default_left` / `+menu_default_right` | Change slider value | Keys are ignored while the pause menu is open. ## Common issues [#common-issues] - **`attempt to index a nil value (global 'xLib')`**: `esx_lib` is missing or starts after this resource. - **Menu does not close**: `submit` and `cancel` never close the menu automatically. Call `menu.close()`. - **Same menu opens twice**: `ESX.UI.Menu.Open` does not check for duplicates. Guard with `if ESX.UI.Menu.IsOpen('default', GetCurrentResourceName(), 'shop') then return end` or call `ESX.UI.Menu.CloseAll()` first. - **Changes to elements are not visible**: call `menu.refresh()` after `setElement`, `update` or `setTitle`. - **Untrusted text breaks the menu**: `label`, `description` and `title` are rendered as HTML. Escape player names or other user input before you show them. ## Related [#related] - [Client UI functions](/docs/es_extended/client/ui) - [esx_menu_dialog](/docs/core/esx_menu_dialog) - [esx_menu_list](/docs/core/esx_menu_list) - [esx_context](/docs/core/esx_context) for mouse-driven menus - [esx_lib](/docs/core/esx_lib) # esx_menu_dialog (/docs/core/esx_menu_dialog) esx_menu_dialog is the input box of ESX Legacy. It shows a title, one text field (or a larger text area) and Submit/Cancel buttons. Use it when you need a single value from the player, such as an amount of money, a plate or a short message. You open it with `ESX.UI.Menu.Open('dialog', ...)`. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_menu_dialog` | | Side | Client only | | Menu type | `dialog` | | Dependencies | `es_extended`, `esx_lib` (loaded via `@esx_lib/imports.lua`) | | UI | jQuery + Mustache page in `html/` | The resource has no configuration file. The dialog takes mouse and keyboard focus about 200 ms after opening and releases it when the last dialog closes. <Callout type="warn"> The manifest only lists `es_extended`, but the client loads `@esx_lib/imports.lua`. Start `esx_lib` first. </Callout> ## Opening a dialog [#opening-a-dialog] ```lua ESX.UI.Menu.Open('dialog', namespace, name, data, submit, cancel, change, close) ``` | Name | Type | Description | | --- | --- | --- | | `namespace` | `string` | Usually `GetCurrentResourceName()`. | | `name` | `string` | Unique dialog name inside the namespace. | | `data` | `table` | Dialog data, see below. | | `submit` | `function(data, menu)?` | Submit button or Enter. | | `cancel` | `function(data, menu)?` | Cancel button or Escape. | | `change` | `function(data, menu)?` | Runs on every keystroke with the current text in `data.value`. | | `close` | `function()?` | Runs after `menu.close()`. | The returned `menu` object and the generic helpers (`ESX.UI.Menu.Close`, `CloseAll`, `IsOpen`) are documented on the [esx_menu_default page](/docs/core/esx_menu_default#menu-object). ### Dialog data [#dialog-data] | Field | Type | Description | | --- | --- | --- | | `title` | `string` | Text in the header. Escaped, so HTML is shown as text. | | `type` | `string?` | `'default'` (single-line input, default) or `'big'` (multi-line text area). | | `value` | `string?` | Prefilled value. | ### Callback data [#callback-data] | Field | Description | | --- | --- | | `data.value` | What the player typed. On submit it is always a **string**. | | `data.title`, `data.type` | Copied from your dialog data. | | `data._namespace`, `data._name` | Menu identifiers. | ## Input rules on submit [#input-rules-on-submit] Before your `submit` callback runs, the resource processes the value: 1. If the text parses as a number, it is rounded to the nearest integer. 2. If that number is `0` or negative, the dialog shows the notification "That input is not allowed!" and **does not call** `submit`. 3. The value is trimmed and converted to a string. So `" 12.6 "` arrives as `"13"`, `"-5"` and `"0"` never arrive, and text like `"ABC 123"` passes through trimmed. ## Example [#example] ```lua title="client/main.lua" local function askDepositAmount() ESX.UI.Menu.Open('dialog', GetCurrentResourceName(), 'bank_deposit', { title = 'Deposit amount', }, function(data, menu) local amount = tonumber(data.value) if not amount then ESX.ShowNotification('Enter a valid number', 'error') return end menu.close() TriggerServerEvent('my_bank:deposit', amount) end, function(data, menu) menu.close() end) end ``` A multi-line dialog with a prefilled value: ```lua ESX.UI.Menu.Open('dialog', GetCurrentResourceName(), 'edit_note', { title = 'Note', type = 'big', value = currentNote, }, function(data, menu) menu.close() TriggerServerEvent('my_notes:save', data.value) end, function(data, menu) menu.close() end) ``` <Callout type="error" title="Validate on the server"> The rounding and the positive-number check run on the client. A cheater can trigger your server event with any value, so check the amount again on the server. </Callout> ## Controls [#controls] | Key | Action | | --- | --- | | Enter | Submit | | Escape | Cancel | | Mouse | Submit / Cancel buttons | ## Common issues [#common-issues] - **Comparing `data.value` to a number fails**: it is a string. Use `tonumber(data.value)`. - **Players cannot enter `0`**: by design. If you need zero or negative values, read them in a `change` callback or use [esx_context](/docs/core/esx_context) number inputs. - **Dialog stays open after Submit**: call `menu.close()` in `submit` and `cancel`. - **Mouse cursor stays after closing**: focus is only released when no dialog is left open. Make sure every dialog you open is closed, or call `ESX.UI.Menu.CloseAll()`. ## Related [#related] - [esx_menu_default](/docs/core/esx_menu_default) - [esx_menu_list](/docs/core/esx_menu_list) - [esx_context](/docs/core/esx_context) - [Client UI functions](/docs/es_extended/client/ui) # esx_menu_list (/docs/core/esx_menu_list) esx_menu_list renders a table menu: a header row and any number of data rows, with buttons inside cells. Use it to show tabular data the player can act on, such as a list of employees with "Promote" and "Fire" buttons. You open it with `ESX.UI.Menu.Open('list', ...)`. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_menu_list` | | Side | Client only | | Menu type | `list` | | Dependencies | `es_extended` | | UI | jQuery + Mustache page in `html/` | The resource has no configuration file. The menu takes mouse focus about 200 ms after opening and releases it when the last list menu closes. If several list menus are open, only the most recently opened one is visible. ## Opening a list menu [#opening-a-list-menu] ```lua ESX.UI.Menu.Open('list', namespace, name, data, submit, cancel) ``` | Name | Type | Description | | --- | --- | --- | | `namespace` | `string` | Usually `GetCurrentResourceName()`. | | `name` | `string` | Unique menu name inside the namespace. | | `data` | `table` | `{ head = {...}, rows = {...} }`, see below. | | `submit` | `function(data, menu)?` | A cell button was clicked. | | `cancel` | `function(data, menu)?` | Escape was pressed. | The `change` and `close` arguments of `ESX.UI.Menu.Open` are accepted, but this menu type never sends change events. The returned `menu` object is described on the [esx_menu_default page](/docs/core/esx_menu_default#menu-object). ### Menu data [#menu-data] | Field | Type | Description | | --- | --- | --- | | `head` | `string[]` | Column headers. Escaped (shown as plain text). | | `rows` | `table[]` | One entry per row. | | `rows[i].cols` | `string[]` | Cell contents, one per column. Rendered as HTML. | | `rows[i].data` | `table?` | Any data you want back when a button in this row is clicked. | There is no `title` field; the table header is the only heading. ### Buttons in cells [#buttons-in-cells] Inside a cell string, `{{Label|value}}` becomes a button. A cell can contain several buttons: ```lua cols = { 'John Doe', 'Mechanic', '{{Promote|promote}} {{Fire|fire}}' } ``` ### Callback data [#callback-data] | Field | Description | | --- | --- | | `data.value` | The `value` part of the clicked button. | | `data.data` | The `data` table of the clicked row, plus `currentRow` (1-based row index). | | `data._namespace`, `data._name` | Menu identifiers. | `cancel` receives only `_namespace` and `_name`. ## Example [#example] ```lua title="client/main.lua" local function openEmployeeList(employees) local rows = {} for i = 1, #employees do local employee = employees[i] rows[#rows + 1] = { data = { identifier = employee.identifier }, cols = { employee.name, employee.gradeLabel, '{{Promote|promote}} {{Fire|fire}}', }, } end ESX.UI.Menu.Open('list', GetCurrentResourceName(), 'employees', { head = { 'Name', 'Grade', 'Actions' }, rows = rows, }, function(data, menu) menu.close() TriggerServerEvent('my_boss:employeeAction', data.data.identifier, data.value) end, function(data, menu) menu.close() end) end ``` <Callout type="warn" title="Escape cell content"> `cols` are inserted as raw HTML. If a cell shows player-controlled text (names, notes), strip `<` and `>` before you add it, or a player can inject markup into other players' menus. </Callout> ## Controls [#controls] | Key | Action | | --- | --- | | Mouse click on a cell button | Submit | | Escape | Cancel (on the visible menu) | ## Common issues [#common-issues] - **Escape does nothing**: you did not pass a `cancel` callback, or it does not call `menu.close()`. - **Button value has the wrong type**: jQuery reads `data-value` and converts numeric strings to numbers, so `{{Buy|5}}` arrives as the number `5`. Use string keys like `buy_5` if you need consistent types. - **`|` or `}}` in labels breaks buttons**: the button syntax is parsed with a simple pattern. Keep labels free of these characters. ## Related [#related] - [esx_menu_default](/docs/core/esx_menu_default) - [esx_menu_dialog](/docs/core/esx_menu_dialog) - [esx_context](/docs/core/esx_context) - [Client UI functions](/docs/es_extended/client/ui) # esx_multicharacter (/docs/core/esx_multicharacter) `esx_multicharacter` lets one FiveM account own several ESX characters. Players pick, create or delete a character on a selection screen before they spawn, and admins can grant extra slots or lock characters. ESX enables multicharacter mode automatically as soon as this resource is present on the server. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_multicharacter` | | Version | 1.16.3 (fxmanifest, ships with ESX Legacy 1.16.4) | | Side | Client + server, React NUI | | Dependencies | `es_extended`, `esx_context`, [esx_identity](/docs/core/esx_identity), [esx_skin](/docs/core/esx_skin), `esx_lib`, `oxmysql` | | Requirements | OneSync enabled (not `off` or `legacy`), server artifact 10188 or newer | `es_extended` sets `Config.Multichar = GetResourceState("esx_multicharacter") ~= "missing"`. You do not toggle multicharacter in the es_extended config; you add or remove this resource. ## How characters are stored [#how-characters-are-stored] Every character is a normal row in `users`. Its identifier is `<Config.Prefix><slot>:<license>`, for example `char1:1a2b3c...`. The player keeps one license; the prefix selects the character. On start the resource migrates the database itself: - adds `users.disabled` (`TINYINT(1)`, default `0`) used by `/disablechar` - creates the `multicharacter_slots` table (`identifier`, `slots`) - scans every table with an `identifier` or `owner` `VARCHAR` column, widens it to fit the prefix and adds an index The scanned tables are also the ones cleaned when a character is deleted, so you do not need to register your own tables for deletion as long as they use an `identifier` or `owner` column. <Callout type="warn"> If the migration fails, the console prints `[esx_multicharacter] 1.16.0 migration failed; character selection is disabled` and players hang on a black screen. Check that your database user can run `ALTER TABLE` and `CREATE INDEX`. </Callout> ## Configuration [#configuration] `config.lua` is shared, but some keys only exist on one side. | Key | Side | Default | Description | | --- | --- | --- | --- | | `Config.Locale` | Shared | `GetConvar("esx:locale", "en")` | UI language. | | `Config.CanDelete` | Shared | `true` | Allows players to delete their own characters. | | `Config.Relog` | Shared | `true` | Registers the `/relog` command. | | `Config.Slots` | Server | `3` | Default slot count for every player. The code falls back to `4` if you remove the key. | | `Config.Prefix` | Server | `"char"` | Identifier prefix. Keep it short; changing it on a live server orphans existing characters. | | `Config.Spawn` | Client | `{ { x = -284.2856, y = 562.4627, z = 172.9182, w = 19.9895 } }` | List of camera positions for the selection scene. One is picked at random. | | `Config.Default` | Client | `{ m = {...}, f = {...} }` | Default skinchanger appearance for new male and female characters. | <Callout type="info"> `Config.Spawn` is only the selection scene. The spawn point of a new character is the default value of the `position` column in the `users` table. </Callout> ## Commands [#commands] | Command | Permission | Arguments | Description | | --- | --- | --- | --- | | `/relog` | everyone | none | Logs out the current character and returns to selection. Only when `Config.Relog = true`. Rate limited. | | `/setslots` | `admin` | `identifier` (string), `slots` (number) | Sets a custom slot count. | | `/remslots` | `admin` | `identifier` (string) | Removes the custom slot count, so the player falls back to `Config.Slots`. | | `/enablechar` | `admin` | `identifier` (string), `charslot` (number) | Re-enables a disabled character. | | `/disablechar` | `admin` | `identifier` (string), `charslot` (number) | Locks a character so it cannot be selected. | The admin commands are allowed from the server console. The database change is applied, but the confirmation notification then throws a script error because there is no xPlayer, so prefer running them in game. `identifier` is the raw license (without `char1:`), for example: ```txt title="In-game chat" /setslots 1a2b3c4d5e6f7a8b9c0d 5 /disablechar 1a2b3c4d5e6f7a8b9c0d 2 ``` The slot count is clamped between `1` and `100`. ## Events and callbacks [#events-and-callbacks] ### Server [#server] | Name | Type | Description | | --- | --- | --- | | `esx_multicharacter:SetupCharacters` | Net event (client to server) | Loads the player's characters and sends `esx_multicharacter:SetupUI`. Rate limited to one request per 5 seconds. | | `esx_multicharacter:CharacterChosen` | Net event | `(charid, isNew)`. Selects an existing slot or reserves an empty one for registration. | | `esx_multicharacter:DeleteCharacter` | Net event | `(charid)`. Deletes a character if `Config.CanDelete` is on. | | `esx_multicharacter:relog` | Net event | Starts a relog. | | `esx_multicharacter:chooseCharacter` | `xLib` callback | Same as `CharacterChosen`, returns `{ success, error? }`. | | `esx_multicharacter:deleteCharacter` | `xLib` callback | Returns `{ success, error? }`. | | `esx_multicharacter:relog` | `xLib` callback | Returns `{ success, error? }`. Used by `/relog`. | The resource listens to `esx_identity:completedRegistration` to create the new character and then fires `esx:onPlayerJoined` with the `charN` prefix. For relog it triggers `esx:playerLogout`, which saves and unloads the xPlayer. Possible `error` values: `delete_disabled`, `invalid_character`, `busy`, `rate_limited`, `stale_session`, `database_error`, `refresh_failed`, `already_loaded`, `disabled`, `not_loaded`. ### Client [#client] | Name | Description | | --- | --- | | `esx_multicharacter:SetupUI` | Secure net event from the server with `(characters, slots)`. Opens the selection UI. | | `esx:onPlayerLogout` | Fades out and reopens the selection after a relog. | ### Reacting to a character switch [#reacting-to-a-character-switch] Use the ESX events instead of multicharacter internals. They fire for both first spawn and relog: ```lua title="client/main.lua" RegisterNetEvent('esx:playerLoaded', function(xPlayer, isNew, skin) ESX.PlayerData = xPlayer -- build blips, jobs, UI for the selected character end) RegisterNetEvent('esx:onPlayerLogout', function() -- clean up anything tied to the previous character end) ``` ## Common issues [#common-issues] - **"ESX Requires Onesync Infinity to work".** Add `set onesync on` to `server.cfg`. - **"Cannot Find the servers mysql_connection_string".** The resource parses the database name from `mysql_connection_string`. Use the `mysql://user:pass@host/database` or `database=...;` form. - **"identifier-active" when joining.** Another connected player (or a stale session) uses the same license. The resource cleans stale entries automatically if the old player no longer has a ping. With `EnableDebug` on in es_extended, the duplicate check is skipped for testing. - **Black screen after selecting a character.** Usually a failed schema migration or a resource that errors in `esx:playerLoaded`. Check the server console. - **Players see the default slot count after `/setslots`.** Make sure you passed the license without the `char1:` prefix. ## Related [#related] - [esx_identity](/docs/core/esx_identity) - [esx_skin](/docs/core/esx_skin) - [es_extended configuration](/docs/es_extended/config) - [Client events](/docs/es_extended/events/client) - [Database schema](/docs/guides/database-schema) # esx_notify (/docs/core/esx_notify) esx_notify is the default notification UI of ESX Legacy. It draws small toast notifications with a type (success, error, info, warning), an optional title and a configurable screen position. You rarely call it directly: `ESX.ShowNotification` on the client and `xPlayer.showNotification` on the server both end up in this resource. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_notify` | | Side | Client only (server code uses events or `xPlayer`) | | Dependencies | `es_extended` (loads `@es_extended/imports.lua`) | | UI | NUI page (`nui/index.html`), jQuery, Material Symbols icons | | Required | Yes, `ESX.ShowNotification` errors when it is missing | ## Configuration [#configuration] `Config.lua` has two keys. | Key | Default | Description | | --- | --- | --- | | `Config.notificationSoundEnabled` | `true` | Plays a short generated tone per notification. The pitch depends on the type. | | `Config.position` | `"middle-right"` | Default position when a call passes no valid position. | Valid positions: `top-right`, `top-left`, `top-middle`, `bottom-right`, `bottom-left`, `bottom-middle`, `middle-left`, `middle-right`. Any other value falls back to `Config.position`. ## Notification types [#notification-types] | Type | Icon | | --- | --- | | `success` | check circle | | `error` | error | | `info` | info (also used for unknown types) | | `warning` | warning triangle | ## Functions [#functions] ### ESX.ShowNotification [#esxshownotification] Client wrapper in es_extended. This is the call you use in almost every script. ```lua ESX.ShowNotification(message, notifyType, length, title, position) ``` | Name | Type | Description | | --- | --- | --- | | `message` | `string` | Text to show. Supports color codes and `~br~`. | | `notifyType` | `string?` | `success`, `error`, `info` or `warning`. Default `"info"`. | | `length` | `number?` | Duration in milliseconds. Default `5000`. | | `title` | `string?` | Bold title above the message. Default `"New Notification"`. | | `position` | `string?` | One of the positions above. Default `Config.position`. | ```lua title="client/main.lua" ESX.ShowNotification("You bought a ~g~water bottle~s~.", "success", 3000, "Shop") ESX.ShowNotification("Inventory full!~br~Some items were dropped.", "warning", 4000, "Inventory", "bottom-left") ``` ### xPlayer.showNotification [#xplayershownotification] Server side, the `xPlayer` object has the same signature. It triggers the client event `esx:showNotification`, which calls `ESX.ShowNotification`. ```lua title="server/main.lua" local xPlayer = ESX.GetPlayerFromId(source) if xPlayer then xPlayer.showNotification("Your paycheck has arrived.", "info", 5000, "Bank") end ``` ### Notify export [#notify-export] The raw export. Note the different argument order: type and length come first. ```lua exports["esx_notify"]:Notify(notifyType, length, message, title, position) ``` | Name | Type | Description | | --- | --- | --- | | `notifyType` | `string` | Notification type. Non-string values become `"info"`. | | `length` | `number` | Duration in ms. Invalid values (negative, `NaN`, infinite, non-number) become `3000`. | | `message` | `any` | Converted with `tostring`. `~br~` becomes a line break. | | `title` | `string?` | Optional title. | | `position` | `string?` | Optional position. | ```lua title="client/main.lua" exports["esx_notify"]:Notify("error", 4000, "You don't have enough money!", "Transaction failed") ``` ## Events [#events] | Event | Side | Arguments | | --- | --- | --- | | `esx:showNotification` | Client (net, registered by es_extended) | `message, type, length, title, position` | | `ESX:Notify` | Client (net, registered by esx_notify) | `type, length, message, title, position` | Trigger them from the server when you have no `xPlayer` at hand: ```lua title="server/main.lua" TriggerClientEvent("esx:showNotification", playerId, "Server restart in 5 minutes", "warning", 10000, "Server") ``` ## Formatting [#formatting] The NUI escapes HTML, so you cannot inject markup. Two formatting helpers exist: - `~br~` (or a literal `<br>`) inserts a line break. - Color codes: `~r~` red, `~b~` blue, `~g~` green, `~y~` yellow, `~p~` purple, `~c~` grey, `~m~` dark grey, `~u~` black, `~o~` orange. <Callout type="warn" title="~s~ does not reset the color"> In 1.16.4 the NUI script removes every `~s~` before it applies colors. A color code therefore colors the rest of the message, not just the next word. Put the colored part at the end of the message, or split the information into title and message. </Callout> ## Debug commands [#debug-commands] When `Config.EnableDebug` is `true` in es_extended, esx_notify registers test commands: `/oldnotify`, `/notify`, `/notify1`, `/notify2`, `/notify3` and `/notify4`. They are not available on a normal server. ## Common issues [#common-issues] - **Nothing shows up.** Check that `esx_notify` is started after `es_extended` and that no other resource overrides `ESX.ShowNotification`. - **Wrong text in the title or a number as the message.** You passed the `ESX.ShowNotification` argument order to the export, or the other way around. - **Replacing esx_notify with another UI.** Keep a resource named `esx_notify` or edit `es_extended/client/modules/ui/notifications.lua`; es_extended only checks for this resource name. ## Related [#related] - [Core resources overview](/docs/core) - [Client functions](/docs/es_extended/client/functions) - [xPlayer](/docs/es_extended/server/xplayer) - [esx_textui](/docs/core/esx_textui) - [esx_progressbar](/docs/core/esx_progressbar) # esx_progressbar (/docs/core/esx_progressbar) esx_progressbar shows a timed progress bar at the bottom of the screen, with an optional animation or scenario and an optional player freeze. Use it for actions that take time, such as lockpicking, crafting or searching a trash bin. Call it from client code with `ESX.Progressbar`. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_progressbar` | | Side | Client only | | Dependencies | `esx_lib` and `es_extended` (both loaded via imports, not listed as manifest dependencies) | | Config | None | | Required | Yes if any script calls `ESX.Progressbar`; es_extended errors when it is missing | ## Functions [#functions] ### ESX.Progressbar [#esxprogressbar] ```lua local started = ESX.Progressbar(message, length, options) ``` | Name | Type | Description | | --- | --- | --- | | `message` | `string?` | Text on the bar. Default `"ESX-Framework"`. Supports color codes such as `~g~`. | | `length` | `number?` | Duration in milliseconds. Default `3000`. | | `options` | `table?` | See the options table below. | **Returns** `boolean`: `true` when the bar started, `false` when another bar is already running. #### Options [#options] | Field | Type | Description | | --- | --- | --- | | `FreezePlayer` | `boolean?` | Freezes the player ped until the bar finishes or is canceled. | | `animation` | `table?` | Animation to play, see below. | | `onFinish` | `function?` | Called when the bar completes. | | `onCancel` | `function?` | Called when the bar is canceled. | `animation` has two forms: | Field | Form | Description | | --- | --- | --- | | `type` | both | `"anim"` or `"Scenario"` (capital S, case-sensitive). | | `dict` | `anim` | Animation dictionary. Loaded through `xLib.streaming.requestAnimDict`. | | `lib` | `anim` | Animation name inside the dictionary. | | `Scenario` | `Scenario` | Scenario name, e.g. `"PROP_HUMAN_BUM_BIN"` (capital S). | ```lua title="client/main.lua" local function searchTrash() local started = ESX.Progressbar("Searching the trash...", 5000, { FreezePlayer = true, animation = { type = "Scenario", Scenario = "PROP_HUMAN_BUM_BIN", }, onFinish = function() TriggerServerEvent("my_trash:searched") end, onCancel = function() ESX.ShowNotification("You stopped searching.", "info") end, }) if not started then ESX.ShowNotification("You are already busy.", "error") end end ``` Animation example: ```lua title="client/main.lua" ESX.Progressbar("Unlocking storage", 3000, { FreezePlayer = true, animation = { type = "anim", dict = "anim@mp_player_intmenu@key_fob@", lib = "fob_click", }, onFinish = function() TriggerServerEvent("my_storage:open") end, }) ``` ### ESX.CancelProgressbar [#esxcancelprogressbar] Cancels the active bar: closes the UI, clears the ped tasks, unfreezes the player and calls `onCancel`. Does nothing when no bar is active. ```lua ESX.CancelProgressbar() ``` Use it when the player dies, gets into a vehicle or leaves the area: ```lua title="client/main.lua" AddEventHandler("esx:onPlayerDeath", function() ESX.CancelProgressbar() end) ``` ## Exports [#exports] The ESX functions are thin wrappers around these exports. | Export | Signature | | --- | --- | | `Progressbar` | `exports["esx_progressbar"]:Progressbar(message, length, options)` returns `boolean` | | `CancelProgressbar` | `exports["esx_progressbar"]:CancelProgressbar()` | ## Controls [#controls] | Key mapping | Default | Action | | --- | --- | --- | | `[ProgressBar] Cancel Progressbar` | Backspace | Cancels the active bar | Players can rebind it in **Settings → Key Bindings → FiveM**. The key is registered with `xLib.addKeybind`. ## Common issues [#common-issues] - **Animation stops before the bar ends.** The animation plays with the bar length as its duration and flag `1` (loop). If your animation still stops, another script is clearing the ped tasks. - **Players skip timed actions.** Every bar can be canceled with Backspace, and a modified client can call `onFinish` early. Never rely on the bar for timing on the server; check elapsed time server-side. - **Scenario does not play.** Use `type = "Scenario"` and the key `Scenario` with a capital S. `"scenario"` in lowercase is ignored. - **`ESX.Progressbar` returns `false`.** A bar is still running. Check the return value and tell the player they are busy, or cancel the old bar first. ## Related [#related] - [Core resources overview](/docs/core) - [Client functions](/docs/es_extended/client/functions) - [esx_notify](/docs/core/esx_notify) - [esx_lib](/docs/core/esx_lib) # esx_skin (/docs/core/esx_skin) `esx_skin` is the ESX Legacy character creator. It opens a Svelte NUI on top of [skinchanger](/docs/core/skinchanger), lets players edit their ped, and stores the result in `users.skin`. Use its server exports when your own resource (clothing shop, barber, outfit wardrobe) needs to read or save a player's appearance. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_skin` | | Version | 1.16.3 (fxmanifest, ships with ESX Legacy 1.16.4) | | Side | Client (menu, camera) + server (validation, storage) | | Dependencies | `es_extended`, [skinchanger](/docs/core/skinchanger), `esx_lib`, `oxmysql` | | Database | `users.skin` (JSON) | ## How saving works [#how-saving-works] 1.16 moved all skin storage to the server and hardened it: - Every skin is validated against `Config.SkinFields`. Unknown keys, non-integers or out-of-range values are rejected. - Clients can only save through `esx_skin:save` while they have an **edit session**. Sessions are created on the first login of a player without a skin, by the `/skin` command, and by the `BeginSkinEdit` export. A session expires after `Config.SkinEditSessionTTL` and is consumed by a successful save. - Saves are rate limited per player (`Config.SkinSaveCooldown`) and queued per identifier. - Shops should save on the server with a **scope** (`clothes`, `barber`, `outfit`) so the player cannot change fields the shop does not sell. ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language. | | `Config.BackpackWeight` | `{ [40] = 16, [41] = 20, [44] = 25, [45] = 23 }` | Extra carry weight per `bags_1` drawable. Ignored when es_extended uses a custom inventory. | | `Config.AllowClothingBackpackCapacity` | `false` | `true`: any worn bag adds weight. `false`: only bags granted with `SetBackpackCapacity` count. | | `Config.SkinMaxBytes` | `8192` | Maximum JSON size of a skin. | | `Config.SkinSaveCooldown` | `2000` | Rate limiter refill interval for saves, in ms (burst of 2). | | `Config.SkinCacheTTL` | `60000` | How long a loaded skin stays cached, in ms. | | `Config.SkinFields` | table | Allowed skin keys with `{ min, max }`, for example `sex = { 0, 1 }`, `mom = { 0, 45 }`, `nose_1 = { -10, 10 }`, `torso_1 = { 0, 4095 }`. | | `Config.SkinEditSessionTTL` | `900000` | Lifetime of an edit session, in ms (15 minutes). | | `Config.SkinEditFields` | built in config | Field whitelists per scope: `clothes`, `barber`, `outfit`. | Scopes are built in `config.lua`: | Scope | Fields | | --- | --- | | `clothes` | `tshirt`, `torso`, `decals`, `pants`, `shoes`, `bags`, `chain`, `helmet`, `glasses`, `watches` (`_1` and `_2`), `arms`, `arms_2` | | `barber` | `beard_1-4`, `eyebrows_1-4`, `makeup_1-4`, `lipstick_1-4`, `hair_1`, `hair_2`, `hair_color_1`, `hair_color_2`, `ears_1`, `ears_2` | | `outfit` | all `clothes` fields plus `mask`, `bproof`, `bracelets`, `ears` (`_1` and `_2`) | <Callout type="warn"> If you add custom components to skinchanger, also add them to `Config.SkinFields`. Otherwise every save fails with `invalid_appearance:<field>:unknown_field`. </Callout> ## Server exports [#server-exports] ### SaveSkin [#saveskin] ```lua exports.esx_skin:SaveSkin(playerId, skin, callback, scope) ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | `number` | Server ID of a loaded player. | | `skin` | `table` | Full skin table (must include `sex`). | | `callback` | `fun(success: boolean, reason?: string)` \| `nil` | Called when the write finished or failed. | | `scope` | `"clothes"` \| `"barber"` \| `"outfit"` \| `nil` | Limits which fields may differ from the stored skin. `nil` saves the whole skin. | Returns `false` when the request was rejected immediately, otherwise `true` (queued). With `barber`, only barber fields are copied onto the stored skin. With `clothes` or `outfit`, any change outside the scope is rejected with `appearance_scope_mismatch:<field>`. ```lua title="server/main.lua" xLib.callback.register('myshop:buyHaircut', function(source, skin) local p = promise.new() exports.esx_skin:SaveSkin(source, skin, function(success, reason) p:resolve({ success = success, reason = reason }) end, 'barber') return Citizen.Await(p) end) ``` ### SaveOutfit [#saveoutfit] ```lua exports.esx_skin:SaveOutfit(playerId, clothes, callback) ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | `number` | Server ID. | | `clothes` | `table` | Clothing values. Only `outfit` scope fields are copied; everything else is ignored. | | `callback` | `fun(saved: boolean, skin?: table)` \| `nil` | Receives the merged skin on success. | ```lua title="server/main.lua" local policeUniform = { tshirt_1 = 58, tshirt_2 = 0, torso_1 = 55, torso_2 = 0, arms = 41, pants_1 = 25, pants_2 = 0, shoes_1 = 25, shoes_2 = 0 } exports.esx_skin:SaveOutfit(source, policeUniform, function(saved, skin) if saved then TriggerClientEvent('skinchanger:loadSkin', source, skin) end end) ``` ### GetSkin [#getskin] ```lua local skin, ready = exports.esx_skin:GetSkin(playerId) ``` Returns the stored skin table (or `nil`) and `ready`, which is `false` when the player is not loaded or the database read failed. Uses the cache when possible. ### BeginSkinEdit [#beginskinedit] ```lua local ok = exports.esx_skin:BeginSkinEdit(playerId) ``` Opens an edit session so the client may save its full skin once through the saveable menu. Returns `false` if the player is not loaded or a save is in flight. ```lua title="server/main.lua" RegisterCommand('plasticsurgery', function(source) if exports.esx_skin:BeginSkinEdit(source) then TriggerClientEvent('esx_skin:openSaveableMenu', source) end end, true) ``` ### ValidateSkin [#validateskin] ```lua local clean = exports.esx_skin:ValidateSkin(skin) ``` Returns a decoded copy of the skin if it passes `Config.SkinFields` validation, otherwise `nil`. ### SetBackpackCapacity [#setbackpackcapacity] ```lua local ok = exports.esx_skin:SetBackpackCapacity(playerId, bag) ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | `number` | Server ID. | | `bag` | `number` \| `nil` | A key from `Config.BackpackWeight`, or `nil` to remove the bonus. | Stores `authorizedBackpack` in the player's metadata and sets max weight to `ESX MaxWeight + Config.BackpackWeight[bag]`. Returns `false` for unknown bags. ## Server callback [#server-callback] `esx_skin:getPlayerSkin` returns the stored skin and the job outfits: ```lua title="client/main.lua" xLib.callback('esx_skin:getPlayerSkin', false, function(skin, jobSkin) if skin then TriggerEvent('skinchanger:loadSkin', skin) end -- jobSkin = { skin_male = ..., skin_female = ... } from the player's job grade end) ``` It is also registered as a compat callback, so `ESX.TriggerServerCallback('esx_skin:getPlayerSkin', cb)` keeps working. ## Client events [#client-events] All menu events take Lua functions as arguments, so trigger them locally with `TriggerEvent` (or from the server without callbacks). | Event | Arguments | Description | | --- | --- | --- | | `esx_skin:openMenu` | `submitCb, cancelCb` | Opens the editor without saving. | | `esx_skin:openRestrictedMenu` | `submitCb, cancelCb, restrict` | Same, limited to the component names in `restrict`. | | `esx_skin:openSaveableMenu` | `submitCb?, cancelCb?` | Opens the editor and saves on submit. Needs an edit session. | | `esx_skin:openSaveableRestrictedMenu` | `submitCb?, cancelCb?, restrict` | Saveable and restricted. | | `esx_skin:getLastSkin` | `cb(skin)` | Returns the skin before the menu was opened. | | `esx_skin:setLastSkin` | `skin` | Overrides the last skin, for example after a paid change. | | `esx_skin:playerRegistered` | none | Fired by esx_identity; loads the saved skin or opens the creator. | | `esx_skin:resetFirstSpawn` | none | Marks the next spawn as first spawn (used by identity and multicharacter). | | `esx_skin:saveFailed` | none | Server tells the client the legacy `esx_skin:save` net event failed. | `submitCb(data, menu)` and `cancelCb(data, menu)` receive `data.current` (focused element), `data.elements`, and a `menu` handle with `close()`, `update(_, element)` and `refresh()`. When you cancel, the last skin is restored automatically. ```lua title="client/main.lua" -- shop: only show clothes, let the server decide whether to save TriggerEvent('esx_skin:openRestrictedMenu', function(data, menu) menu.close() TriggerEvent('skinchanger:getSkin', function(skin) TriggerServerEvent('myshop:buyClothes', skin) -- server calls SaveSkin(..., 'clothes') end) end, function(data, menu) menu.close() end, { 'tshirt_1', 'tshirt_2', 'torso_1', 'torso_2', 'arms', 'pants_1', 'pants_2', 'shoes_1', 'shoes_2' }) ``` <Callout type="error"> Never add an unauthenticated server event that saves whatever skin the client sends. Validate payment or permission first, then call `SaveSkin` with a scope. </Callout> ## Commands [#commands] | Command | Permission | Arguments | Description | | --- | --- | --- | --- | | `/skin` | `admin` | `playerId` (optional) | Opens a saveable editor for yourself or the target player. | ## Common issues [#common-issues] - **"Your appearance could not be saved".** Check the F8 console for `[esx_skin] Appearance save failed: <reason>`. `edit_session_missing` means no `BeginSkinEdit` was called; `invalid_appearance:...` means a field is unknown or out of range; `rate_limited` means saves come too fast. - **Old shop scripts stop saving.** Scripts that open `esx_skin:openSaveableMenu` or fire `esx_skin:save` from the client without a server-side `BeginSkinEdit` no longer work. Move the save to the server and use `SaveSkin` with a scope. - **Backpack weight does not apply.** With `Config.AllowClothingBackpackCapacity = false` you must call `SetBackpackCapacity`. It never applies when es_extended uses ox_inventory or another custom inventory. - **`esx_skin:setWeight` does nothing.** It is kept as an empty handler for compatibility. ## Related [#related] - [skinchanger](/docs/core/skinchanger) - [esx_identity](/docs/core/esx_identity) - [esx_multicharacter](/docs/core/esx_multicharacter) - [Callbacks guide](/docs/guides/callbacks) # esx_textui (/docs/core/esx_textui) esx_textui shows one persistent prompt on the screen, typically an interaction hint such as "Press [E] to open the shop". Unlike a notification it stays until you hide it. Call `ESX.TextUI` when the player enters an interaction area and `ESX.HideUI` when they leave. ## Overview [#overview] | | | | --- | --- | | Resource | `esx_textui` | | Side | Client only | | Dependencies | `es_extended` (loads `@es_extended/imports.lua`) | | Config | None | | Required | Yes if any script calls `ESX.TextUI`; es_extended errors when it is missing | Only one TextUI is visible at a time. Showing a new one replaces the current one. ## Functions [#functions] ### ESX.TextUI [#esxtextui] ```lua ESX.TextUI(message, type) ``` | Name | Type | Description | | --- | --- | --- | | `message` | `string` | Text to show. Default `"ESX-TextUI"`. Supports color codes, HTML is escaped. | | `type` | `string?` | `"success"`, `"error"` or `"info"`. Non-string values become `"info"`. | ### ESX.HideUI [#esxhideui] Hides the current TextUI. Does nothing when none is shown. ```lua ESX.HideUI() ``` <Callout type="warn"> `ESX.HideUI` hides whatever TextUI is visible, including one opened by another resource. Only hide the prompt when your own script showed it. </Callout> ### Example: interaction point [#example-interaction-point] ```lua title="client/main.lua" local shopCoords = vector3(25.7, -1347.3, 29.49) local isShowing = false CreateThread(function() while true do local sleep = 500 local distance = #(GetEntityCoords(PlayerPedId()) - shopCoords) if distance < 2.0 then sleep = 0 if not isShowing then ESX.TextUI("Press ~g~[E]~s~ to open the shop", "info") isShowing = true end if IsControlJustReleased(0, 38) then -- 38 = E TriggerEvent("my_shop:open") end elseif isShowing then ESX.HideUI() isShowing = false end Wait(sleep) end end) ``` For many points, use the point helpers from [esx_lib](/docs/core/esx_lib) instead of a distance loop. ## Exports [#exports] | Export | Signature | | --- | --- | | `TextUI` | `exports["esx_textui"]:TextUI(message, type)` | | `HideUI` | `exports["esx_textui"]:HideUI()` | ## Events [#events] | Event | Side | Arguments | | --- | --- | --- | | `ESX:TextUI` | Client (net) | `message, type` | | `ESX:HideUI` | Client (net) | none | Both are registered with `ESX.SecureNetEvent`, so they are meant to be triggered from the server: ```lua title="server/main.lua" TriggerClientEvent("ESX:TextUI", playerId, "You are in a ~r~restricted~s~ zone", "error") TriggerClientEvent("ESX:HideUI", playerId) ``` ## Color codes [#color-codes] `~r~` red, `~b~` blue, `~g~` green, `~y~` yellow, `~p~` purple, `~c~` grey, `~m~` dark grey, `~u~` black, `~o~` orange. Close a color with `~s~`. ## Debug commands [#debug-commands] With `Config.EnableDebug = true` in es_extended, the commands `/textui:error`, `/textui:success`, `/textui:info` and `/textui:hide` are registered for testing. ## Common issues [#common-issues] - **Prompt flickers.** You call `ESX.TextUI` every frame. Use a flag such as `isShowing` and only call it on state changes. - **Prompt never disappears.** The script that showed it was restarted or never reached the `ESX.HideUI` branch. Hide it in an `onResourceStop` handler. - **Nothing appears with type `"warning"`.** esx_textui has no warning style. The NUI script fails on unknown types; use `"error"` instead. ## Related [#related] - [Core resources overview](/docs/core) - [Client functions](/docs/es_extended/client/functions) - [esx_notify](/docs/core/esx_notify) - [Point helper](/docs/es_extended/client/imports/point) # esx_whitelist (/docs/core/esx_whitelist) `esx_whitelist` controls who can connect to an ESX Legacy server. It checks players during `playerConnecting` against a database identifier list, Discord guild roles, or both, and adds an admin-only maintenance mode, an emergency bypass list, Discord webhook logging and an in-game admin panel. It ships disabled, so installing it changes nothing until you enable it. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `esx_whitelist` | | Side | Server (connection check) + client (admin panel NUI) | | Dependencies | `oxmysql`, `esx_lib`, `es_extended` | | Database | `esx_whitelist_config`, `esx_whitelist_identifiers` (created automatically) | | Version | 1.16.3 (shipped with ESX Legacy 1.16.4) | ## Installation [#installation] <Steps> <Step> ### Import the SQL (optional) [#import-the-sql-optional] Import `whitelist.sql`. The resource also creates missing tables and indexes on start, so this step is optional. </Step> <Step> ### Add convars and ensure the resource [#add-convars-and-ensure-the-resource] Secrets must use `set` (server only). UI convars must use `setr` so the client can read them. ```bash title="server.cfg" set discord:botToken "YOUR_DISCORD_BOT_TOKEN" # only for discord/both mode set whitelist:webhook "https://discord.com/api/webhooks/..." # optional logging setr esx:ui:logoUrl "https://example.com/logo.png" # optional panel logo ensure oxmysql ensure esx_lib ensure es_extended ensure esx_whitelist ``` </Step> <Step> ### Configure and enable [#configure-and-enable] Edit `config/main.lua`: pick a `Mode`, add identifiers or Discord roles, then set `Config.Whitelist.Enabled = true`. You can also do all of this later in the `/whitelist` panel. </Step> </Steps> <Callout type="warn" title="Never put the bot token in config or client files"> The token is read only from the `discord:botToken` convar. Keep it out of `config/main.lua` (it is a shared script and is sent to clients) and out of public repositories. </Callout> ## How a connection is checked [#how-a-connection-is-checked] Every connection runs through the same order. The first matching rule decides: ```txt 0. Whitelist disabled -> allow 1. Bypass identifiers -> allow (method: bypass) 2. Admin-only mode enabled -> allow/deny (skips everything below) 3. Cached admin group -> allow (method: admin_group) 4. Mode-specific check -> allow/deny 5. Default -> deny (fail closed) ``` | Mode | Behavior | | --- | --- | | `identifier` | One of the player's identifiers is in the `whitelist` list. | | `discord` | The player has a linked Discord identifier, is a member of `GuildId`, and has at least one role from `AllowedRoles`. | | `both` + `CombinationMode = 'or'` | Identifier match **or** Discord verification. | | `both` + `CombinationMode = 'and'` | Identifier match **and** Discord verification. | Admin groups are read from the `group` column of the ESX `users` table (matched via the `esx:identifier` convar, default `license`) and updated live on `esx:playerLoaded` and `esx:setGroup`. If the database is not ready within 15 seconds, the player is rejected with a verification error. ## Configuration [#configuration] All keys live in `config/main.lua`. Values changed in the panel are saved to `esx_whitelist_config` and **override** the file. ### General [#general] | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar('esx:locale', 'en')` | Language. Only `en` ships with the resource. | | `Config.IdentifierTypes` | `{ license2 = true, license = true, fivem = true }` | Identifier prefixes accepted for access lists. `ip` is always rejected. | | `Config.Debug` | `false` | Print extra logs to the server console. | ### Config.Whitelist [#configwhitelist] | Key | Default | Description | | --- | --- | --- | | `Enabled` | `false` | Master switch. When `false`, everyone may connect and no checks run. | | `Mode` | `'identifier'` | `'discord'`, `'identifier'` or `'both'`. | | `CombinationMode` | `'or'` | How `both` combines the checks: `'or'` or `'and'`. | | `AllowedIdentifiers` | `{}` | Initial identifier list, imported **once** into `esx_whitelist_identifiers` (type `whitelist`). | ### Config.Discord [#configdiscord] | Key | Default | Description | | --- | --- | --- | | `Enabled` | `false` | Perform Discord verification. | | `GuildId` | `''` | Discord server id the player must be a member of. | | `AllowedRoles` | `{}` | Role ids; the member needs at least one. | | `Timeout` | `5000` | Per-request HTTP timeout (ms). | | `CacheTTL` | `60` | Seconds a verified result may be reused. `0` disables the cache. | | `CacheMaxEntries` | `10000` | Max cached Discord results. | | `RequestsPerSecond` | `40` | Outbound request smoothing, in addition to Discord rate-limit headers. | | `MaxRetries` | `2` | Retries for timeouts, 5xx and network errors. | | `RetryBaseDelay` | `750` | First retry delay (ms), doubled per attempt. | | `RetryMaxDelay` | `4000` | Max retry delay (ms). | ### Config.Admin [#configadmin] | Key | Default | Description | | --- | --- | --- | | `Groups` | `{ admin = true, superadmin = true, owner = true }` | ESX groups that may use the panel and commands, and that bypass the normal whitelist. | | `IdentifierPriority` | `{ 'license2', 'license', 'fivem' }` | Order used to pick the stable identifier for cache keys. | | `Cache.Enabled` | `true` | Enable the admin group cache. | | `Cache.SaveDelay` | `5000` | Kept for panel compatibility. Admin decisions are not persisted. | ### Config.AdminOnly [#configadminonly] | Key | Default | Description | | --- | --- | --- | | `Enabled` | `false` | Maintenance mode: only these groups or identifiers may connect. | | `Groups` | `{ admin = true, superadmin = true, owner = true }` | Groups allowed in admin-only mode. | | `AllowedIdentifiers` | `{}` | Identifiers allowed in admin-only mode (imported once, type `admin_only`). | ### Config.Bypass [#configbypass] | Key | Default | Description | | --- | --- | --- | | `AllowedIdentifiers` | `{}` | Emergency list checked before everything else, even admin-only mode (imported once, type `bypass`). Keep it tiny. | ### Config.Logging [#configlogging] The webhook URL comes from the `whitelist:webhook` convar. | Key | Default | Description | | --- | --- | --- | | `Enabled` | `true` | Send events to the webhook (only if the convar is set). | | `MinInterval` | `1200` | Minimum ms between webhook requests. | | `BatchSize` | `5` | Events sent per cycle. | | `QueueLimit` | `250` | Max queued events; the oldest are dropped. | | `Levels` | `{ info = true, warning = true, error = true, security = true }` | Severities that are sent. | ### Config.Performance [#configperformance] | Key | Default | Description | | --- | --- | --- | | `DiscordConcurrency` | `10` | Max simultaneous Discord API requests. | | `DiscordQueueLimit` | `4096` | Max pending Discord verifications. | | `VerificationTimeout` | `120000` | Hard limit (ms) for one connection check. | | `PanelRateLimit` | `10` | Panel actions allowed per window. | | `PanelRateWindow` | `5000` | Rate-limit window (ms). | ### Config.Panel [#configpanel] | Key | Default | Description | | --- | --- | --- | | `Command` | `'whitelist'` | Command name for the panel and subcommands. | | `Subcommands` | `Panel = 'panel'`, `Status = 'status'`, `Reload = 'reload'`, `Cache = 'cache'`, `CacheClear = 'clear'`, `Test = 'test'`, `Add = 'add'`, `Remove = 'remove'` | Names of the subcommands. | ## Commands [#commands] All commands work in the server console. In-game, the player's ESX group must be in `Config.Admin.Groups`; unauthorized attempts are logged as security events. | Command | Description | | --- | --- | | `/whitelist` or `/whitelist panel` | Open the admin panel (in-game only). | | `whitelist status` | Mode, admin-only state, Discord readiness and decision counters. | | `whitelist reload` | Reload runtime config and admin cache from the database, clear the Discord cache. | | `whitelist cache` | Cache and queue statistics. | | `whitelist cache clear` | Clear the Discord verification cache. | | `whitelist test <serverId>` | Run the full check against an online player. | | `whitelist add <identifier> [type]` | Add an identifier. `type` is `whitelist` (default), `admin_only` or `bypass`. | | `whitelist remove <identifier> [type]` | Remove an identifier from that list. | ```bash title="Server console" whitelist add license2:1234567890abcdef1234567890abcdef12345678 whitelist add license:abcdef1234567890abcdef1234567890abcdef12 bypass whitelist status ``` ## Admin panel [#admin-panel] `/whitelist` opens a NUI panel with the tabs Dashboard, Whitelist, Discord, Admins and Logging & Performance. Changes are validated on the server, rate limited, persisted to `esx_whitelist_config` and logged. The panel refuses changes to admin groups that would remove your own current group, so you cannot lock yourself out. Theme convars: `esx:ui:logoUrl`, `esx:ui:primaryColor`, `esx:ui:secondaryColor`, `esx:ui:backgroundColor`, `esx:ui:accentColor` (all `setr`). ## Discord bot setup [#discord-bot-setup] 1. Create an application and bot at the [Discord developer portal](https://discord.com/developers/applications). 2. Invite the bot to your guild with the `bot` scope. No special permissions are required; role checks use the guild member endpoint. 3. Enable Developer Mode in Discord, then copy the guild id and role ids into `GuildId` and `AllowedRoles` (or the panel). 4. Set `Config.Discord.Enabled = true` and `Config.Whitelist.Mode = 'discord'` or `'both'`. Players must have Discord running and linked to FiveM, otherwise they have no `discord:` identifier and are denied. ## Database schema [#database-schema] ```sql title="whitelist.sql" CREATE TABLE IF NOT EXISTS `esx_whitelist_config` ( `setting_key` VARCHAR(64) NOT NULL, `setting_value` LONGTEXT NOT NULL, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`setting_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS `esx_whitelist_identifiers` ( `identifier` VARCHAR(128) NOT NULL, `type` VARCHAR(32) NOT NULL DEFAULT 'whitelist', `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`identifier`, `type`), KEY `idx_type` (`type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ``` `type` is one of `whitelist`, `admin_only` or `bypass`. ## Exports and events [#exports-and-events] esx_whitelist has no public exports. Its net events (prefix `esxwl:`) are internal to the panel and command bridge. It listens to `playerConnecting`, `playerDropped`, `esx:playerLoaded` and `esx:setGroup`. ## Common issues [#common-issues] <Accordions> <Accordion title="Everyone can still join"> `Config.Whitelist.Enabled` is `false` by default. Enable it in the config or panel, then check `whitelist status`. A value saved in the panel wins over the file. </Accordion> <Accordion title="Identifiers added to AllowedIdentifiers are ignored"> The config lists are imported into the database only on the first start. After that, use `whitelist add` or the panel. Also make sure the prefix is allowed by `Config.IdentifierTypes` (default `license2`, `license`, `fivem`). </Accordion> <Accordion title="Discord players are always denied"> Check `whitelist status` for `Discord: NOT READY`. Common causes: missing or wrong `discord:botToken`, bot not in the guild, wrong `GuildId`, `Config.Discord.Enabled = false`, or the player has no linked Discord identifier. Discord failures fail closed. </Accordion> <Accordion title="Locked out in admin-only mode"> Add your identifier from the server console: `whitelist add license2:... bypass`. Bypass is checked before admin-only mode. </Accordion> </Accordions> ## Related [#related] - [Core resources overview](/docs/core) - [Admin permissions](/docs/guides/admin-permissions) - [server.cfg](/docs/getting-started/server-cfg) - [Database schema](/docs/guides/database-schema) # Core Resources (/docs/core) The `[core]` folder of ESX Legacy contains `es_extended` plus a set of small resources that provide the UI, character creation, inventory and utilities the framework relies on. This page lists every core resource in ESX Legacy 1.16.4, what it does and whether you can remove it. ## Resource overview [#resource-overview] | Resource | Purpose | Side | Required? | | --- | --- | --- | --- | | [esx_lib](/docs/core/esx_lib) | Shared library (xLib): callbacks, streaming, points, validation, SQL catalog | Shared | Yes, es_extended depends on it | | [es_extended](/docs/es_extended) | The framework itself: players, jobs, accounts, items | Shared | Yes | | [esx_notify](/docs/core/esx_notify) | Toast notifications behind `ESX.ShowNotification` | Client | Yes in practice | | [esx_textui](/docs/core/esx_textui) | Persistent prompt behind `ESX.TextUI` | Client | Yes in practice | | [esx_progressbar](/docs/core/esx_progressbar) | Timed progress bar behind `ESX.Progressbar` | Client | Yes in practice | | [esx_context](/docs/core/esx_context) | Mouse context menus and small forms (`ESX.OpenContext`) | Client | Yes in practice | | [esx_menu_default](/docs/core/esx_menu_default) | Classic keyboard menu (`ESX.UI.Menu`, type `default`) | Client | Needed by most addons | | [esx_menu_dialog](/docs/core/esx_menu_dialog) | Text and number input dialog (type `dialog`) | Client | Needed by most addons | | [esx_menu_list](/docs/core/esx_menu_list) | Table-style list menu (type `list`) | Client | Needed by some addons | | [skinchanger](/docs/core/skinchanger) | Applies ped model, clothes and face data | Client | Yes, `ESX.SpawnPlayer` waits for it | | [esx_skin](/docs/core/esx_skin) | Character appearance editor and skin storage | Shared | Yes for character creation | | [esx_identity](/docs/core/esx_identity) | Registration form: name, date of birth, sex, height | Shared | Yes for character registration | | [esx_multicharacter](/docs/core/esx_multicharacter) | Character selection with several slots per player | Shared | No | | [esx_inventory](/docs/core/esx_inventory) | Default slot inventory and storages | Shared | Unless you use ox_inventory | | [esx_whitelist](/docs/core/esx_whitelist) | Identifier or Discord-role whitelist with admin panel | Shared | No (disabled by default) | | [esx_loadingscreen](/docs/core/esx_loadingscreen) | Loading screen while connecting | Client | No | | [esx_chat_theme](/docs/core/esx_chat_theme) | Visual theme for the cfx `chat` resource | Client | No | | [cron](/docs/core/cron) | Daily scheduled tasks via `cron:runAt` | Server | Only for addons that use it (esx_society, esx_vehicleshop) | <Callout type="info" title="Yes in practice"> es_extended does not list these as manifest dependencies, but its client functions call them directly. If one of them is missing, `ESX.ShowNotification`, `ESX.TextUI`, `ESX.Progressbar` or `ESX.OpenContext` throws `Resource [name] is Missing!`. </Callout> ## Recommended ensure order [#recommended-ensure-order] The official txAdmin recipe starts the core like this: ```bash title="server.cfg" ensure chat ensure oxmysql ensure esx_lib ensure es_extended ensure [core] ensure [standalone] ensure [esx_addons] ``` `ensure [core]` starts every resource in the folder. Each resource declares its dependencies in `fxmanifest.lua` (for example esx_multicharacter depends on esx_context, esx_identity and esx_skin), so FiveM starts them in a valid order. If you list resources one by one, follow the dependency order: ```bash title="server.cfg" ensure chat ensure oxmysql ensure esx_lib ensure es_extended ensure cron ensure esx_notify ensure esx_textui ensure esx_progressbar ensure esx_context ensure esx_menu_default ensure esx_menu_dialog ensure esx_menu_list ensure skinchanger ensure esx_skin ensure esx_identity ensure esx_multicharacter ensure esx_inventory ensure esx_whitelist ensure esx_loadingscreen ensure esx_chat_theme ``` Leave out the optional resources you do not use. See [server.cfg](/docs/getting-started/server-cfg) for the full file. ## Explore the core resources [#explore-the-core-resources] <Cards> <Card title="esx_lib" href="/docs/core/esx_lib">The xLib library every ESX resource builds on.</Card> <Card title="esx_notify" href="/docs/core/esx_notify">Notifications, types, positions and events.</Card> <Card title="esx_context" href="/docs/core/esx_context">Context menus and forms with ESX.OpenContext.</Card> <Card title="esx_menu_default" href="/docs/core/esx_menu_default">The classic ESX.UI.Menu API.</Card> <Card title="esx_inventory" href="/docs/core/esx_inventory">Default inventory, storages and exports.</Card> <Card title="esx_multicharacter" href="/docs/core/esx_multicharacter">Character slots, selection and admin commands.</Card> <Card title="esx_whitelist" href="/docs/core/esx_whitelist">Whitelist modes, panel, Discord roles and SQL.</Card> <Card title="cron" href="/docs/core/cron">Run server tasks at a fixed time each day.</Card> </Cards> ## Related [#related] - [Installation](/docs/getting-started/installation) - [server.cfg](/docs/getting-started/server-cfg) - [es_extended](/docs/es_extended) - [Official addons](/docs/addons) # skinchanger (/docs/core/skinchanger) `skinchanger` is the client-only ESX Legacy resource that applies appearance values (heritage, face features, hair, overlays, clothes, props) to the `mp_m_freemode_01` and `mp_f_freemode_01` peds and reads them back. It does not save anything; [esx_skin](/docs/core/esx_skin) handles the UI and the database. ## Overview [#overview] | | | | --- | --- | | Resource | `skinchanger` | | Version | 1.16.3 (fxmanifest, ships with ESX Legacy 1.16.4) | | Side | Client only | | Dependencies | `es_extended` (locale file only) | | Used by | `esx_skin`, `esx_multicharacter`, job and shop addons | ## Skin table [#skin-table] A skin is a flat table of component names and integers. `sex` is required: `0` is male, `1` is female. Keys you omit keep their current value. | Group | Keys | | --- | --- | | Model | `sex` | | Heritage | `mom`, `dad`, `grandparents`, `face_md_weight`, `skin_md_weight`, `face_g_weight` | | Face features | `nose_1`-`nose_6`, `cheeks_1`-`cheeks_3`, `lip_thickness`, `jaw_1`, `jaw_2`, `chin_1`-`chin_4`, `neck_thickness`, `eye_color`, `eye_squint` | | Hair | `hair_1`, `hair_2`, `hair_color_1`, `hair_color_2` | | Clothes | `tshirt_1/2`, `torso_1/2`, `decals_1/2`, `arms`, `arms_2`, `pants_1/2`, `shoes_1/2`, `mask_1/2`, `bproof_1/2`, `chain_1/2`, `bags_1/2` | | Props | `helmet_1/2`, `glasses_1/2`, `watches_1/2`, `bracelets_1/2`, `ears_1/2` | | Overlays | `eyebrows_1`-`eyebrows_6`, `beard_1`-`beard_4`, `makeup_1`-`makeup_4`, `lipstick_1`-`lipstick_4`, `chest_1`-`chest_3`, `bodyb_1`-`bodyb_4`, `age_1/2`, `blemishes_1/2`, `blush_1`-`blush_3`, `complexion_1/2`, `sun_1/2`, `moles_1/2` | For props, `-1` removes the item (for example `helmet_1 = -1`). <Callout type="info"> Old examples use `face` and `skin` keys. They no longer exist; use `mom`, `dad` and the `*_md_weight` blend keys instead. </Callout> ## Configuration [#configuration] | Key | Default | Description | | --- | --- | --- | | `Config.Locale` | `GetConvar("esx:locale", "en")` | Language of component labels. | | `Config.Components` | 98 entries | Ordered list of editable components. esx_skin builds its menu from it. | Each entry in `Config.Components` has these fields: | Field | Type | Description | | --- | --- | --- | | `name` | `string` | Skin key, for example `"torso_1"`. | | `label` | `string` | Translated menu label. | | `value` | `number` | Default value used at startup. | | `min` | `number` | Minimum value. | | `max` | `number` \| `function(ped, character)` | Maximum value, or a function that reads drawable or texture counts from the ped. | | `zoomOffset`, `camOffset` | `number` | Camera position in the esx_skin editor. | | `textureof` | `string` | Optional. Marks a texture slider that depends on another component (for example `hair_2` depends on `hair_1`). | | `componentId` | `number` | Optional. GTA component or prop index for clothing entries. | ## Exports [#exports] All exports are client side. ### GetSkin [#getskin] ```lua local skin = exports.skinchanger:GetSkin() ``` Returns the current skin table with every component name. ### LoadSkin [#loadskin] ```lua local ok = exports.skinchanger:LoadSkin(skin) ``` | Name | Type | Description | | --- | --- | --- | | `skin` | `table` | Skin table with `sex = 0` or `sex = 1`. | Returns `false` if the skin is invalid. When `sex` changes, the freemode model is swapped first and the skin is applied afterwards. ### LoadClothes [#loadclothes] ```lua local ok = exports.skinchanger:LoadClothes(playerSkin, clothesSkin) ``` | Name | Type | Description | | --- | --- | --- | | `playerSkin` | `table` | The player's base skin (must contain `sex`). | | `clothesSkin` | `table` | Clothing values to put on top. Face, heritage, hair and overlay (beard, makeup, eyebrows, ...) keys in this table are ignored. | ```lua title="client/main.lua" -- put on the job uniform without touching face or hair xLib.callback('esx_skin:getPlayerSkin', false, function(skin, jobSkin) local uniform = skin.sex == 0 and jobSkin.skin_male or jobSkin.skin_female exports.skinchanger:LoadClothes(skin, uniform) end) ``` ### Change [#change] ```lua exports.skinchanger:Change(key, value) ``` | Name | Type | Description | | --- | --- | --- | | `key` | `string` | Component name. | | `value` | `number` | New value. Changing `sex` reloads the model. | ### GetData [#getdata] ```lua local components, maxVals = exports.skinchanger:GetData(noMax) ``` Returns `Config.Components` with current values filled in, plus a table of maximum values per component. Pass `true` as `noMax` to skip the max calculation. ### GetMaxVals [#getmaxvals] ```lua local maxVals = exports.skinchanger:GetMaxVals() ``` Returns `{ [componentName] = max }` for the current ped. ## Events [#events] | Event | Type | Arguments | Description | | --- | --- | --- | --- | | `skinchanger:loadSkin` | Net event | `skin, cb?` | Same as `LoadSkin`. `cb` only works when triggered locally. | | `skinchanger:loadClothes` | Net event | `playerSkin, clothesSkin` | Same as `LoadClothes`. | | `skinchanger:getSkin` | Local event | `cb(skin)` | Returns the current skin. | | `skinchanger:getData` | Local event | `cb(components, maxVals)` | Same as `GetData`. | | `skinchanger:change` | Local event | `key, value` | Same as `Change`. | | `skinchanger:loadDefaultModel` | Local event | `isMale, cb?` | Sets `mp_m_freemode_01` (true) or `mp_f_freemode_01` (false). | | `skinchanger:modelLoaded` | Local event | none | Re-applies a pending skin after a model change. | ```lua title="client/main.lua" TriggerEvent('skinchanger:loadDefaultModel', true, function() TriggerEvent('skinchanger:loadSkin', { sex = 0, hair_1 = 4, hair_color_1 = 2 }) end) TriggerEvent('skinchanger:getSkin', function(skin) print(json.encode(skin)) end) ``` Because `skinchanger:loadSkin` and `skinchanger:loadClothes` are net events, the server can push a skin: ```lua title="server/main.lua" TriggerClientEvent('skinchanger:loadSkin', playerId, skin) ``` ## Common issues [#common-issues] - **Skin not applied.** The table has no valid `sex`. Always include `sex = 0` or `sex = 1`. - **Changes are lost after relog.** skinchanger only changes the ped locally. Save through [esx_skin](/docs/core/esx_skin) exports on the server. - **Callback of `LoadSkin` export never runs.** The export does not accept a callback. Use the `skinchanger:loadSkin` event with a callback instead. - **Custom component not saved.** Add new components to both `Config.Components` here and `Config.SkinFields` in esx_skin. ## Related [#related] - [esx_skin](/docs/core/esx_skin) - [esx_multicharacter](/docs/core/esx_multicharacter) - [Core resources](/docs/core) # Client Functions (/docs/es_extended/client/functions) These are the general-purpose functions that `es_extended` adds to the `ESX` object on the client. Use them to check whether the character is loaded, read and update the cached player data, search the built-in inventory, register secure net events and key bindings. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` | | Side | Client | | Source | `client/modules/player/*.lua`, `client/modules/core/events.lua`, `client/compat.lua`, `client/modules/game/*.lua` | | Access | `ESX = exports['es_extended']:getSharedObject()` or `shared_script '@es_extended/imports.lua'` | UI helpers (notifications, menus, context, progress bar) are on [Client UI](/docs/es_extended/client/ui). Entity helpers are on [ESX.Game](/docs/es_extended/client/modules/game). ## Client fields [#client-fields] These values are set when `es_extended` starts on the client. | Field | Type | Description | |---|---|---| | `ESX.PlayerData` | `table` | Cached data of the local character. See [Client PlayerData](/docs/es_extended/shared/playerdata). | | `ESX.PlayerLoaded` | `boolean` | `true` after `esx:playerLoaded` has been handled, `false` after logout. | | `ESX.playerId` | `number` | Local player index (`PlayerId()`). | | `ESX.serverId` | `number` | Server id of the local player (`GetPlayerServerId(PlayerId())`). | ## Player data [#player-data] ### ESX.IsPlayerLoaded [#esxisplayerloaded] ```lua ESX.IsPlayerLoaded() ``` Returns whether the local character is fully loaded. Check this before you read `ESX.PlayerData`. **Returns:** `boolean` ```lua title="client/main.lua" CreateThread(function() while not ESX.IsPlayerLoaded() do Wait(250) end print(('Loaded as %s'):format(ESX.PlayerData.job.label)) end) ``` ### ESX.GetPlayerData [#esxgetplayerdata] ```lua ESX.GetPlayerData() ``` Returns the `ESX.PlayerData` table. It is the same table reference, so reading `ESX.PlayerData` directly is equivalent. **Returns:** `table` ```lua local data = ESX.GetPlayerData() print(data.identifier, data.job.name, data.job.grade_label) ``` ### ESX.SetPlayerData [#esxsetplayerdata] ```lua ESX.SetPlayerData(key, val) ``` Writes a value into the local `ESX.PlayerData` and fires the local `esx:setPlayerData` event so other scripts (and `OnPlayerData` from the imports file) can react. The event is not fired for the `loadout` key, or when a non-table value did not change. | Name | Type | Description | |---|---|---| | `key` | `string` | Key in `ESX.PlayerData` to set. | | `val` | `any` | New value. | **Returns:** nothing <Callout type="warn"> This only changes the client cache. It does not save anything on the server and the server will overwrite the key the next time it sends an update. Change real data (job, money, items) on the server through the [xPlayer](/docs/es_extended/server/xplayer) object. </Callout> ```lua ESX.SetPlayerData('isCuffed', true) ``` ### ESX.GetAccount [#esxgetaccount] ```lua ESX.GetAccount(account) ``` Returns one entry from `ESX.PlayerData.accounts`. | Name | Type | Description | |---|---|---| | `account` | `string` | Account name, for example `money`, `bank` or `black_money`. | **Returns:** `table|nil` with `name`, `money`, `label`, `round` and `index`, or `nil` if the account does not exist. ```lua local bank = ESX.GetAccount('bank') if bank then print(('Bank balance: $%s'):format(ESX.Math.GroupDigits(bank.money))) end ``` ### ESX.SearchInventory [#esxsearchinventory] ```lua ESX.SearchInventory(items, count) ``` Searches `ESX.PlayerData.inventory` (built-in ESX inventory only). | Name | Type | Description | |---|---|---| | `items` | `string \| string[]` | One item name, or an array of item names. | | `count` | `boolean?` | When `true`, return counts instead of item tables. Missing items return `0`. | **Returns:** - with a single item name: the item table (`name`, `count`, `label`, `weight`, `usable`, `rare`, `canRemove`), its count, or `nil`. - with an array: a table keyed by item name. ```lua local counts = ESX.SearchInventory({ 'bread', 'water' }, true) print(('bread: %d, water: %d'):format(counts.bread, counts.water)) local bread = ESX.SearchInventory('bread') if bread then print(bread.label, bread.count) end ``` ### ESX.ShowInventory [#esxshowinventory] ```lua ESX.ShowInventory() ``` Opens the default inventory UI through `exports.esx_inventory:ShowInventory()`. Does nothing unless `Config.EnableDefaultInventory` is enabled. **Returns:** nothing ## Events and spawning [#events-and-spawning] ### ESX.SecureNetEvent [#esxsecurenetevent] ```lua ESX.SecureNetEvent(name, func) ``` Registers a client net event that only runs when the **server** triggered it. Calls from a local `TriggerEvent` (where `source` is an empty string) are ignored. Handlers are removed automatically when the registering resource stops. Errors inside the handler are re-thrown with the original message. | Name | Type | Description | |---|---|---| | `name` | `string` | Event name. | | `func` | `function` | Handler, receives the event arguments. | **Returns:** nothing ```lua title="client/main.lua" ESX.SecureNetEvent('myjob:setDuty', function(onDuty) ESX.ShowNotification(onDuty and 'You are on duty' or 'You are off duty') end) ``` ```lua title="server/main.lua" TriggerClientEvent('myjob:setDuty', source, true) ``` Use it for every client event that changes state or grants something. A plain `RegisterNetEvent` handler can also be fired locally by any client script. ### ESX.DisableSpawnManager [#esxdisablespawnmanager] ```lua ESX.DisableSpawnManager() ``` Turns off auto-spawn of the default `spawnmanager` resource if it is started. ESX calls this itself before it spawns the player. **Returns:** nothing ### ESX.SpawnPlayer [#esxspawnplayer] ```lua ESX.SpawnPlayer(skin, coords, cb) ``` Loads a skin through `skinchanger:loadSkin`, freezes the player, moves the ped to `coords`, waits up to 5 seconds for collision, resurrects the ped and fires the `playerSpawned` event. Used by ESX on first spawn and by multicharacter resources. | Name | Type | Description | |---|---|---| | `skin` | `table` | Skin table for skinchanger. | | `coords` | `table` | Table with `x`, `y`, `z` and `heading` (not `w`). | | `cb` | `function` | Called when the spawn is done. | **Returns:** nothing <Callout type="warn"> `ESX.SpawnPlayer` leaves the player frozen. ESX unfreezes it in its own `esx:playerLoaded` handler. If you call it yourself later, unfreeze the ped in `cb` (for example with `FreezeEntityPosition(PlayerPedId(), false)`). </Callout> ## Input and keys [#input-and-keys] ### ESX.RegisterInput [#esxregisterinput] ```lua ESX.RegisterInput(command_name, label, input_group, key, on_press, on_release) ``` Registers a remappable key binding. It wraps `xLib.addKeybind`, which creates the commands `+command_name` and `-command_name` and a key mapping the player can change in the GTA settings. The callbacks do not fire while the pause menu is open. | Name | Type | Description | |---|---|---| | `command_name` | `string` | Unique name. ESX prefixes it with `+` / `-`. | | `label` | `string` | Text shown in the key bindings settings. | | `input_group` | `string` | Input mapper, usually `keyboard`. See [input mapper ids](https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/). | | `key` | `string` | Default key, for example `f6`. | | `on_press` | `function?` | Called on key down. | | `on_release` | `function?` | Called on key up. | **Returns:** a keybind object with `currentKey`, `isPressed`, `disable(self, toggle)`, `remove(self)` and `getCurrentKey(self)`. ```lua title="client/main.lua" local menuKey = ESX.RegisterInput('myjob_menu', 'Open job menu', 'keyboard', 'f6', function() if ESX.PlayerData.job.name == 'police' then OpenPoliceMenu() end end) print('Menu key is', menuKey.currentKey) ``` ### ESX.HashString [#esxhashstring] ```lua ESX.HashString(str) ``` Despite the name, this builds a `~INPUT_...~` text token for a command mapping, so GTA can render the bound key inside help text. | Name | Type | Description | |---|---|---| | `str` | `string` | The mapped command name. For keys made with `ESX.RegisterInput`, the mapped command is `+` plus your name. | **Returns:** `string` ```lua ESX.ShowHelpNotification(('Press %s to open the menu'):format(ESX.HashString('+myjob_menu'))) ``` If the token does not render for your binding, use the `currentKey` field of the object returned by `ESX.RegisterInput` instead. ## Vehicles [#vehicles] ### ESX.GetVehicleTypeClient [#esxgetvehicletypeclient] ```lua ESX.GetVehicleTypeClient(model) ``` Returns the vehicle type string FiveM expects for `CreateVehicleServerSetter`, based on the vehicle class plus a list of known exceptions. `ESX.GetVehicleType` is an alias on the client. | Name | Type | Description | |---|---|---| | `model` | `string \| number` | Model name or hash. | **Returns:** `string|false`: one of `automobile`, `bike`, `boat`, `heli`, `plane`, `submarine`, `trailer`, `train`, or `false` when the model does not exist or is not a vehicle. ```lua print(ESX.GetVehicleTypeClient('policeb')) -- bike print(ESX.GetVehicleTypeClient('kosatka')) -- submarine ``` The server version is documented in [Server functions](/docs/es_extended/server/functions). ## Common issues [#common-issues] - **`ESX.PlayerData.job` is nil.** You read it before the player loaded. Wait for `ESX.IsPlayerLoaded()` or the `esx:playerLoaded` event. - **Changes from `ESX.SetPlayerData` disappear.** The server owns the real data and pushes it again on the next update. - **Inventory helpers return nothing.** `ESX.SearchInventory` and `ESX.ShowInventory` only work with the built-in inventory, not with `Config.CustomInventory`. ## Related [#related] - [Client UI functions](/docs/es_extended/client/ui) - [Callbacks](/docs/es_extended/client/modules/callback) - [Client events](/docs/es_extended/events/client) - [Client PlayerData](/docs/es_extended/shared/playerdata) - [Server functions](/docs/es_extended/server/functions) # Points (/docs/es_extended/client/imports/point) `ESX.Point` is a small class for proximity triggers. You give it coordinates, a radius and callbacks for entering, leaving and staying inside. One shared loop does the distance checks for all points, which is cheaper than one `while true` loop per marker. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` (implemented in `esx_lib` as `xLib.point`) | | Side | Client | | Check interval | 500 ms for enter/leave, every frame while inside a point with `inside` | ## Creating a point [#creating-a-point] ### ESX.Point:new [#esxpointnew] ```lua local point = ESX.Point:new(properties) ``` | Property | Type | Description | |---|---|---| | `coords` | `vector3` | Center of the point. | | `distance` | `number` | Radius in units. | | `hidden` | `boolean?` | Start hidden (no callbacks fire until shown). | | `enter` | `function?` | `function(self)` when the player enters the radius. | | `leave` | `function?` | `function(self)` when the player leaves the radius. | | `inside` | `function?` | `function(self, distance)` every frame while inside. | **Returns:** the point object. It keeps `coords`, `hidden`, `enter`, `leave`, `inside` and an internal `handle`. The callbacks are called as methods, so the first argument is the point itself and the distance is the second argument of `inside`. ```lua title="client/main.lua" local shop = ESX.Point:new({ coords = vector3(25.7, -1347.3, 29.5), distance = 2.0, enter = function(self) ESX.TextUI('[E] Open shop') end, leave = function(self) ESX.HideUI() end, inside = function(self, distance) if distance < 1.0 and IsControlJustReleased(0, 38) then OpenShopMenu() end end, }) ``` <Callout type="info"> For key presses, consider [ESX.RegisterInteraction](/docs/es_extended/client/modules/interactions) instead of polling a control inside `inside`. Then you do not need the every-frame callback at all. </Callout> ## Methods [#methods] ### point:delete [#pointdelete] ```lua point:delete() ``` Removes the point. `leave` is **not** called, so hide any UI yourself. ```lua ESX.HideUI() shop:delete() ``` ### point:toggle [#pointtoggle] ```lua point:toggle(hidden) ``` | Name | Type | Description | |---|---|---| | `hidden` | `boolean?` | `true` hides, `false` shows. Without an argument the state is flipped. | A hidden point is treated as out of range: if the player was inside, `leave` fires on the next check. ```lua RegisterNetEvent('esx:setJob', function(job) shop:toggle(job.name ~= 'shopkeeper') end) ``` ## Low-level functions [#low-level-functions] `ESX.Point` is built on three functions that are also exposed. You normally do not need them. | Function | Description | |---|---| | `ESX.CreatePointInternal(coords, distance, hidden, enter, leave, inside)` | Creates a raw point. Returns a numeric handle. Callbacks get no `self`; `inside` gets `distance`. | | `ESX.RemovePointInternal(handle)` | Removes a raw point. | | `ESX.HidePointInternal(handle, hidden)` | Hides or shows a raw point. | ## Common issues [#common-issues] - **Errors in callbacks.** An error in `enter` or `leave` is printed as `point X from resource errored` and the loop continues. An error in `inside` stops that point's `inside` calls until the player leaves and enters again. - **`point.currDistance` is nil.** That field does not exist. Use the second argument of `inside`. - **Nothing happens.** Check that `coords` is a `vector3` and `distance` is set. ## Related [#related] - [Interactions](/docs/es_extended/client/modules/interactions) - [Client UI (TextUI)](/docs/es_extended/client/ui) - [Require](/docs/es_extended/client/imports/require) - [Performance guide](/docs/guides/performance) # Require (/docs/es_extended/client/imports/require) FiveM does not ship a working Lua `require`. ESX provides one so you can split a resource into modules and load them on demand, including from other resources. You get it automatically when your manifest loads `@es_extended/imports.lua` and/or `@esx_lib/imports.lua`. ## Overview [#overview] | | | |---|---| | Provided by | `@es_extended/imports.lua` (as `ESX.require`, only when `ox_lib` is missing) and `@esx_lib/imports.lua` (as `xLib.require`) | | Side | Client and server | | Global | `require` is replaced by one of the above | ```lua title="fxmanifest.lua" fx_version 'cerulean' game 'gta5' lua54 'yes' shared_scripts { '@esx_lib/imports.lua', '@es_extended/imports.lua', } client_scripts { 'client/main.lua' } server_scripts { 'server/main.lua' } files { 'modules/**/*.lua', 'data/*.json', } ``` <Callout type="warn"> The module loader in `@es_extended/imports.lua` uses `xLib` internally. Load `@esx_lib/imports.lua` before it, as the official ESX resources do, or loading a module file fails. </Callout> ## Module names [#module-names] - A module name is a path relative to the resource root, **without** `.lua`. - Use `.` (or `/`) to separate folders: `modules.inventory.utils` loads `modules/inventory/utils.lua`. - If no file matches, `modules/inventory/utils/init.lua` is tried. - `@resource.path` loads from another resource, for example `@my_utils.shared.math`. - Results are cached per resource. The second `require` of the same name returns the cached value. - Two modules that require each other raise a "circular-dependency" error. ## Functions [#functions] ### require / ESX.require [#require--esxrequire] ```lua require(modName) ``` | Name | Type | Description | |---|---|---| | `modName` | `string` | Module name as described above. | **Returns:** whatever the module file returns, or `true` if it returns nothing. ```lua title="modules/garage/config.lua" return { spots = { vector4(229.7, -800.1, 30.6, 160.0), vector4(233.3, -801.4, 30.6, 160.0), }, } ``` ```lua title="client/main.lua" local garage = require 'modules.garage.config' for i = 1, #garage.spots do print(garage.spots[i]) end ``` ### ESX.load [#esxload] ```lua ESX.load(filePath, env) ``` Loads and runs a Lua file like `require`, but **does not cache** it. Each call runs the file again. | Name | Type | Description | |---|---|---| | `filePath` | `string` | Module name. | | `env` | `table?` | Environment for the chunk. Default: the calling resource's environment. | **Returns:** what the file returns. Raises an error when the file is not found. ### ESX.loadJson [#esxloadjson] ```lua ESX.loadJson(filePath) ``` Reads `<filePath>.json` and decodes it. | Name | Type | Description | |---|---|---| | `filePath` | `string` | Path without `.json`. Dots become folder separators, so use `data.items` for `data/items.json`. | **Returns:** `table` with the decoded JSON. Raises an error when the file is missing. ```lua title="server/main.lua" local prices = ESX.loadJson('data.prices') print(prices.bread) ``` `ESX.load` and `ESX.loadJson` exist only when `ox_lib` is not running. With `@esx_lib/imports.lua` you can always use `xLib.load` and `xLib.loadJson`, which behave the same. ## Other client imports [#other-client-imports] `@es_extended/imports.lua` also loads `client/imports/class.lua` on the client and exposes it as `ESX.Class`. ```lua ESX.Class(body, heritage) ``` | Name | Type | Description | |---|---|---| | `body` | `table?` | Prototype with methods. A `constructor` method runs on `:new(...)`. | | `heritage` | `table?` | Parent class to inherit from. | **Returns:** the class. Create instances with `MyClass:new(...)`. If `constructor` returns a table, that table is returned instead of the instance. ```lua title="client/main.lua" local Vehicle = ESX.Class({ constructor = function(self, plate) self.plate = plate end, describe = function(self) return ('Vehicle %s'):format(self.plate) end, }) local car = Vehicle:new('ESX 123') print(car:describe()) ``` ## Related [#related] - [Points](/docs/es_extended/client/imports/point) - [esx_lib](/docs/core/esx_lib) - [Your first resource](/docs/getting-started/first-resource) - [Coding practices](/docs/getting-started/coding-practices) # Callbacks (/docs/es_extended/client/modules/callback) Callbacks let one side ask the other side for data and get an answer back. The client asks the server with `ESX.TriggerServerCallback` or `ESX.AwaitServerCallback`, and the server answers through a handler registered with `ESX.RegisterServerCallback`. The reverse direction works the same way with client callbacks. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` (implemented by `esx_lib`) | | Side | Client and server | | Source | `client/modules/callback.lua`, `server/modules/callback.lua` | In ESX Legacy 1.16 the ESX callback functions are thin wrappers around `xLib.callback` from [esx_lib](/docs/core/esx_lib). The ESX signatures (`cb` as first or second argument) still work, so old scripts do not need changes. | Direction | Register on | Trigger with | |---|---|---| | Client asks server | Server: `ESX.RegisterServerCallback` | Client: `ESX.TriggerServerCallback`, `ESX.AwaitServerCallback` | | Server asks client | Client: `ESX.RegisterClientCallback` | Server: `ESX.TriggerClientCallback`, `ESX.AwaitClientCallback` | ## Client functions [#client-functions] ### ESX.TriggerServerCallback [#esxtriggerservercallback] ```lua ESX.TriggerServerCallback(eventName, callback, ...) ``` Calls a server callback and passes the response to `callback`. Does not block. | Name | Type | Description | |---|---|---| | `eventName` | `string` | Callback name. | | `callback` | `function` | Receives the values the server passed to `cb`. | | `...` | `any` | Arguments sent to the server handler. | **Returns:** nothing ```lua title="client/main.lua" ESX.TriggerServerCallback('garage:getVehicles', function(vehicles) for i = 1, #vehicles do print(vehicles[i].plate) end end, 'pillbox') ``` ### ESX.AwaitServerCallback [#esxawaitservercallback] ```lua ESX.AwaitServerCallback(eventName, ...) ``` Calls a server callback and waits for the answer. Must run inside a thread (command handler, `CreateThread`, event handler). | Name | Type | Description | |---|---|---| | `eventName` | `string` | Callback name. | | `...` | `any` | Arguments sent to the server handler. | **Returns:** every value the server passed to `cb`. ```lua title="client/main.lua" RegisterCommand('mygarage', function() local vehicles = ESX.AwaitServerCallback('garage:getVehicles', 'pillbox') print(('You own %d vehicles here'):format(#vehicles)) end, false) ``` <Callout type="warn"> `ESX.AwaitServerCallback` throws an error when the callback does not exist or times out. Wrap it in `pcall` if your code must continue in that case. </Callout> ### ESX.RegisterClientCallback [#esxregisterclientcallback] ```lua ESX.RegisterClientCallback(eventName, callback) ``` Registers a callback the server can call on this client. The handler gets `cb` first, then the server's arguments. It is removed when your resource stops. | Name | Type | Description | |---|---|---| | `eventName` | `string` | Callback name (max. 200 characters). | | `callback` | `function` | `function(cb, ...)`. Call `cb(...)` once with the response. | **Returns:** nothing ```lua title="client/main.lua" ESX.RegisterClientCallback('police:getStreetName', function(cb) local coords = GetEntityCoords(ESX.PlayerData.ped) local street = GetStreetNameFromHashKey(GetStreetNameAtCoord(coords.x, coords.y, coords.z)) cb(street) end) ``` ### ESX.DoesClientCallbackExist [#esxdoesclientcallbackexist] ```lua ESX.DoesClientCallbackExist(eventName) ``` **Returns:** `boolean`, whether a callback with this name is registered in the local esx_lib registry. ## Server functions [#server-functions] These are listed here so both halves are in one place. The full server reference is in [Server functions](/docs/es_extended/server/functions). ### ESX.RegisterServerCallback [#esxregisterservercallback] ```lua ESX.RegisterServerCallback(eventName, callback) ``` | Name | Type | Description | |---|---|---| | `eventName` | `string` | Callback name (max. 200 characters). | | `callback` | `function` | `function(source, cb, ...)`. Call `cb(...)` once with the response. | **Returns:** nothing ```lua title="server/main.lua" ESX.RegisterServerCallback('garage:getVehicles', function(source, cb, garage) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer or type(garage) ~= 'string' then return cb({}) end local vehicles = MySQL.query.await( 'SELECT plate, vehicle FROM owned_vehicles WHERE owner = ? AND parking = ?', { xPlayer.identifier, garage } ) cb(vehicles or {}) end) ``` ### ESX.TriggerClientCallback [#esxtriggerclientcallback] ```lua ESX.TriggerClientCallback(player, eventName, callback, ...) ``` | Name | Type | Description | |---|---|---| | `player` | `number` | Server id of the target player. | | `eventName` | `string` | Client callback name. | | `callback` | `function` | Receives the client's response. | | `...` | `any` | Arguments for the client handler. | **Returns:** nothing ```lua title="server/main.lua" ESX.TriggerClientCallback(source, 'police:getStreetName', function(street) print(('Player %s is on %s'):format(source, street)) end) ``` ### ESX.AwaitClientCallback [#esxawaitclientcallback] ```lua ESX.AwaitClientCallback(player, eventName, ...) ``` Blocking version of `ESX.TriggerClientCallback`. **Returns:** the values the client passed to `cb`. ### ESX.DoesServerCallbackExist [#esxdoesservercallbackexist] ```lua ESX.DoesServerCallbackExist(eventName) ``` **Returns:** `boolean` ## Timeouts and limits [#timeouts-and-limits] | Setting | Default | Description | |---|---|---| | `esx:callbackTimeout` / `xLib:callbackTimeout` | `300000` ms | How long the caller waits. `0` disables the timeout. Set on the server; it is replicated to clients. | | `xLib:callbackValidationGrace` | `15000` ms | How long an unknown callback name is given to appear before the caller gets "does not exist". | | `xLib:callbackBurst` / `xLib:callbackRate` | `60` / `30` per second | Per-player rate limit for client to server callbacks. | | `xLib:callbackMaxInFlight` | `8` | Open client to server callbacks per player. | | `xLib:callbackMaxPayloadBytes` | `65536` | Max size of the arguments a client may send. | ```bash title="server.cfg" setr esx:callbackTimeout 30000 ``` If a client exceeds the rate limit or payload size, or a server handler takes longer than the receiver timeout (30 s unless `esx:callbackTimeout` is set), the client gets `false` as the response. ## Built-in callbacks [#built-in-callbacks] | Name | Side | Returns | |---|---|---| | `esx:getPlayerData` | Server | Identifier, accounts, inventory, job, loadout, money, position and metadata of the caller. | | `esx:getOtherPlayerData` | Server | Same data for another player id. Admins only, otherwise `nil`. | | `esx:isUserAdmin` | Server | `true` if the caller is an admin. | | `esx:getGameBuild` | Server | Value of `sv_enforceGameBuild` (default `1604`). | | `esx:getPlayerNames` | Server | Character names for a table of player ids. | | `esx:spawnVehicle` | Server | Deprecated. Admin-only vehicle spawn, prints a warning. | | `esx:GetVehicleType` | Client | Vehicle type string for a model. Used by the server-side `ESX.GetVehicleType`. | ```lua local isAdmin = ESX.AwaitServerCallback('esx:isUserAdmin') ``` ## Security [#security] <Callout type="error" title="Never trust callback arguments"> Everything a client sends to `ESX.RegisterServerCallback` can be faked by a cheater. Validate types and ranges, look the player up with `ESX.GetPlayerFromId(source)`, and check permissions and distances on the server. Never let a callback give money or items based on a value the client sent. </Callout> ## Common issues [#common-issues] - **Old code calls `cb` twice.** Only the first call is used. - **Server handler never calls `cb`.** After the receiver timeout (30 s by default) the server answers with `false`, so the client gets `false` instead of your data. - **"callback 'x' is already owned by resource 'y'".** Two resources use the same name. Prefix names with your resource name. ## Related [#related] - [Server functions](/docs/es_extended/server/functions) - [Callbacks guide](/docs/guides/callbacks) - [esx_lib](/docs/core/esx_lib) - [Client functions](/docs/es_extended/client/functions) # ESX.Game (/docs/es_extended/client/modules/game) `ESX.Game` is the client toolbox for entities: spawning and deleting vehicles and objects, finding the closest ped, player or vehicle, teleporting, and reading or applying vehicle tuning. In ESX Legacy 1.16 these functions live in [esx_lib](/docs/core/esx_lib) (`xLib.game`, `xLib.entity`) and `es_extended` maps them onto `ESX.Game` for compatibility. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` (implemented in `esx_lib`) | | Side | Client | | Mapping | `es_extended/client/compat.lua` | | Implementation | `esx_lib/imports/game/client.lua`, `esx_lib/imports/entity/client.lua` | `coords` parameters accept a `vector3` or a table with `x`, `y`, `z`. `modelFilter` parameters are sets keyed by model hash, for example ``{ [`prop_atm_01`] = true }``. ## Spawning and deleting [#spawning-and-deleting] ### ESX.Game.SpawnVehicle [#esxgamespawnvehicle] ```lua ESX.Game.SpawnVehicle(vehicleModel, coords, heading, cb, networked) ``` Loads the model, creates the vehicle, waits for collision and then returns it. The vehicle is unlocked for the player, does not need hotwiring and the radio is off. | Name | Type | Description | |---|---|---| | `vehicleModel` | `string \| number` | Model name or hash. | | `coords` | `vector3 \| table` | Spawn position. Must be within 424 units of the player. | | `heading` | `number` | Heading in degrees. | | `cb` | `function?` | `function(vehicle)`. Without it the function waits and returns the vehicle. | | `networked` | `boolean?` | Default `true`. Pass `true` explicitly to also make the network id migratable and the vehicle a mission entity. | **Returns:** `number?` vehicle handle when no `cb` is given. ```lua local coords = GetOffsetFromEntityInWorldCoords(ESX.PlayerData.ped, 0.0, 5.0, 0.0) local vehicle = ESX.Game.SpawnVehicle('sultan', coords, GetEntityHeading(ESX.PlayerData.ped)) TaskWarpPedIntoVehicle(ESX.PlayerData.ped, vehicle, -1) ``` Invalid models raise an error (or reject the internal promise when you use the return value). ### ESX.Game.SpawnLocalVehicle [#esxgamespawnlocalvehicle] ```lua ESX.Game.SpawnLocalVehicle(vehicleModel, coords, heading, cb) ``` Same as `SpawnVehicle` with `networked = false`. Only the local player sees the vehicle, which is useful for showroom previews. **Returns:** nothing, use `cb`. ### ESX.Game.SpawnObject [#esxgamespawnobject] ```lua ESX.Game.SpawnObject(object, coords, cb, networked) ``` | Name | Type | Description | |---|---|---| | `object` | `string \| number` | Model name or hash. | | `coords` | `vector3 \| table` | Position. | | `cb` | `function?` | `function(object)`. | | `networked` | `boolean?` | Default `true`. | **Returns:** the object handle, or the return value of `cb` when you pass one. ```lua local coords = GetEntityCoords(ESX.PlayerData.ped) local cone = ESX.Game.SpawnObject('prop_roadcone02a', coords) PlaceObjectOnGroundProperly(cone) ``` ### ESX.Game.SpawnLocalObject [#esxgamespawnlocalobject] ```lua ESX.Game.SpawnLocalObject(object, coords, cb) ``` Spawns a non-networked object. **Returns:** nothing, use `cb`. ### ESX.Game.DeleteVehicle [#esxgamedeletevehicle] ```lua ESX.Game.DeleteVehicle(vehicle) ``` Marks the vehicle as mission entity and deletes it. The client must own the entity for the delete to replicate. ### ESX.Game.DeleteObject [#esxgamedeleteobject] ```lua ESX.Game.DeleteObject(object) ``` Marks the object as mission entity and deletes it. ### ESX.Game.Teleport [#esxgameteleport] ```lua ESX.Game.Teleport(entity, coords, cb) ``` Requests collision at the target, waits until it is loaded, then moves the entity and sets its heading. | Name | Type | Description | |---|---|---| | `entity` | `number` | Entity handle, usually the player ped. | | `coords` | `vector3 \| vector4 \| table` | Target. Heading is read from `w` or `heading`, default `0.0`. | | `cb` | `function?` | Called when done. | ```lua ESX.Game.Teleport(ESX.PlayerData.ped, vector4(-1037.7, -2737.9, 20.2, 330.0), function() ESX.ShowNotification('Welcome to Los Santos') end) ``` ### ESX.Game.GetPedMugshot [#esxgamegetpedmugshot] ```lua ESX.Game.GetPedMugshot(ped, transparent) ``` Registers a ped headshot and waits until it is ready. | Name | Type | Description | |---|---|---| | `ped` | `number` | Ped handle. | | `transparent` | `boolean?` | Use a transparent background. | **Returns:** `mugshot` handle and `txd` string, or nothing if the ped does not exist. Free it with `UnregisterPedheadshot(mugshot)` when you are done. ```lua local mugshot, txd = ESX.Game.GetPedMugshot(ESX.PlayerData.ped) ESX.ShowAdvancedNotification('LSPD', 'ID check', 'Identity verified', txd, 1, false, true) UnregisterPedheadshot(mugshot) ``` ## Entity pools [#entity-pools] ### ESX.Game.GetObjects [#esxgamegetobjects] ```lua ESX.Game.GetObjects() ``` **Returns:** `number[]` from `GetGamePool('CObject')`. ### ESX.Game.GetPeds [#esxgamegetpeds] ```lua ESX.Game.GetPeds(onlyOtherPeds) ``` **Returns:** `number[]` from `GetGamePool('CPed')`, without the local player ped when `onlyOtherPeds` is `true`. ### ESX.Game.GetVehicles [#esxgamegetvehicles] ```lua ESX.Game.GetVehicles() ``` **Returns:** `number[]` from `GetGamePool('CVehicle')`. ### ESX.Game.GetPlayers [#esxgamegetplayers] ```lua ESX.Game.GetPlayers(onlyOtherPlayers, returnKeyValue, returnPeds) ``` | Name | Type | Description | |---|---|---| | `onlyOtherPlayers` | `boolean?` | Exclude the local player. | | `returnKeyValue` | `boolean?` | Return `{ [playerIndex] = ped }`. | | `returnPeds` | `boolean?` | Return peds instead of player indexes (array mode only). | **Returns:** `table`. Only players whose ped exists (in scope) are included. ## Closest entity [#closest-entity] All "closest" functions use the player position when `coords` is `nil` and return `-1, -1` when nothing is found. ### ESX.Game.GetClosestEntity [#esxgamegetclosestentity] ```lua ESX.Game.GetClosestEntity(entities, isPlayerEntities, coords, modelFilter) ``` | Name | Type | Description | |---|---|---| | `entities` | `table` | Entity list. For players a `{ [playerIndex] = ped }` table. | | `isPlayerEntities` | `boolean` | Return the table key (player index) instead of the entity. | | `coords` | `vector3? \| table?` | Search origin. | | `modelFilter` | `table?` | Model hash set. | **Returns:** `entity`, `distance` ### ESX.Game.GetClosestObject [#esxgamegetclosestobject] ```lua ESX.Game.GetClosestObject(coords, modelFilter) ``` **Returns:** object handle and distance. ```lua local atm, distance = ESX.Game.GetClosestObject(nil, { [`prop_atm_01`] = true, [`prop_atm_02`] = true, [`prop_fleeca_atm`] = true, }) if atm ~= -1 and distance < 1.5 then OpenBankMenu() end ``` ### ESX.Game.GetClosestPed [#esxgamegetclosestped] ```lua ESX.Game.GetClosestPed(coords, modelFilter) ``` **Returns:** ped handle and distance. The local player ped is excluded. ### ESX.Game.GetClosestPlayer [#esxgamegetclosestplayer] ```lua ESX.Game.GetClosestPlayer(coords) ``` **Returns:** client player index and distance. The local player is excluded. There is no model filter. ```lua local player, distance = ESX.Game.GetClosestPlayer() if player ~= -1 and distance <= 3.0 then TriggerServerEvent('police:handcuff', GetPlayerServerId(player)) else ESX.ShowNotification('Nobody nearby', 'error') end ``` ### ESX.Game.GetClosestVehicle [#esxgamegetclosestvehicle] ```lua ESX.Game.GetClosestVehicle(coords, modelFilter) ``` **Returns:** vehicle handle and distance. ### ESX.Game.GetPlayersInArea [#esxgamegetplayersinarea] ```lua ESX.Game.GetPlayersInArea(coords, maxDistance) ``` **Returns:** array of client player indexes within `maxDistance` (local player excluded). ### ESX.Game.GetVehiclesInArea [#esxgamegetvehiclesinarea] ```lua ESX.Game.GetVehiclesInArea(coords, maxDistance) ``` **Returns:** array of vehicle handles within `maxDistance`. ### ESX.Game.IsSpawnPointClear [#esxgameisspawnpointclear] ```lua ESX.Game.IsSpawnPointClear(coords, maxDistance) ``` **Returns:** `true` when no vehicle is within `maxDistance` of `coords`. ```lua local spot = vector3(229.7, -800.1, 30.6) if ESX.Game.IsSpawnPointClear(spot, 3.0) then ESX.Game.SpawnVehicle('blista', spot, 160.0) else ESX.ShowNotification('The parking spot is blocked', 'error') end ``` ### ESX.Game.GetVehicleInDirection [#esxgamegetvehicleindirection] ```lua ESX.Game.GetVehicleInDirection() ``` Casts a short ray from the camera. **Returns:** `vehicle`, `coords` of the hit when the ray hits a vehicle, otherwise nothing. ### ESX.Game.IsVehicleEmpty [#esxgameisvehicleempty] ```lua ESX.Game.IsVehicleEmpty(vehicle) ``` **Returns:** `true` when the vehicle has no passengers and the driver seat is free. ### EnumerateEntitiesWithinDistance [#enumerateentitieswithindistance] ```lua EnumerateEntitiesWithinDistance(entities, isPlayerEntities, coords, maxDistance) ``` Global helper (no `ESX.` prefix) that returns every entity (or player key) within `maxDistance`. ## Vehicle properties [#vehicle-properties] ### ESX.Game.GetVehicleProperties [#esxgamegetvehicleproperties] ```lua ESX.Game.GetVehicleProperties(vehicle) ``` Reads the full state of a vehicle into a table you can store in `owned_vehicles.vehicle`. **Returns:** `table|nil` (`nil` if the vehicle does not exist). | Group | Keys | |---|---| | Identity | `model`, `plate` (trimmed), `plateIndex` | | Condition | `bodyHealth`, `engineHealth`, `tankHealth`, `fuelLevel`, `oilLevel`, `dirtLevel`, `paintFade` | | Damage | `doorsBroken`, `doorsAngleRatio`, `windowsBroken`, `tyreBurst`, `tyreBurstComplete`, `wheelHealth`, `wheelsBroken`, `tyresCanBurst`, `driftTyres` | | Paint | `color1`, `color2` (number or RGB table), `paintType1`, `paintType2`, `pearlescentColor`, `wheelColor`, `dashboardColor`, `interiorColor` | | Wheels and lights | `wheels`, `wheelSize`, `wheelWidth`, `windowTint`, `xenonColor`, `customXenonColor`, `customXenonColorEnabled`, `neonEnabled`, `neonColor`, `tyreSmokeColor` | | Other | `roofState`, `extras` | | Mods | `modSpoilers` ... `modLightbar` (one key per mod slot, for example `modEngine`, `modBrakes`, `modTransmission`, `modSuspension`, `modArmor`), toggles `modNitrous`, `modTurbo`, `modSubwoofer`, `modSmokeEnabled`, `modHydraulics`, `modXenon`, plus `modCustomFrontWheels`, `modCustomBackWheels`, `modRoofLivery`, `modDoorR`, `modLivery`, `modLiveryIndex`, `livery` | Damage tables are keyed by index as string (`'0'`, `'1'`, ...), so they survive JSON encoding. ```lua local vehicle = GetVehiclePedIsIn(ESX.PlayerData.ped, false) local props = ESX.Game.GetVehicleProperties(vehicle) if props then TriggerServerEvent('garage:store', props.plate, props) end ``` ### ESX.Game.SetVehicleProperties [#esxgamesetvehicleproperties] ```lua ESX.Game.SetVehicleProperties(vehicle, props) ``` Applies a properties table. Only keys present in `props` are changed. Sets the mod kit and waits up to 5 seconds for mods to load. Must run in a thread. | Name | Type | Description | |---|---|---| | `vehicle` | `number` | Vehicle handle. | | `props` | `table` | Properties as returned by `GetVehicleProperties`. | ```lua local function spawnOwnedPreview(props) local spawn = vector4(215.8, -810.1, 30.7, 157.0) ESX.Game.SpawnVehicle(props.model, spawn.xyz, spawn.w, function(vehicle) ESX.Game.SetVehicleProperties(vehicle, props) end) end ``` For vehicles spawned on the server, setting the `VehicleProperties` state bag on the entity makes the owning client apply the properties automatically. ## Utilities [#utilities] ### ESX.Game.Utils.DrawText3D [#esxgameutilsdrawtext3d] ```lua ESX.Game.Utils.DrawText3D(coords, text, size, font) ``` Draws text at a world position, scaled by camera distance and FOV. Call it every frame. | Name | Type | Description | |---|---|---| | `coords` | `vector3 \| table` | World position. | | `text` | `string` | Text. | | `size` | `number?` | Scale factor, default `1`. | | `font` | `number?` | Font id, default `0`. | ```lua CreateThread(function() local npc = vector3(-265.0, -963.6, 31.2) while true do local sleep = 500 if #(GetEntityCoords(ESX.PlayerData.ped) - npc) < 10.0 then sleep = 0 ESX.Game.Utils.DrawText3D(npc + vector3(0.0, 0.0, 1.0), 'Job center') end Wait(sleep) end end) ``` <Callout type="warn" title="Removed raycast helpers"> `ESX.Game.GetShapeTestResultSync`, `ESX.Game.RaycastScreen` and `ESX.Game.StartRaycasting` no longer exist in 1.16. `ESX.Game.StopRaycasting`, `ESX.Game.IsRaycastActive` and `ESX.Game.GetRaycastResult` remain only as compatibility stubs. Use the raycast module of [esx_lib](/docs/core/esx_lib) or the native `StartShapeTestLosProbe` instead. </Callout> ## Related [#related] - [Streaming](/docs/es_extended/client/modules/streaming) - [OneSync server functions](/docs/es_extended/server/onesync) - [Client functions](/docs/es_extended/client/functions) - [esx_lib](/docs/core/esx_lib) # Interactions (/docs/es_extended/client/modules/interactions) Interactions connect an action to the shared ESX interact key. You register a name, a function to run on key press and an optional condition. When the player presses the key, every interaction whose condition returns `true` runs. Combined with [points](/docs/es_extended/client/imports/point) this replaces the old `while true do ... IsControlJustReleased(0, 38)` loops. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` (implemented in `esx_lib`) | | Side | Client | | Key mapping | `+esx_interact`, default key `E`, label "Interact" | Interactions are removed automatically when the resource that registered them stops. ## Functions [#functions] ### ESX.RegisterInteraction [#esxregisterinteraction] ```lua ESX.RegisterInteraction(name, onPress, condition) ``` | Name | Type | Description | |---|---|---| | `name` | `string` | Unique name. Registering the same name again replaces the old interaction. | | `onPress` | `function` | Runs when the key is pressed and the condition passes. | | `condition` | `function?` | Returns `true` when the interaction is allowed. Default: always `true`. Errors in it count as `false`. | **Returns:** nothing ```lua title="client/main.lua" local nearArmory = false local armory = ESX.Point:new({ coords = vector3(452.6, -980.0, 30.7), distance = 1.5, enter = function() nearArmory = true ESX.TextUI(('[%s] Open armory'):format(ESX.GetInteractKey())) end, leave = function() nearArmory = false ESX.HideUI() end, }) ESX.RegisterInteraction('police_armory', function() OpenArmoryMenu() end, function() return nearArmory and ESX.PlayerData.job.name == 'police' end) ``` <Callout type="warn"> Without a condition, `onPress` runs on every key press anywhere on the map. Always pass a condition unless that is what you want. </Callout> ### ESX.RemoveInteraction [#esxremoveinteraction] ```lua ESX.RemoveInteraction(name) ``` | Name | Type | Description | |---|---|---| | `name` | `string` | Name used in `RegisterInteraction`. | **Returns:** nothing ```lua ESX.RemoveInteraction('police_armory') ``` ### ESX.GetInteractKey [#esxgetinteractkey] ```lua ESX.GetInteractKey() ``` **Returns:** `string`, the label of the key currently bound to `+esx_interact` (for example `E`). ```lua ESX.ShowHelpNotification(('Press %s to talk'):format(ESX.GetInteractKey()), true) ``` ## Common issues [#common-issues] - **Two interactions fire at once.** Every interaction with a passing condition runs. Make conditions specific (distance, job, vehicle state). - **Interaction stops working after a restart.** It belongs to the resource that registered it. Register it again when your resource starts. ## Related [#related] - [Points](/docs/es_extended/client/imports/point) - [Client UI (TextUI)](/docs/es_extended/client/ui) - [Client functions (RegisterInput)](/docs/es_extended/client/functions) - [esx_lib](/docs/core/esx_lib) # Scaleform (/docs/es_extended/client/modules/scaleform) `ESX.Scaleform` shows a few common GTA V scaleform screens with one call and gives you two helpers to work with any other scaleform movie. Use it for big on-screen announcements such as "WASTED"-style shards, a news ticker or a warning popup. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` (implemented in `esx_lib` as `xLib.scaleform`) | | Side | Client | The four `Show*` functions **block** the calling thread for `sec` seconds while they draw the movie every frame, then release it. ## Functions [#functions] ### ESX.Scaleform.ShowFreemodeMessage [#esxscaleformshowfreemodemessage] ```lua ESX.Scaleform.ShowFreemodeMessage(title, msg, sec) ``` Shows the big centered shard of the `MP_BIG_MESSAGE_FREEMODE` movie. | Name | Type | Description | |---|---|---| | `title` | `string` | Large title. | | `msg` | `string` | Subtitle. | | `sec` | `number` | Duration in seconds. | **Returns:** nothing ```lua CreateThread(function() ESX.Scaleform.ShowFreemodeMessage('~g~HEIST COMPLETE', 'You earned $50,000', 5) end) ``` ### ESX.Scaleform.ShowBreakingNews [#esxscaleformshowbreakingnews] ```lua ESX.Scaleform.ShowBreakingNews(title, msg, bottom, sec) ``` Shows the Weazel News `BREAKING_NEWS` overlay. | Name | Type | Description | |---|---|---| | `title` | `string` | Scrolling ticker text. | | `msg` | `string` | Main headline. | | `bottom` | `string` | Line under the headline. | | `sec` | `number` | Duration in seconds. | **Returns:** nothing ```lua CreateThread(function() ESX.Scaleform.ShowBreakingNews('Pacific Standard robbed', 'BANK ROBBERY', 'Police on scene', 10) end) ``` ### ESX.Scaleform.ShowPopupWarning [#esxscaleformshowpopupwarning] ```lua ESX.Scaleform.ShowPopupWarning(title, msg, bottom, sec) ``` Shows the full-screen `POPUP_WARNING` alert. | Name | Type | Description | |---|---|---| | `title` | `string` | Title. | | `msg` | `string` | Message. | | `bottom` | `string` | Footer text. | | `sec` | `number` | Duration in seconds. | **Returns:** nothing ```lua CreateThread(function() ESX.Scaleform.ShowPopupWarning('Server restart', 'The server restarts in 5 minutes.', 'Please finish your roleplay.', 8) end) ``` ### ESX.Scaleform.ShowTrafficMovie [#esxscaleformshowtrafficmovie] ```lua ESX.Scaleform.ShowTrafficMovie(sec) ``` Plays the `TRAFFIC_CAM` movie full screen. | Name | Type | Description | |---|---|---| | `sec` | `number` | Duration in seconds. | **Returns:** nothing ## Utils [#utils] ### ESX.Scaleform.Utils.RequestScaleformMovie [#esxscaleformutilsrequestscaleformmovie] ```lua ESX.Scaleform.Utils.RequestScaleformMovie(movie) ``` Requests a scaleform movie and waits until it is loaded. There is no timeout, so only use valid movie names. | Name | Type | Description | |---|---|---| | `movie` | `string` | Movie name, for example `MIDSIZED_MESSAGE`. | **Returns:** `number` scaleform handle. ### ESX.Scaleform.Utils.RunMethod [#esxscaleformutilsrunmethod] ```lua ESX.Scaleform.Utils.RunMethod(scaleform, methodName, returnValue, ...) ``` Calls a method on a scaleform. If you pass a movie name instead of a handle, the movie is loaded first. Arguments are pushed by type: integers, floats, strings and booleans. | Name | Type | Description | |---|---|---| | `scaleform` | `number \| string` | Handle or movie name. | | `methodName` | `string` | Method to call. | | `returnValue` | `boolean?` | When `true`, end with `EndScaleformMovieMethodReturnValue`. | | `...` | `number \| string \| boolean` | Method arguments. | **Returns:** the scaleform handle, plus the return value handle when `returnValue` is `true`. You must release the movie yourself with `SetScaleformMovieAsNoLongerNeeded`. ```lua CreateThread(function() local scaleform = ESX.Scaleform.Utils.RunMethod('MIDSIZED_MESSAGE', 'SHOW_SHARD_MIDSIZED_MESSAGE', false, 'Shift started', 'Drive to the depot') local endTime = GetGameTimer() + 4000 while GetGameTimer() < endTime do DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0) Wait(0) end SetScaleformMovieAsNoLongerNeeded(scaleform) end) ``` <Callout type="info"> Floats and integers are different in Lua 5.4. Write `500.0` when a scaleform method expects a float and `500` when it expects an integer. </Callout> ## Related [#related] - [Client UI](/docs/es_extended/client/ui) - [Streaming](/docs/es_extended/client/modules/streaming) - [esx_lib](/docs/core/esx_lib) - [FiveM native reference](https://docs.fivem.net/natives/) # Streaming (/docs/es_extended/client/modules/streaming) `ESX.Streaming` wraps the GTA "request and wait until loaded" pattern. Each function requests an asset, waits until it is loaded and returns it, or returns `nil` if loading fails. Use it before you spawn a model, play an animation or draw a texture. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` (implemented in `esx_lib` as `xLib.streaming`) | | Side | Client | | Timeout | `xLib:streamingTimeout` convar, default `10000` ms, `0` waits forever | Every function has the same shape: `(asset, cb?)`. With `cb`, the function calls `cb(asset)` and returns its result. Without `cb`, it returns the asset. On timeout it returns `nil` and `cb` is not called. The functions block, so call them from a thread. ## Functions [#functions] ### ESX.Streaming.RequestModel [#esxstreamingrequestmodel] ```lua ESX.Streaming.RequestModel(modelHash, cb) ``` | Name | Type | Description | |---|---|---| | `modelHash` | `number \| string` | Model hash or name. Names are hashed with `joaat`. | | `cb` | `function?` | `function(modelHash)`. | **Returns:** `number|nil` model hash. `nil` if the model does not exist or timed out. ```lua local model = ESX.Streaming.RequestModel('a_m_y_business_01') if model then local ped = CreatePed(4, model, 240.0, -862.0, 29.7, 340.0, false, true) SetModelAsNoLongerNeeded(model) end ``` ### ESX.Streaming.RequestAnimDict [#esxstreamingrequestanimdict] ```lua ESX.Streaming.RequestAnimDict(animDict, cb) ``` | Name | Type | Description | |---|---|---| | `animDict` | `string` | Animation dictionary. | | `cb` | `function?` | `function(animDict)`. | **Returns:** `string|nil` ```lua ESX.Streaming.RequestAnimDict('mp_arresting', function(dict) TaskPlayAnim(ESX.PlayerData.ped, dict, 'idle', 8.0, -8.0, -1, 49, 0.0, false, false, false) RemoveAnimDict(dict) end) ``` ### ESX.Streaming.RequestAnimSet [#esxstreamingrequestanimset] ```lua ESX.Streaming.RequestAnimSet(animSet, cb) ``` Loads a movement clipset (walk style). | Name | Type | Description | |---|---|---| | `animSet` | `string` | Clipset name, for example `move_m@drunk@verydrunk`. | | `cb` | `function?` | `function(animSet)`. | **Returns:** `string|nil` ```lua local walk = ESX.Streaming.RequestAnimSet('move_m@drunk@verydrunk') if walk then SetPedMovementClipset(ESX.PlayerData.ped, walk, 1.0) end ``` ### ESX.Streaming.RequestStreamedTextureDict [#esxstreamingrequeststreamedtexturedict] ```lua ESX.Streaming.RequestStreamedTextureDict(textureDict, cb) ``` | Name | Type | Description | |---|---|---| | `textureDict` | `string` | Texture dictionary. | | `cb` | `function?` | `function(textureDict)`. | **Returns:** `string|nil` ```lua CreateThread(function() local dict = ESX.Streaming.RequestStreamedTextureDict('commonmenu') if not dict then return end while showLogo do DrawSprite(dict, 'shop_box_tick', 0.9, 0.1, 0.03, 0.05, 0.0, 255, 255, 255, 255) Wait(0) end SetStreamedTextureDictAsNoLongerNeeded(dict) end) ``` ### ESX.Streaming.RequestNamedPtfxAsset [#esxstreamingrequestnamedptfxasset] ```lua ESX.Streaming.RequestNamedPtfxAsset(assetName, cb) ``` | Name | Type | Description | |---|---|---| | `assetName` | `string` | Particle asset, for example `core`. | | `cb` | `function?` | `function(assetName)`. | **Returns:** `string|nil` ```lua ESX.Streaming.RequestNamedPtfxAsset('core', function(asset) UseParticleFxAsset(asset) local coords = GetEntityCoords(ESX.PlayerData.ped) StartParticleFxNonLoopedAtCoord('exp_grd_bzgas_smoke', coords.x, coords.y, coords.z, 0.0, 0.0, 0.0, 1.0, false, false, false) end) ``` ### ESX.Streaming.RequestWeaponAsset [#esxstreamingrequestweaponasset] ```lua ESX.Streaming.RequestWeaponAsset(weaponHash, cb) ``` | Name | Type | Description | |---|---|---| | `weaponHash` | `number` | Weapon hash, for example `` `WEAPON_PISTOL` ``. | | `cb` | `function?` | `function(weaponHash)`. | **Returns:** `number|nil` ```lua local weapon = ESX.Streaming.RequestWeaponAsset(`WEAPON_PISTOL`) ``` <Callout type="info" title="More in esx_lib"> `xLib.streaming` also has `requestModelWithSpinner(model, message, cb)` and `requestAudioBank(bankName, cb)`. They are not mapped to `ESX.Streaming`. Load `@esx_lib/imports.lua` in your manifest to use them through `xLib`. </Callout> ## Common issues [#common-issues] - **Function returns `nil` for a custom vehicle or ped.** The addon resource is not started or the model name is wrong. Check `IsModelInCdimage`. - **Memory grows over time.** Release assets with the matching `SetModelAsNoLongerNeeded` / `Remove*` native. ## Related [#related] - [ESX.Game](/docs/es_extended/client/modules/game) - [Scaleform](/docs/es_extended/client/modules/scaleform) - [esx_lib](/docs/core/esx_lib) - [Performance guide](/docs/guides/performance) # Client UI (/docs/es_extended/client/ui) The client UI functions show feedback to the player: GTA notifications and help text, the ESX notify, TextUI, progress bar and context menu resources, and the classic `ESX.UI.Menu` system used by older menu resources. All of them run on the client. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` | | Side | Client | | Source | `client/modules/ui/*.lua` | | Optional dependencies | `esx_notify`, `esx_textui`, `esx_progressbar`, `esx_context`, menu resources | `ESX.ShowNotification`, `ESX.TextUI`, `ESX.Progressbar` and the context functions call exports of separate resources. If the resource was not present when `es_extended` started, the call raises `Resource [name] is Missing!`. ## Notifications [#notifications] ### ESX.ShowNotification [#esxshownotification] ```lua ESX.ShowNotification(message, notifyType, length, title, position) ``` Shows a notification through `esx_notify`. | Name | Type | Description | |---|---|---| | `message` | `string` | Text. `~br~` becomes a line break. | | `notifyType` | `string?` | `info` (default), `success`, `error` or `warning`. | | `length` | `number?` | Duration in milliseconds. Default `5000`. | | `title` | `string?` | Title. esx_notify shows "New Notification" when empty. | | `position` | `string?` | `top-right`, `top-left`, `top-middle`, `bottom-right`, `bottom-left`, `bottom-middle`, `middle-left` or `middle-right`. Falls back to the esx_notify config. | **Returns:** nothing ```lua ESX.ShowNotification('Vehicle stored in the garage', 'success', 4000, 'Garage') ``` From the server, use `xPlayer.showNotification(...)` or trigger `esx:showNotification` with the same arguments. ### ESX.ShowAdvancedNotification [#esxshowadvancednotification] ```lua ESX.ShowAdvancedNotification(sender, subject, msg, textureDict, iconType, flash, saveToBrief, hudColorIndex) ``` Shows a native GTA feed notification with a picture, like a phone message. | Name | Type | Description | |---|---|---| | `sender` | `string` | Title line. | | `subject` | `string` | Subtitle line. | | `msg` | `string` | Message body. | | `textureDict` | `string` | Texture dictionary of the picture, for example `CHAR_BANK_MAZE`. Used as dictionary and texture name. | | `iconType` | `number` | Small icon next to the title (`1` chat, `2` email, `3` friend request, `7` arrow, `8` RP, `9` dollar). | | `flash` | `boolean` | Passed to the ticker native. | | `saveToBrief` | `boolean?` | Store in the pause menu brief. Default `true`. | | `hudColorIndex` | `number?` | Background [HUD color](https://docs.fivem.net/docs/game-references/hud-colors/). | **Returns:** nothing ```lua ESX.ShowAdvancedNotification('Maze Bank', 'Transfer', 'You received $2,500', 'CHAR_BANK_MAZE', 9, false, true, 140) ``` ### ESX.ShowHelpNotification [#esxshowhelpnotification] ```lua ESX.ShowHelpNotification(msg, thisFrame, beep, duration) ``` Shows the black help box in the top left corner. | Name | Type | Description | |---|---|---| | `msg` | `string` | Text. Input tokens such as `~INPUT_CONTEXT~` work. | | `thisFrame` | `boolean?` | Show only for the current frame. Call it every frame in a loop. | | `beep` | `boolean?` | Play the beep sound. Default `true`. Ignored with `thisFrame`. | | `duration` | `number?` | Duration in ms. Default `-1` (until replaced). Ignored with `thisFrame`. | **Returns:** nothing ```lua CreateThread(function() while isNearShop do ESX.ShowHelpNotification('Press ~INPUT_CONTEXT~ to open the shop', true) Wait(0) end end) ``` ### ESX.ShowFloatingHelpNotification [#esxshowfloatinghelpnotification] ```lua ESX.ShowFloatingHelpNotification(msg, coords) ``` Shows a help bubble anchored at a world position. Call it every frame. | Name | Type | Description | |---|---|---| | `msg` | `string` | Text. | | `coords` | `vector3` | World position. | **Returns:** nothing ```lua local shop = vector3(25.7, -1347.3, 29.5) CreateThread(function() while true do local sleep = 500 if #(GetEntityCoords(ESX.PlayerData.ped) - shop) < 3.0 then sleep = 0 ESX.ShowFloatingHelpNotification('Shop', shop) end Wait(sleep) end end) ``` ### ESX.DrawMissionText [#esxdrawmissiontext] ```lua ESX.DrawMissionText(msg, time) ``` Shows subtitle text at the bottom of the screen, like mission instructions. | Name | Type | Description | |---|---|---| | `msg` | `string` | Text. | | `time` | `number` | Duration in milliseconds. | **Returns:** nothing ```lua ESX.DrawMissionText('Deliver the package to ~y~Sandy Shores~s~', 6000) ``` ### ESX.UI.ShowInventoryItemNotification [#esxuishowinventoryitemnotification] ```lua ESX.UI.ShowInventoryItemNotification(add, item, count) ``` Shows the small "+1 Bread" / "-1 Bread" item popup of the built-in inventory UI. The server triggers it through the secure event `esx:showInventoryItemNotification`. | Name | Type | Description | |---|---|---| | `add` | `boolean` | `true` for added, `false` for removed. | | `item` | `string` | Label to display. | | `count` | `number` | Amount. | **Returns:** nothing ## TextUI [#textui] ### ESX.TextUI [#esxtextui] ```lua ESX.TextUI(message, notifyType) ``` Shows a persistent text box through `esx_textui` until you hide it. | Name | Type | Description | |---|---|---| | `message` | `string` | Text to show. | | `notifyType` | `string?` | Style, default `info`. | **Returns:** nothing ### ESX.HideUI [#esxhideui] ```lua ESX.HideUI() ``` Hides the TextUI box. ```lua local point = ESX.Point:new({ coords = vector3(441.0, -981.0, 30.7), distance = 2.0, enter = function() ESX.TextUI(('[%s] Open armory'):format(ESX.GetInteractKey())) end, leave = function() ESX.HideUI() end, }) ``` ## Progress bar [#progress-bar] ### ESX.Progressbar [#esxprogressbar] ```lua ESX.Progressbar(message, length, options) ``` Starts a progress bar through `esx_progressbar`. Only one can run at a time. | Name | Type | Description | |---|---|---| | `message` | `string` | Label. | | `length` | `number?` | Duration in ms. Default `3000`. | | `options` | `table?` | See below. | | Option | Type | Description | |---|---|---| | `FreezePlayer` | `boolean?` | Freeze the ped while running. | | `animation` | `table?` | `{ type = 'anim', dict = '...', lib = '...' }` or `{ type = 'Scenario', Scenario = '...' }`. | | `onFinish` | `function?` | Called when the bar completes. | | `onCancel` | `function?` | Called when the bar is cancelled. | **Returns:** `boolean`, `false` if another progress bar is running. ```lua ESX.Progressbar('Repairing vehicle', 8000, { FreezePlayer = true, animation = { type = 'Scenario', Scenario = 'PROP_HUMAN_BUM_BIN' }, onFinish = function() TriggerServerEvent('mechanic:repairDone') end, onCancel = function() ESX.ShowNotification('Repair cancelled', 'error') end, }) ``` ### ESX.CancelProgressbar [#esxcancelprogressbar] ```lua ESX.CancelProgressbar() ``` Cancels the running progress bar and calls its `onCancel`. ## Context menu [#context-menu] These wrap the `esx_context` exports. ESX strips HTML tags from the `title`, `description`, `label` and `inputPlaceholder` fields of every element before it opens or refreshes the menu. Element format and callbacks are documented on [esx_context](/docs/core/esx_context). ### ESX.OpenContext [#esxopencontext] ```lua ESX.OpenContext(position, elements, onSelect, onClose, canClose) ``` | Name | Type | Description | |---|---|---| | `position` | `string` | `left`, `center` or `right`. | | `elements` | `table[]` | Menu elements. | | `onSelect` | `function?` | `function(menu, element)` when an element is selected. | | `onClose` | `function?` | `function(menu)` when the menu closes. | | `canClose` | `boolean?` | Whether the player may close it with Escape. Default `true`. | ```lua ESX.OpenContext('right', { { unselectable = true, icon = 'fas fa-car', title = 'Garage' }, { icon = 'fas fa-warehouse', title = 'Store vehicle', value = 'store' }, { icon = 'fas fa-key', title = 'Take out vehicle', value = 'take' }, }, function(menu, element) if element.value == 'store' then StoreVehicle() end ESX.CloseContext() end) ``` ### ESX.PreviewContext [#esxpreviewcontext] ```lua ESX.PreviewContext(position, elements, onSelect, onClose, canClose) ``` Opens the context menu as a preview without taking NUI focus. ### ESX.CloseContext [#esxclosecontext] ```lua ESX.CloseContext() ``` Closes the open context menu. ### ESX.RefreshContext [#esxrefreshcontext] ```lua ESX.RefreshContext(elements, position) ``` Replaces the elements of the open context menu. ## ESX.UI.Menu [#esxuimenu] `ESX.UI.Menu` is the classic menu API. Menu resources such as [esx_menu_default](/docs/core/esx_menu_default), [esx_menu_dialog](/docs/core/esx_menu_dialog) and [esx_menu_list](/docs/core/esx_menu_list) register a type, and scripts open menus of that type. ### ESX.UI.Menu.RegisterType [#esxuimenuregistertype] ```lua ESX.UI.Menu.RegisterType(menuType, open, close) ``` | Name | Type | Description | |---|---|---| | `menuType` | `string` | Type key, for example `default` or `dialog`. | | `open` | `function` | `function(namespace, name, data)` that renders the menu. | | `close` | `function` | `function(namespace, name)` that hides it. | **Returns:** nothing ### ESX.UI.Menu.Open [#esxuimenuopen] ```lua ESX.UI.Menu.Open(menuType, namespace, name, data, submit, cancel, change, close) ``` | Name | Type | Description | |---|---|---| | `menuType` | `string` | Registered type. | | `namespace` | `string` | Usually `GetCurrentResourceName()`. Menus of a stopped resource are closed automatically. | | `name` | `string` | Menu name, unique within the namespace. | | `data` | `table` | Menu data, for example `title`, `align`, `elements`. | | `submit` | `function?` | `function(data, menu)` when an element is selected. | | `cancel` | `function?` | `function(data, menu)` when the player backs out. | | `change` | `function?` | `function(data, menu)` when the selection or a value changes. | | `close` | `function?` | Called after `menu.close()`. | **Returns:** the menu object with `type`, `namespace`, `resourceName`, `name`, `data` and these methods: | Method | Description | |---|---| | `menu.close()` | Close this menu. | | `menu.update(query, newData)` | Merge `newData` into every element that matches all keys of `query`. | | `menu.refresh()` | Re-render with the current `menu.data`. | | `menu.setElement(i, key, val)` | Set one key on element `i`. | | `menu.setElements(elements)` | Replace all elements. | | `menu.setTitle(title)` | Change the title. | | `menu.removeElement(query)` | Remove the first element that matches a key of `query`. | After `update`, `setElement`, `setElements`, `setTitle` or `removeElement`, call `menu.refresh()` to show the change. ```lua ESX.UI.Menu.Open('default', GetCurrentResourceName(), 'armory', { title = 'Armory', align = 'top-left', elements = { { label = 'Pistol', value = 'WEAPON_PISTOL' }, { label = 'Nightstick', value = 'WEAPON_NIGHTSTICK' }, }, }, function(data, menu) TriggerServerEvent('police:takeWeapon', data.current.value) menu.close() end, function(data, menu) menu.close() end) ``` ### ESX.UI.Menu.Close [#esxuimenuclose] ```lua ESX.UI.Menu.Close(menuType, namespace, name, cancel) ``` Closes a specific menu. With `cancel = true` the menu's `cancel` callback runs instead of `close`. ### ESX.UI.Menu.CloseAll [#esxuimenucloseall] ```lua ESX.UI.Menu.CloseAll(cancel) ``` Closes every open menu. With `cancel = true`, each menu's `cancel` callback runs. ### ESX.UI.Menu.GetOpened [#esxuimenugetopened] ```lua ESX.UI.Menu.GetOpened(menuType, namespace, name) ``` **Returns:** the menu object, or `nil` if it is not open. `ESX.UI.Menu.IsOpen` is an alias, so it also returns the menu (truthy) or `nil`. ```lua if ESX.UI.Menu.IsOpen('default', GetCurrentResourceName(), 'armory') then ESX.UI.Menu.Close('default', GetCurrentResourceName(), 'armory') end ``` ### ESX.UI.Menu.GetOpenedMenus [#esxuimenugetopenedmenus] ```lua ESX.UI.Menu.GetOpenedMenus() ``` **Returns:** the internal array of open menus (`ESX.UI.Menu.Opened`). Closed menus leave `nil` holes, so iterate with `pairs`. ## Common issues [#common-issues] - **"Resource [esx_context] is Missing!"**: the helper resource is not installed or starts after `es_extended`. Fix the start order in `server.cfg`. - **Menu does not open**: no resource registered the menu type. Start the matching menu resource. - **HTML in context titles disappears**: ESX strips tags on purpose. Use plain text. ## Related [#related] - [Client functions](/docs/es_extended/client/functions) - [esx_notify](/docs/core/esx_notify) - [esx_textui](/docs/core/esx_textui) - [esx_progressbar](/docs/core/esx_progressbar) - [esx_context](/docs/core/esx_context) # Commands (/docs/es_extended/commands) es_extended registers a set of chat commands for staff and players. Admin commands handle teleporting, money, items, jobs and groups. A few player commands show information about your character. Who may run each command is set in `Config.CommandPermissions`, and every command is registered through [`ESX.RegisterCommand`](/docs/es_extended/server/functions#registercommand). ## Overview [#overview] | Fact | Value | | --- | --- | | Defined in | `es_extended/server/modules/commands/registrations/*.lua` | | Permissions | `Config.CommandPermissions` in [shared/config/main.lua](/docs/es_extended/config#commandpermissions) | | Default admin groups | `owner`, `admin` | | Player target | Server ID, or `me` for yourself | | Logging | Most admin commands log to the `UserActions` webhook when `Config.AdminLogging = true` | ## How permissions work [#how-permissions-work] When a command is registered, ESX runs `add_ace group.<group> command.<name> allow` for every group in its `Config.CommandPermissions` entry. Every loaded player gets the principal `identifier.<license>` → `group.<their group>`, so a player in group `admin` may run all commands that list `admin`. ```lua title="shared/config/main.lua" Config.CommandPermissions = { ["car"] = { "owner", "admin", "mod" }, -- let the "mod" group spawn cars ["clear"] = { "user", "admin", "owner" }, -- ... } ``` <Callout type="warn"> A command whose key is missing from `Config.CommandPermissions` falls back to `admin` only and prints a warning. Aliases share the permission of the main name, for example `/tp` uses the `setcoords` entry. </Callout> Arguments marked "player" accept a server ID of an online player or `me`. When an argument has the wrong type, ESX shows an error notification (or prints it in the console) and does not run the command. ## Player commands [#player-commands] Default permission: `user`, `admin`, `owner`. | Command | Aliases | Arguments | Console | What it does | | --- | --- | --- | --- | --- | | `/clear` | `/cls` | none | No | Clears your own chat. | | `/group` | | none | No | Prints your name and group to the **server console**. | | `/job` | | none | No | Prints your job name, grade label and duty state to the server console. | | `/info` | | none | No | Prints your server ID, name, group and job name to the server console. | | `/playtime` | | none | No | Prints the total playtime of your character in days, hours and minutes to the server console. | ## Teleport and movement [#teleport-and-movement] Default permission: `owner`, `admin`. | Command | Aliases | Arguments | Console | What it does | | --- | --- | --- | --- | --- | | `/setcoords` | `/tp` | `x` `y` `z` (coordinates) | No | Teleports you to the coordinates. Commas are ignored, so `/tp 215.7, -810.1, 30.7` works. | | `/tpm` | | none | No | Teleports you to your map waypoint. | | `/goto` | | `playerId` (player) | No | Teleports you to the player and moves you into their routing bucket. | | `/bring` | | `playerId` (player) | No | Teleports the player to you and moves them into your routing bucket. | | `/noclip` | | none | No | Toggles noclip for yourself. | | `/coords` | | none | No | Prints your position as `vector3` and `vector4` to the server console. | | `/setdim` | `/setbucket` | `playerId` (player), `dimension` (number) | Yes | Sets the player's routing bucket. Bucket `0` is the default world. | ```txt /setcoords -1037.5 -2737.6 20.2 /goto 12 /setbucket me 5 ``` ## Player control [#player-control] | Command | Arguments | Console | What it does | | --- | --- | --- | --- | | `/kill` | `playerId` (player) | Yes | Kills the player. | | `/freeze` | `playerId` (player) | Yes | Freezes the player in place. | | `/unfreeze` | `playerId` (player) | Yes | Unfreezes the player. | | `/setjob` | `playerId` (player), `job` (string), `grade` (number) | Yes | Sets job and grade. Fails with an error if the job or grade does not exist. Duty uses `Config.DefaultJobDuty`. | | `/setgroup` | `playerId` (player), `group` (string) | Yes | Sets the permission group. `superadmin` is changed to `admin`. | | `/save` | `playerId` (player) | Yes | Saves the player to the database now. | | `/saveall` | none | Yes | Saves all online players. | | `/players` | none | Yes | Prints ID, name, group and identifier of every online player to the console. | ```txt /setjob 4 police 2 /setgroup 4 admin ``` ## Money [#money] | Command | Arguments | Console | What it does | | --- | --- | --- | --- | | `/setaccountmoney` | `playerId` (player), `account` (string), `amount` (number) | Yes | Sets the balance of an account (`money`, `bank`, `black_money` or a custom one). | | `/giveaccountmoney` | `playerId` (player), `account` (string), `amount` (number) | Yes | Adds money to the account. | | `/removeaccountmoney` | `playerId` (player), `account` (string), `amount` (number) | Yes | Removes money from the account. The balance never goes below `0`. | ```txt /giveaccountmoney 4 bank 25000 /setaccountmoney me money 0 ``` <Callout type="info"> Use positive amounts. `0` or negative values make the underlying xPlayer method throw an error in the server console. See [xPlayer money functions](/docs/es_extended/server/xplayer#money-and-accounts). </Callout> ## Items and weapons [#items-and-weapons] These commands only exist with the default ESX inventory. With ox_inventory they are not registered. | Command | Arguments | Console | What it does | | --- | --- | --- | --- | | `/giveitem` | `playerId` (player), `item` (item), `count` (number above 0) | Yes | Adds items. Does not check the weight limit. | | `/giveweapon` | `playerId` (player), `weapon` (weapon), `ammo` (number) | Yes | Gives a weapon. Fails if the player already has it. | | `/giveammo` | `playerId` (player), `weapon` (weapon), `ammo` (number above 0) | Yes | Adds ammo to a weapon the player has. | | `/giveweaponcomponent` | `playerId` (player), `weaponName` (weapon), `componentName` (string) | Yes | Attaches a component. Uses ESX component names such as `suppressor` or `clip_extended`. | | `/clearinventory` | `playerId` (player) | Yes | Removes all items. Fires `esx:playerInventoryCleared`. | | `/clearloadout` | `playerId` (player) | Yes | Removes all weapons. Fires `esx:playerLoadoutCleared`. | | `/refreshitems` | none | Yes | Reloads the `items` table and reports how many items were loaded. | ```txt /giveitem me bread 5 /giveweapon 4 weapon_pistol 120 /giveweaponcomponent 4 weapon_pistol flashlight ``` Weapon names are not case-sensitive in commands. Component names come from [`Config.Weapons`](/docs/es_extended/config/weapons). ## Vehicles [#vehicles] | Command | Aliases | Arguments | Console | What it does | | --- | --- | --- | --- | --- | | `/car` | | `car` (model, optional) | No | Deletes the vehicle you sit in, spawns the model (default `adder`) server-side and puts you in the driver seat. | | `/cardel` | `/dv` | `radius` (number, optional) | No | Deletes the vehicle you are in and every vehicle within `radius` (default `5`). | | `/fix` | `/repair` | `playerId` (player) | Yes | Repairs the vehicle the player is driving. The target must be in the driver seat. Use `/fix me` for yourself. | ## Server and chat [#server-and-chat] | Command | Aliases | Arguments | Console | What it does | | --- | --- | --- | --- | --- | | `/clearall` | `/clsall` | none | Yes | Clears the chat for every player. | | `/refreshjobs` | | none | Yes | Reloads all jobs and grades from the database. | ## Console-only commands [#console-only-commands] These are plain FiveM commands that only work in the server console. | Command | What it does | | --- | --- | | `clearvehicletypes` | Clears the cached vehicle types used by [`ESX.GetVehicleType`](/docs/es_extended/server/functions#getvehicletype). | | `resetmigrations` | Marks all ESX database migrations as not run, so they run again on the next start. | ## Add your own command [#add-your-own-command] Use [`ESX.RegisterCommand`](/docs/es_extended/server/functions#registercommand). It handles permissions, argument parsing and chat suggestions: ```lua title="server/commands.lua" ESX.RegisterCommand("heal", { "admin", "owner" }, function(xPlayer, args, showError) local target = args.playerId target.triggerEvent("myadmin:client:heal") showError(("Healed %s"):format(target.getName())) end, true, { help = "Heal a player", validate = true, arguments = { { name = "playerId", help = "Player ID or me", type = "player" }, }, }) ``` Use a table for the group to keep it in line with `Config.CommandPermissions`. If you register a name that already exists (for example `car`), your handler replaces the built-in one. ## Common issues [#common-issues] - **"Command Cannot be executed from console"**: the command needs a player (for example `/car`, `/tp`, `/noclip`). - **Nothing happens for players in a custom group**: add the group to the command in `Config.CommandPermissions` and restart es_extended. - **`/giveweapon` errors with an invalid name**: the weapon must exist in `Config.Weapons`. An unknown name fails the internal lookup. - **`/tpm` or `/noclip` do nothing for a custom staff group**: besides the command permission, the client asks the `esx:isUserAdmin` callback. That only passes for players with the ACE `command` or a group in `Config.AdminGroups`. - **Everyone is admin on a local test server**: with `sv_lan "true"`, `xPlayer.isAdmin()` returns `true` for every player. ## Related [#related] - [Admin permissions guide](/docs/guides/admin-permissions) - [Main config](/docs/es_extended/config) - [Server functions](/docs/es_extended/server/functions) - [xPlayer](/docs/es_extended/server/xplayer) # Adjustments Config (/docs/es_extended/config/adjustments) `shared/config/adjustments.lua` holds client-side game tweaks that es_extended applies when a player has loaded: PvP, wanted level, dispatch, NPC scenarios, population density, HUD components, number plates for AI traffic and Discord Rich Presence. Change them to tune the feel of the world without writing a separate resource. ## Overview [#overview] | Fact | Value | | --- | --- | | File | `es_extended/shared/config/adjustments.lua` | | Applied by | `client/modules/adjustments/*.lua` on `esx:playerLoaded` | | Side | Client (file is shared) | | Apply changes | Restart the server; players must reconnect | ## Toggles [#toggles] | Key | Type | Default | Effect when enabled | | --- | --- | --- | --- | | `DisableHealthRegeneration` | `boolean` | `false` | Sets the health recharge multiplier to `0`. | | `DisableVehicleRewards` | `boolean` | `false` | Players no longer get weapons from entering police or military vehicles. Runs every frame. | | `DisableNPCDrops` | `boolean` | `false` | NPCs no longer drop carbine rifle, pistol and pump shotgun pickups. | | `DisableDispatchServices` | `boolean` | `true` | Turns off all 15 AI dispatch services and the police scanner. | | `DisableScenarios` | `boolean` | `true` | Disables a list of world vehicle scenarios (parked and driving AI vehicles, police cars, boats, planes) and paparazzi. | | `DisableAimAssist` | `boolean` | `false` | Sets free-aim targeting mode, mainly affects controller players. | | `DisableVehicleSeatShuff` | `boolean` | `false` | Stops the automatic shuffle from passenger to driver seat. | | `DisableDisplayAmmo` | `boolean` | `false` | Hides the ammo counter. Runs every frame. | | `EnablePVP` | `boolean` | `true` | Allows players to damage each other (friendly fire). | | `EnableWantedLevel` | `boolean` | `false` | Keeps the normal GTA wanted level. When `false`, ESX clears it and sets the maximum to `0`. | <Callout type="info"> The key is spelled `DisableVehicleSeatShuff` (without the final "le"). Use it exactly like that. </Callout> ## RemoveHudComponents [#removehudcomponents] A list indexed by HUD component ID. `true` hides the component. | ID | Component | Default | | --- | --- | --- | | 1 | Wanted stars | `false` | | 2 | Weapon icon | `false` | | 3 | Cash | `false` | | 4 | MP cash | `false` | | 5 | MP message | `false` | | 6 | Vehicle name | `true` | | 7 | Area name | `true` | | 8 | Vehicle class | `true` | | 9 | Street name | `true` | | 10 | Help text | `false` | | 11 | Floating help text 1 | `false` | | 12 | Floating help text 2 | `false` | | 13 | Cash change | `false` | | 14 | Reticle | `false` | | 15 | Subtitle text | `false` | | 16 | Radio stations | `false` | | 17 | Saving game | `false` | | 18 | Game stream | `false` | | 19 | Weapon wheel | `false` | | 20 | Weapon wheel stats | `false` | | 21 | HUD components | `false` | | 22 | HUD weapons | `false` | Setting `[16] = true` also turns the vehicle radio off and disables radio control whenever the player enters a vehicle. ## Multipliers [#multipliers] Population density, applied every frame on each client. | Key | Default | Native | | --- | --- | --- | | `pedDensity` | `1.0` | `SetPedDensityMultiplierThisFrame` | | `scenarioPedDensityInterior` | `0.0` | `SetScenarioPedDensityMultiplierThisFrame` (interior) | | `scenarioPedDensityExterior` | `1.0` | `SetScenarioPedDensityMultiplierThisFrame` (exterior) | | `ambientVehicleRange` | `1.0` | `SetAmbientVehicleRangeMultiplierThisFrame` | | `parkedVehicleDensity` | `1.0` | `SetParkedVehicleDensityMultiplierThisFrame` | | `randomVehicleDensity` | `1.0` | `SetRandomVehicleDensityMultiplierThisFrame` | | `vehicleDensity` | `1.0` | `SetVehicleDensityMultiplierThisFrame` | ```lua title="shared/config/adjustments.lua" Config.Multipliers = { pedDensity = 0.6, scenarioPedDensityInterior = 0.0, scenarioPedDensityExterior = 0.6, ambientVehicleRange = 1.0, parkedVehicleDensity = 0.5, randomVehicleDensity = 0.5, vehicleDensity = 0.5, } ``` ### Changing density at runtime [#changing-density-at-runtime] The client also listens to the global state bag `esx:populationDensity`. A server resource can set it to override `pedDensity`, `vehicleDensity` and `randomVehicleDensity` for everyone, for example during an event. Values must be between `0` and `1`. When `pedDensity` is set, it also replaces both scenario ped values; when `vehicleDensity` is set, it also replaces `parkedVehicleDensity`. ```lua title="server/events.lua" GlobalState["esx:populationDensity"] = { pedDensity = 0.2, vehicleDensity = 0.1, randomVehicleDensity = 0.1 } -- back to Config.Multipliers GlobalState["esx:populationDensity"] = nil ``` ## CustomAIPlates [#customaiplates] | Type | Default | | --- | --- | | `string` | `"........"` | Number plate pattern for AI vehicles, passed to `SetDefaultVehicleNumberPlateTextPattern`. | Character | Result | | --- | --- | | `1` | random digit 0-9 | | `A` | random letter A-Z | | `.` | random letter or digit (50/50) | | `^1` | a literal `1` | | `^A` | a literal `A` | | anything else | that character | Patterns shorter than 8 characters are padded on the right. ```lua Config.CustomAIPlates = "LS^A 1111" -- e.g. "LSA 4821" ``` ## DiscordActivity [#discordactivity] Discord Rich Presence for every player. Disabled while `appId` is `0`. | Key | Type | Default | Description | | --- | --- | --- | --- | | `appId` | `number` | `0` | Discord application ID. | | `assetName` | `string` | `"LargeIcon"` | Name of the large image asset uploaded to the application. | | `assetText` | `string` | `"{server_name}"` | Hover text of the image. | | `buttons` | `table[]` | Join and Discord buttons | Up to two `{ label, url }` entries. | | `presence` | `string` | `"{player_name} [{player_id}] \| {server_players}/{server_maxplayers}"` | Status text. | | `refresh` | `number` | `60000` | Update interval in milliseconds. | Placeholders you can use in `presence`, `assetText` and button URLs: | Placeholder | Value | | --- | --- | | `{server_name}` | `sv_projectName` convar | | `{server_endpoint}` | server IP and port | | `{server_players}` | current player count | | `{server_maxplayers}` | `sv_maxClients` | | `{player_name}` | FiveM name | | `{player_rp_name}` | character name | | `{player_id}` | server ID | | `{player_street}` | current street name | ```lua Config.DiscordActivity = { appId = 123456789012345678, assetName = "logo", assetText = "{server_name}", buttons = { { label = "Join Server", url = "fivem://connect/{server_endpoint}" }, { label = "Discord", url = "https://discord.gg/yourserver" }, }, presence = "{player_rp_name} | {server_players}/{server_maxplayers}", refresh = 60 * 1000, } ``` ## Related [#related] - [Main config](/docs/es_extended/config) - [Weapons config](/docs/es_extended/config/weapons) - [Performance guide](/docs/guides/performance) - [Client events](/docs/es_extended/events/client) # Main Config (/docs/es_extended/config) The main es_extended config controls accounts, starting money and items, spawn points, command permissions, paychecks, weight and a few framework switches. It lives in `es_extended/shared/config/main.lua` and is loaded on both server and client. You can read it from other resources with `ESX.GetConfig()`. ## Overview [#overview] | Fact | Value | | --- | --- | | File | `es_extended/shared/config/main.lua` | | Side | Shared (server and client) | | Read from other resources | `ESX.GetConfig()` or `ESX.GetConfig("MaxWeight")` | | Other config files | [weapons](/docs/es_extended/config/weapons), [adjustments](/docs/es_extended/config/adjustments), [logs](/docs/es_extended/config/logs) | | Apply changes | Restart the server | <Callout type="warn"> Do not edit other core files. The config files are meant to be changed; logic files get overwritten on every update. Keep a copy of your config so you can merge it after updating. See [updating ESX](/docs/getting-started/updating). </Callout> ## Reading the config [#reading-the-config] ```lua local maxWeight = ESX.GetConfig("MaxWeight") local config = ESX.GetConfig() -- the whole Config table ``` `ESX.GetConfig(key)` returns `Config[key]`, or the whole table without a key. See [shared functions](/docs/es_extended/shared). ## Convars [#convars] Some values are read from convars, so you set them in `server.cfg` instead of the Lua file. Use `setr` so the client can read them too. | Convar | Default | Sets | Description | | --- | --- | --- | --- | | `esx:locale` | not set | `Config.Locale` | Language code, for example `en`, `de`, `fr`. Falls back to the `txAdmin-locale` convar, then `en`. | | `esx:localeFallback` | `"true"` | `Config.LocaleFallback` | Fill missing translation keys from English. Set to `"false"` while writing a new translation. | | `esx:identifier` | `"license"` | `Config.Identifier` | Identifier type used to store characters (`license`, `license2`, `steam`, `discord`, `fivem`, ...). | ```bash title="server.cfg" setr esx:locale "en" setr esx:identifier "license" ``` ## Framework [#framework] | Key | Type | Default | Description | | --- | --- | --- | --- | | `Locale` | `string` | from convars | Active language. Set it with the `esx:locale` convar. | | `LocaleFallback` | `boolean` | `true` | Use English for missing translation keys. | | `CustomInventory` | `false` or `string` | `false` | Leave `false`. Set to `"ox"` automatically when the `ox_inventory` resource exists. Other values only for inventories that ship their own ESX bridge. | | `EnableDebug` | `boolean` | `false` | Enables `ESX.Trace` output and extra error messages (for example in `getMeta`). Keep it off in production. | | `Multichar` | `boolean` | auto | `true` when the `esx_multicharacter` resource exists. Do not set it by hand unless a third-party multicharacter resource tells you to. | | `Identity` | `boolean` | `true` | Load `firstname`, `lastname`, `dateofbirth`, `sex` and `height` from `users`. Always active with multicharacter. | | `Identifier` | `string` | `"license"` | From the `esx:identifier` convar. Do not change it in the file. | | `EnableDefaultInventory` | `boolean` | auto | `true` when `CustomInventory` is `false`. Enables the default `F2` inventory. Do not change. | | `OxInventory` | `boolean` | auto | Backwards compatibility flag, equals `CustomInventory == "ox"`. Defined in `shared/main.lua`. | ## Accounts [#accounts] ### Accounts [#accounts-1] `Config.Accounts` defines the money accounts every character has. Each key is the account name. | Field | Type | Default | Description | | --- | --- | --- | --- | | `label` | `string` | | Display name, usually a translation. | | `round` | `boolean` | `true` | Round amounts to whole numbers. Set to `false` for accounts with decimals. | | `transferable` | `boolean?` | `true` | Set to `false` to stop players from giving or dropping this account through the default inventory. `bank` is not transferable by default. | ```lua title="shared/config/main.lua" Config.Accounts = { bank = { label = TranslateCap("account_bank"), round = true, transferable = false }, black_money = { label = TranslateCap("account_black_money"), round = true }, money = { label = TranslateCap("account_money"), round = true }, crypto = { label = "Crypto", round = false, transferable = false }, -- custom account } ``` New accounts appear for all characters on their next load, with the starting value from `StartingAccountMoney` or `0`. Learn more in the [money and accounts guide](/docs/guides/money-and-accounts). ### StartingAccountMoney [#startingaccountmoney] | Type | Default | | --- | --- | | `table<string, number>` | `{ bank = 50000 }` | Balances for new characters. Also used as the starting value when an existing character is missing an account. With ox_inventory, accounts stored as items (`money`, `black_money`) are given as items on creation. ```lua Config.StartingAccountMoney = { bank = 50000, money = 500 } ``` ### StartingInventoryItems [#startinginventoryitems] | Type | Default | | --- | --- | | `table<string, number>` or `false` | `false` | Items for new characters (default inventory), as `{ [itemName] = count }`. ```lua Config.StartingInventoryItems = { bread = 2, water = 2, phone = 1 } ``` ## Spawning [#spawning] | Key | Type | Default | Description | | --- | --- | --- | --- | | `DefaultSpawns` | `table[]` | one spawn at Legion Square | List of `{ x, y, z, heading }`. One is picked at random for new characters and when a saved position is invalid. | | `PlayerCoordinateLimit` | `number` | `10000.0` | Maximum absolute `x`, `y` and `z` that ESX accepts for player positions. Raise it if you use maps far outside the default world. | ```lua Config.DefaultSpawns = { { x = 222.2027, y = -864.0162, z = 30.2922, heading = 1.0 }, { x = -1037.5, y = -2737.6, z = 20.2, heading = 330.0 }, -- airport } ``` <Callout type="warn"> `DefaultSpawns` must contain at least one valid position. Without one, new players are dropped with the error code `invalid-spawn-config`. </Callout> ## Permissions [#permissions] ### CommandPermissions [#commandpermissions] | Type | Default | | --- | --- | | `table<string, string[]>` | see below | Maps every built-in command to the groups that may use it. Commands without an entry are limited to `admin`. Aliases use the entry of their main command. | Groups | Commands | | --- | --- | | `owner`, `admin` | `setcoords`, `setjob`, `car`, `cardel`, `fix`, `setaccountmoney`, `giveaccountmoney`, `removeaccountmoney`, `giveitem`, `giveweapon`, `giveammo`, `giveweaponcomponent`, `clearall`, `refreshjobs`, `refreshitems`, `clearinventory`, `clearloadout`, `setgroup`, `save`, `saveall`, `goto`, `bring`, `kill`, `freeze`, `unfreeze`, `setdim`, `players`, `noclip`, `tpm`, `coords` | | `user`, `admin`, `owner` | `clear`, `group`, `job`, `info`, `playtime` | See the [commands page](/docs/es_extended/commands) for what each command does. ### AdminGroups [#admingroups] | Type | Default | | --- | --- | | `table<string, boolean>` | `{ owner = true, admin = true }` | <Callout type="warn" title="Deprecated"> Marked deprecated in favor of `CommandPermissions` (the comment says it will be removed in 1.15, but it is still present and used in 1.16.4). ESX still uses it in `xPlayer.isAdmin()` and the `esx:isUserAdmin` callback, which `/tpm` and `/noclip` rely on. </Callout> ### ValidCharacterSets [#validcharactersets] | Type | Default | | --- | --- | | `table<string, boolean>` | all `false` | Extra alphabets that `ESX.IsValidLocaleString` accepts, for example in character names. Basic Latin and the alphabet of the active locale are always allowed. Keys: `el` (Greek), `sr` (Cyrillic), `he` (Hebrew), `ar` (Arabic), `zh-cn` (Chinese, Japanese, Korean). ```lua Config.ValidCharacterSets = { ["el"] = false, ["sr"] = true, ["he"] = false, ["ar"] = false, ["zh-cn"] = false } ``` ## Paychecks [#paychecks] | Key | Type | Default | Description | | --- | --- | --- | --- | | `EnablePaycheck` | `boolean` | `true` | Pay salaries at a fixed interval. | | `PaycheckInterval` | `number` | `7 * 60000` | Interval in milliseconds (7 minutes). | | `PaycheckBatchSize` | `number` | `100` | Players paid before the loop yields. | | `PaycheckBatchDelay` | `number` | `250` | Milliseconds to wait between batches. | | `PaycheckJitter` | `number` | `30 * 1000` | Up to this many extra milliseconds of random delay per run, so servers do not pay at the exact same moment. | | `OffDutyPaycheckMultiplier` | `number` | `0.5` | Share of the salary paid to off-duty players (`0.5` = 50 %). | | `EnableSocietyPayouts` | `boolean` | `false` | Pay salaries from the job's society account (needs esx_society and esx_addonaccount). If the society cannot pay, the player gets nothing. | | `LogPaycheck` | `boolean` | `false` | Log paychecks to the `Paycheck` webhook in the [logs config](/docs/es_extended/config/logs). | Unemployed players receive their `unemployed` grade salary as welfare. Salaries are always paid to the `bank` account. Use `xPlayer.togglePaycheck(false)` to skip single players. ## Jobs [#jobs] | Key | Type | Default | Description | | --- | --- | --- | --- | | `DefaultJobDuty` | `boolean` | `true` | Duty state after `setJob` when no duty is passed, and for characters without a saved duty state. `unemployed` is always off duty. | ## Player [#player] | Key | Type | Default | Description | | --- | --- | --- | --- | | `MaxWeight` | `number` | `24` | Inventory weight limit without a backpack. With ox_inventory it sets the `inventory:weight` convar (`MaxWeight * 1000` grams). | | `SaveDeathStatus` | `boolean` | `true` | Restore a saved health of `0` on reconnect, so players who log out dead are still dead. | | `DistanceGive` | `number` | `4.0` | Maximum distance for giving items, money and weapons to another player. | ## Logging [#logging] | Key | Type | Default | Description | | --- | --- | --- | --- | | `AdminLogging` | `boolean` | `false` | Send admin command usage to the `UserActions` webhook. | ## Related [#related] - [Weapons config](/docs/es_extended/config/weapons) - [Adjustments config](/docs/es_extended/config/adjustments) - [Logs config](/docs/es_extended/config/logs) - [server.cfg setup](/docs/getting-started/server-cfg) - [Admin permissions guide](/docs/guides/admin-permissions) # Logs Config (/docs/es_extended/config/logs) `shared/config/logs.lua` configures the Discord webhooks that es_extended uses for logging. It defines named webhooks and named embed colors. [`ESX.DiscordLog`](/docs/es_extended/server/functions#discordlog) and [`ESX.DiscordLogFields`](/docs/es_extended/server/functions#discordlogfields) look up both by name, so your own resources can log through the same config. ## Overview [#overview] | Fact | Value | | --- | --- | | File | `es_extended/shared/config/logs.lua` | | Loaded as | Server script only (despite the `shared` folder) | | Table | `Config.DiscordLogs` with `Webhooks` and `Colors` | | Used by | `ESX.DiscordLog`, `ESX.DiscordLogFields`, admin commands, paychecks | <Callout type="warn"> Discord webhooks are rate limited and requests can fail silently. ESX sends them without retries. Use them for low-volume staff notices; for a busy server, consider a real logging pipeline (for example Grafana Loki or Datadog). </Callout> ## Webhooks [#webhooks] `Config.DiscordLogs.Webhooks` maps a log name to a webhook URL. An empty string disables that channel. Unknown names fall back to `default`. | Key | Used by | | --- | --- | | `default` | Fallback for every name without its own webhook. | | `test` | Free for testing. | | `Chat` | Free for chat logging resources. | | `UserActions` | Admin commands when `Config.AdminLogging = true` (`/car`, `/setjob`, `/giveitem`, `/setgroup`, ...). | | `Resources` | Free for resource start and stop logging. | | `Paycheck` | Paychecks when `Config.LogPaycheck = true`. | ```lua title="shared/config/logs.lua" Config.DiscordLogs = { Webhooks = { default = "https://discord.com/api/webhooks/...", test = "", Chat = "", UserActions = "https://discord.com/api/webhooks/...", Resources = "", Paycheck = "", Police = "https://discord.com/api/webhooks/...", -- custom channel }, Colors = { -- ... }, } ``` ## Colors [#colors] `Config.DiscordLogs.Colors` maps a color name to a decimal color value. Unknown names fall back to `default`. | Key | Value | | --- | --- | | `default` | `14423100` | | `blue` | `255` | | `red` | `16711680` | | `green` | `65280` | | `white` | `16777215` | | `black` | `0` | | `orange` | `16744192` | | `yellow` | `16776960` | | `pink` | `16761035` | | `lightgreen` | `65309` | To add a color, convert the hex value to decimal (for example `#5865F2` is `5793266`) and add a key. ## Logging from your resource [#logging-from-your-resource] ```lua title="server/main.lua" ESX.DiscordLogFields("Police", "Player cuffed", "blue", { { name = "Officer", value = xPlayer.getName(), inline = true }, { name = "Suspect", value = xTarget.getName(), inline = true }, }) ``` ## Related [#related] - [Main config](/docs/es_extended/config) - [Server functions](/docs/es_extended/server/functions) - [Commands](/docs/es_extended/commands) # Weapons Config (/docs/es_extended/config/weapons) `shared/config/weapons.lua` is the weapon catalog of es_extended. It defines which weapons exist in the default loadout system, their labels, ammo type, attachable components and tints. xPlayer weapon methods, the `/giveweapon` commands and the `ESX.GetWeapon*` helpers all read from it. ## Overview [#overview] | Fact | Value | | --- | --- | | File | `es_extended/shared/config/weapons.lua` | | Side | Shared | | Tables | `Config.DefaultWeaponTints`, `Config.MK2WeaponTints`, `Config.Weapons` | | Entries | 112 weapons and gadgets in 1.16.4 | | Helpers | `ESX.GetWeapon`, `ESX.GetWeaponFromHash`, `ESX.GetWeaponList`, `ESX.GetWeaponLabel`, `ESX.GetWeaponComponent` ([shared functions](/docs/es_extended/shared)) | ## Config.Weapons [#configweapons] An array of weapon definitions, grouped by comments into Melee, Handguns, Shotguns, SMG & LMG, Rifles, Sniper, Heavy / Launchers, Thrown and Tools. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | Yes | Upper-case game name, for example `WEAPON_PISTOL`. Lookups convert input to upper case. | | `label` | `string` | Yes | Display name, usually `TranslateCap("weapon_pistol")`. | | `components` | `table[]` | Yes | Attachments, use `{}` for none. See below. | | `ammo` | `table?` | No | `{ label, hash }`. `hash` is the ammo type hash, for example `` `AMMO_PISTOL` ``. Only for weapons that use ammo. | | `tints` | `table?` | No | Tint labels by index. Usually `Config.DefaultWeaponTints` or `Config.MK2WeaponTints`. `xPlayer.setWeaponTint` only accepts indexes that exist here. | | `throwable` | `boolean?` | No | `true` for grenades and similar. The weapon is removed from the loadout when its ammo reaches `0`. | Each component is `{ name, label, hash }`: | Field | Type | Description | | --- | --- | --- | | `name` | `string` | Short ESX name used in code and commands, for example `clip_extended`, `suppressor`, `flashlight`, `scope`, `grip`. | | `label` | `string` | Display name. | | `hash` | `number` | Game component hash, for example `` `COMPONENT_AT_PI_SUPP` ``. | ```lua title="shared/config/weapons.lua" { name = "WEAPON_COMBATPISTOL", label = TranslateCap("weapon_combatpistol"), ammo = { label = TranslateCap("ammo_rounds"), hash = `AMMO_PISTOL` }, tints = Config.DefaultWeaponTints, components = { { name = "clip_default", label = TranslateCap("component_clip_default"), hash = `COMPONENT_COMBATPISTOL_CLIP_01` }, { name = "clip_extended", label = TranslateCap("component_clip_extended"), hash = `COMPONENT_COMBATPISTOL_CLIP_02` }, { name = "flashlight", label = TranslateCap("component_flashlight"), hash = `COMPONENT_AT_PI_FLSH` }, { name = "suppressor", label = TranslateCap("component_suppressor"), hash = `COMPONENT_AT_PI_SUPP` }, }, }, ``` A melee weapon only needs the basics: ```lua { name = "WEAPON_BAT", label = TranslateCap("weapon_bat"), components = {} }, ``` ## Tints [#tints] | Table | Indexes | Used by | | --- | --- | --- | | `Config.DefaultWeaponTints` | `0` to `7` (Default, Green, Gold, Pink, Army, LSPD, Orange, Platinum) | Regular weapons | | `Config.MK2WeaponTints` | `0` to `32` (classic, contrast, bold and metallic finishes) | MK II weapons | ```lua xPlayer.setWeaponTint("WEAPON_PISTOL", 2) -- gold, from DefaultWeaponTints ``` ## Adding a custom weapon [#adding-a-custom-weapon] <Steps> <Step> ### Add the entry [#add-the-entry] Append your weapon to `Config.Weapons`. Use the exact spawn name from the weapon resource, in upper case. ```lua { name = "WEAPON_M4CUSTOM", label = "M4 Custom", ammo = { label = TranslateCap("ammo_rounds"), hash = `AMMO_RIFLE` }, tints = Config.DefaultWeaponTints, components = { { name = "clip_default", label = TranslateCap("component_clip_default"), hash = `COMPONENT_M4CUSTOM_CLIP_01` }, }, }, ``` </Step> <Step> ### Add a translation (optional) [#add-a-translation-optional] If you use `TranslateCap("weapon_m4custom")`, add the key to the locale files in `es_extended/locales`. A plain string works too. </Step> <Step> ### Restart and test [#restart-and-test] Restart the server and run `/giveweapon me weapon_m4custom 100`. </Step> </Steps> ## Common issues [#common-issues] - **`Invalid weapon name!` assertion**: the weapon is not in `Config.Weapons`. `ESX.GetWeapon`, `ESX.GetWeaponLabel` and `ESX.GetWeaponComponent` all assert on unknown names. - **Component cannot be added**: the component `name` must exist in that weapon's `components` list, and the player must own the weapon. - **Weapons from the old config disappeared after an update**: loadout entries whose name is no longer in `Config.Weapons` are dropped when the character loads. ## Related [#related] - [xPlayer weapon methods](/docs/es_extended/server/xplayer#weapons) - [Shared functions](/docs/es_extended/shared) - [Commands](/docs/es_extended/commands) - [Main config](/docs/es_extended/config) # Client Events (/docs/es_extended/events/client) These are the `esx:` events on the client. Some are sent by the server to update the local player (load, job, money, inventory), others are fired locally by `es_extended` when something happens in the game (death, spawn, entering a vehicle). The tables show the arguments, who fires the event and whether you may trigger it yourself. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` | | Side | Client | | Server to client | Listen with `RegisterNetEvent(name, handler)` | | Local client events | Listen with `AddEventHandler(name, handler)` | | Marker | Meaning | |---|---| | **Secure** | Registered with [ESX.SecureNetEvent](/docs/es_extended/client/functions). ESX ignores it unless the server sent it. | | **Not secured** | Plain `RegisterNetEvent` in ESX. Any client script (or injected cheat) can fire it locally with `TriggerEvent`. | | **Local** | Fired by ESX on the client with `TriggerEvent`. Only listen. | <Callout type="warn" title="Do not trigger ESX events yourself"> Triggering ESX state events (for example `esx:setJob` or `esx:setAccountMoney`) from your own client code only changes the local cache and confuses other scripts. Change data on the server with the [xPlayer](/docs/es_extended/server/xplayer) methods; ESX then sends the right events. </Callout> ## Player lifecycle [#player-lifecycle] ### esx:playerLoaded [#esxplayerloaded] **Sent by the server**, not secured. Fired once the character is loaded. | Name | Type | Description | |---|---|---| | `xPlayer` | `table` | PlayerData table (see [Client PlayerData](/docs/es_extended/shared/playerdata)). | | `isNew` | `boolean` | `true` for a newly created character. | | `skin` | `table` | Saved skin. | ```lua title="client/main.lua" RegisterNetEvent('esx:playerLoaded', function(xPlayer, isNew, skin) ESX.PlayerData = xPlayer if xPlayer.job.name == 'police' then CreatePoliceBlips() end end) ``` ### esx:onPlayerLogout [#esxonplayerlogout] **Secure**, no arguments. Sent after the server logged the player out of the character (multicharacter). `ESX.PlayerLoaded` becomes `false`. ```lua RegisterNetEvent('esx:onPlayerLogout', function() RemovePoliceBlips() end) ``` ### esx:onPlayerSpawn [#esxonplayerspawn] **Local**, no arguments. Fired by ESX after the first spawn (non-multicharacter), and by other resources such as esx_multicharacter and esx_death after a character spawn or revive. ESX resets `PlayerData.dead`, restores saved health and armor on the first spawn and starts death detection. ```lua AddEventHandler('esx:onPlayerSpawn', function() SetPedArmour(PlayerPedId(), 0) end) ``` ### esx:onPlayerDeath [#esxonplayerdeath] **Local**, also sent to the server. Fired when ESX detects the death of the local ped. | Field of `data` | Type | Description | |---|---|---| | `victimCoords` | `table` | `{ x, y, z }`. | | `killedByPlayer` | `boolean` | Killed by another player. | | `deathCause` | `number` | Weapon hash. | | `killerCoords` | `table?` | Only for player kills. | | `distance` | `number?` | Only for player kills. | | `killerServerId` | `number?` | Only for player kills. | | `killerClientId` | `number?` | Client player index of the killer. | ```lua AddEventHandler('esx:onPlayerDeath', function(data) if data.killedByPlayer then ESX.ShowNotification(('You were killed by player %s'):format(data.killerServerId), 'error') end end) ``` ### esx:loadingScreenOff [#esxloadingscreenoff] **Local**, no arguments. Fired after the first spawn when ESX shuts down the loading screen (non-multicharacter). ### esx:restoreLoadout [#esxrestoreloadout] **Local**, internal. Removes all weapons and gives back the weapons, tints, components and ammo from `PlayerData.loadout`. ESX fires it after spawn and when skinchanger loads a new model. Built-in inventory only. ### esx:setPlayerData [#esxsetplayerdata] **Local.** Fired by `ESX.SetPlayerData` with `(key, val, last)` for every PlayerData change. See [Client PlayerData](/docs/es_extended/shared/playerdata). ## Player data updates (server to client) [#player-data-updates-server-to-client] All of these are **secure** and update `ESX.PlayerData` through `ESX.SetPlayerData`. | Event | Arguments | Updates | |---|---|---| | `esx:setJob` | `job, lastJob` | `PlayerData.job` | | `esx:jobDataRefreshed` | `job, lastJob` | `PlayerData.job` after a job definition changed | | `esx:setGroup` | `group, lastGroup` | `PlayerData.group` | | `esx:setAccountMoney` | `account` (`name`, `money`, `label`, `round`, `index`) | the matching entry in `PlayerData.accounts` | | `esx:updatePlayerData` | `key, val` | any key, used for `metadata` and `variables` | | `esx:setMaxWeight` | `maxWeight` | `PlayerData.maxWeight` | | `esx:setInventory` | `inventory` | `PlayerData.inventory` | ```lua title="client/hud.lua" RegisterNetEvent('esx:setAccountMoney', function(account) if account.name == 'money' then SendNUIMessage({ action = 'cash', value = account.money }) end end) RegisterNetEvent('esx:setJob', function(job, lastJob) print(('Job changed from %s to %s'):format(lastJob.name, job.name)) end) ``` ## Inventory and weapons (built-in inventory) [#inventory-and-weapons-built-in-inventory] **Secure.** Not registered when `Config.CustomInventory` is set. | Event | Arguments | Description | |---|---|---| | `esx:addInventoryItem` | `item, count, showNotification, itemData` | `count` is the **new total**. `itemData` has `label`, `weight`, `usable`, `rare`, `canRemove`. | | `esx:removeInventoryItem` | `item, count, showNotification` | `count` is the new total, `0` removes the entry. | | `esx:addLoadoutItem` | `weaponName, weaponLabel, ammo` | A weapon was added. | | `esx:removeLoadoutItem` | `weaponName, weaponLabel` | A weapon was removed. | | `esx:updateWeaponAmmo` | `weaponName, ammo` | Ammo changed on the server. | | `esx:setWeaponTint` | `weaponName, tintIndex` | Applies a tint. | | `esx:removeWeaponComponent` | `weaponName, componentName` | Removes a component. | | `esx:showInventoryItemNotification` | `add, label, count` | Shows the item popup. | | `esx:createPickup` | `pickupId, label, coords, itemType, name, components, tintIndex` | Spawns a ground pickup. | | `esx:createMissingPickups` | `pickups` | Spawns pickups near the player. | | `esx:removePickup` | `pickupId` | Deletes a pickup. | ```lua RegisterNetEvent('esx:addInventoryItem', function(item, count) if item == 'radio' and count == 1 then ESX.ShowNotification('You can now use /radio') end end) ``` ## Notifications and commands (server to client) [#notifications-and-commands-server-to-client] | Event | Arguments | Security | |---|---|---| | `esx:showNotification` | `message, type, length, title, position` | Not secured | | `esx:showAdvancedNotification` | `sender, subject, msg, textureDict, iconType, flash, saveToBrief, hudColorIndex` | Not secured | | `esx:showHelpNotification` | `msg, thisFrame, beep, duration` | Not secured | | `esx:registerSuggestions` | `registeredCommands` | Secure | | `esx:executeCommand` | `command` | Secure. Sent by `xPlayer.executeCommand`. | The notification events are harmless if faked locally; they only display text. From the server prefer `xPlayer.showNotification(...)`. ## Admin events (server to client) [#admin-events-server-to-client] These are sent by the admin commands. They are **not secured**, so a client-side script could fire them locally. | Event | Arguments | Sent by | |---|---|---| | `esx:killPlayer` | none | `/kill` | | `esx:freezePlayer` | `'freeze' \| 'unfreeze'` | `/freeze`, `/unfreeze` | | `esx:repairPedVehicle` | none | `/repair` | | `esx:tpm` | none | `/tpm`. Asks the server (`esx:isUserAdmin`) before teleporting. | | `esx:noclip` | none | `/noclip`. Asks the server (`esx:isUserAdmin`) before toggling. | | `esx:requestModel` | `model` | Loads a model. Not sent by es_extended itself. | ## Game state (local) [#game-state-local] ESX checks these values in a loop every 500 ms (vehicle, pause menu) or through the esx_lib cache (ped, weapon) and fires local events. | Event | Arguments | When | |---|---|---| | `esx:enteringVehicle` | `vehicle, plate, seat, netId` | The player starts entering a vehicle. Also sent to the server without `vehicle`. | | `esx:enteringVehicleAborted` | none | The enter attempt was cancelled. Also sent to the server. | | `esx:enteredVehicle` | `vehicle, plate, seat, displayName, netId` | The player is in the vehicle. Also sent to the server without `vehicle`. | | `esx:exitedVehicle` | `vehicle, plate, seat, displayName, netId` | The player left the vehicle or died in it. Also sent to the server without `vehicle`. | | `esx:vehicleSeatChanged` | `seat` | Seat changed inside the same vehicle. | | `esx:pauseMenuActive` | `isActive` | Pause menu opened or closed. | | `esx:playerPedChanged` | `ped` | The player ped handle changed (model change, respawn). | | `esx:weaponChanged` | `weapon` | Selected weapon hash changed. | `plate` is the raw plate text with padding; trim it with `ESX.Math.Trim`. `netId` is the local handle when the vehicle is not networked. ```lua title="client/main.lua" AddEventHandler('esx:enteredVehicle', function(vehicle, plate, seat) if seat == -1 then ESX.ShowNotification(('Driving %s'):format(ESX.Math.Trim(plate))) end end) AddEventHandler('esx:pauseMenuActive', function(isActive) SendNUIMessage({ action = 'hud', visible = not isActive }) end) ``` ## Deprecated and removed [#deprecated-and-removed] | Event | Status | |---|---| | `esx:getSharedObject` | Deprecated. Use `exports['es_extended']:getSharedObject()` or the imports file. | | `esx:addWeapon`, `esx:addWeaponComponent`, `esx:setWeaponAmmo`, `esx:removeWeapon` | Removed. Triggering them raises an error. Use `xPlayer.addWeapon`, `xPlayer.addWeaponComponent`, `xPlayer.addWeaponAmmo` and `xPlayer.removeWeapon` on the server. | | `esx:playerJumping` | Removed. Not fired in 1.16. | ## Related [#related] - [Server events](/docs/es_extended/events/server) - [Client PlayerData](/docs/es_extended/shared/playerdata) - [Client functions](/docs/es_extended/client/functions) - [xPlayer](/docs/es_extended/server/xplayer) # Server Events (/docs/es_extended/events/server) These are the `esx:` events you meet on the server. Most of them are **local server events** that `es_extended` fires so other resources can react, for example when a player loads, changes job or gets money. A second group are **net events sent by clients**, which you must treat as untrusted input. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` | | Side | Server | | Listen with | `AddEventHandler` (local events), `RegisterNetEvent` (events sent by clients) | | Marker | Meaning | |---|---| | **Listen only** | Fired by `es_extended`. Do not trigger it yourself, other scripts would act on fake data. | | **Internal** | Used by ESX for its own flow. Do not trigger or override. | | **Client-sent (untrusted)** | Arrives from a client through `TriggerServerEvent`. Anyone can send any arguments. | <Callout type="error" title="Security"> Never give money, items or permissions inside a handler for an event that a client can send. The client-sent events below carry whatever the client wants. Always look the player up with `ESX.GetPlayerFromId(source)` and verify on the server. </Callout> ## Player lifecycle [#player-lifecycle] ### esx:playerLoaded [#esxplayerloaded] **Listen only.** Fired after a character is loaded and the xPlayer object exists. Also fires for every character switch with multicharacter. | Name | Type | Description | |---|---|---| | `playerId` | `number` | Server id. | | `xPlayer` | `table` | The [xPlayer](/docs/es_extended/server/xplayer) object. | | `isNew` | `boolean` | `true` if the character was just created. | ```lua title="server/main.lua" AddEventHandler('esx:playerLoaded', function(playerId, xPlayer, isNew) if isNew then xPlayer.addInventoryItem('phone', 1) xPlayer.showNotification('Welcome to the city!') end end) ``` ### esx:playerDropped [#esxplayerdropped] **Listen only.** Fired when an xPlayer is removed: disconnect, logout or a stale session cleanup. Fired before the final save, so the xPlayer still exists. | Name | Type | Description | |---|---|---| | `playerId` | `number` | Server id. | | `reason` | `string` | Drop reason. `esx_player_logout` for logouts. | ```lua AddEventHandler('esx:playerDropped', function(playerId, reason) local xPlayer = ESX.GetPlayerFromId(playerId) if xPlayer then print(('%s left (%s)'):format(xPlayer.getName(), reason)) end end) ``` ### esx:playerSaved [#esxplayersaved] **Listen only.** Fired after a player's row was written to the database (autosave every 10 minutes, on drop and manual saves). | Name | Type | Description | |---|---|---| | `playerId` | `number` | Server id. | | `xPlayer` | `table` | The saved xPlayer. | ### esx:playerLogout [#esxplayerlogout] **Server-only trigger.** Trigger it from server code to log a player out of the current character. ESX saves and removes the xPlayer, then sends `esx:onPlayerLogout` to the client. Registered with `AddEventHandler`, so clients cannot trigger it. | Name | Type | Description | |---|---|---| | `playerId` | `number` | Server id. | | `cb` | `function?` | Called when the logout is done. | ```lua TriggerEvent('esx:playerLogout', playerId, function() print(('Player %s is back at character selection'):format(playerId)) end) ``` ### esx:onPlayerJoined [#esxonplayerjoined] **Internal.** Starts loading the player. - Without multicharacter: client-sent net event with no arguments. ESX ignores it if the player is already loaded or loading. - With multicharacter: local server event `(playerId, char, data)` triggered by the multicharacter resource. ### esx:onPlayerDropped [#esxonplayerdropped] **Internal.** Local server event `(playerId, reason, cb)` that runs the ESX drop and save routine. Use `esx:playerLogout` instead. ### esx:onPlayerSpawn [#esxonplayerspawn] **Client-sent (untrusted).** Sent by the client after the first spawn. ESX only sets `xPlayer.spawned = true`. ## Jobs and groups [#jobs-and-groups] ### esx:setJob [#esxsetjob] **Listen only.** Fired after `xPlayer.setJob`. | Name | Type | Description | |---|---|---| | `playerId` | `number` | Server id. | | `job` | `table` | New job (`name`, `label`, `grade`, `grade_name`, `grade_label`, `grade_salary`, ...). | | `lastJob` | `table` | Previous job. | ```lua AddEventHandler('esx:setJob', function(playerId, job, lastJob) if job.name == 'police' and lastJob.name ~= 'police' then exports.my_radio:GiveChannelAccess(playerId, 1) end end) ``` ### esx:jobDataRefreshed [#esxjobdatarefreshed] **Listen only.** Fired when a player's job data was rebuilt after the job definition changed (for example after `ESX.RefreshJob`). | Name | Type | Description | |---|---|---| | `playerId` | `number` | Server id. | | `job` | `table` | Updated job. | | `lastJob` | `table` | Job before the refresh. | ### esx:setGroup [#esxsetgroup] **Listen only.** Fired after `xPlayer.setGroup`. | Name | Type | Description | |---|---|---| | `playerId` | `number` | Server id. | | `group` | `string` | New group. | | `lastGroup` | `string` | Previous group. | ### esx:jobsRefreshed [#esxjobsrefreshed] **Listen only.** No arguments. Fired after all jobs were loaded from the database (`ESX.RefreshJobs`). ### esx:jobRefreshed [#esxjobrefreshed] **Listen only.** Fired after one job was reloaded with `ESX.RefreshJob`. | Name | Type | Description | |---|---|---| | `jobName` | `string` | Job name. | | `job` | `table` | Job data with `grades`. | ### esx:jobRemoved [#esxjobremoved] **Listen only.** Fired when a refreshed job no longer exists or has no grades. Players with that job are set to `unemployed`. | Name | Type | Description | |---|---|---| | `jobName` | `string` | Removed job. | | `reason` | `string` | `missing job` or `no job grades found`. | ### esx:jobCreated [#esxjobcreated] **Listen only.** Fired after `ESX.CreateJob` created a job. | Name | Type | Description | |---|---|---| | `name` | `string` | Job name. | | `job` | `table` | The new entry in `ESX.Jobs`. | ## Money and inventory [#money-and-inventory] ### esx:setAccountMoney / esx:addAccountMoney / esx:removeAccountMoney [#esxsetaccountmoney--esxaddaccountmoney--esxremoveaccountmoney] **Listen only.** Fired by `xPlayer.setAccountMoney`, `addAccountMoney` and `removeAccountMoney` (also with ox_inventory). | Name | Type | Description | |---|---|---| | `playerId` | `number` | Server id. | | `accountName` | `string` | `money`, `bank`, `black_money`, ... | | `money` | `number` | For `set`: the new balance. For `add` / `remove`: the amount changed. | | `reason` | `string?` | Reason passed to the xPlayer method. | ```lua AddEventHandler('esx:addAccountMoney', function(playerId, account, amount, reason) if account == 'black_money' and amount >= 100000 then print(('[audit] %s received %s dirty money (%s)'):format(playerId, amount, reason or 'no reason')) end end) ``` ### esx:onAddInventoryItem / esx:onRemoveInventoryItem [#esxonaddinventoryitem--esxonremoveinventoryitem] **Listen only.** Built-in inventory only. Fired after `xPlayer.addInventoryItem` / `removeInventoryItem`. | Name | Type | Description | |---|---|---| | `playerId` | `number` | Server id. | | `itemName` | `string` | Item name. | | `count` | `number` | The **new total** count of the item, not the amount added. | ### esx:playerInventoryCleared / esx:playerLoadoutCleared [#esxplayerinventorycleared--esxplayerloadoutcleared] **Listen only.** Fired by the `/clearinventory` and `/clearloadout` admin commands. | Name | Type | Description | |---|---|---| | `xPlayer` | `table` | The target xPlayer (not an id). | ## Vehicles [#vehicles] ### esx:createdExtendedVehicle / esx:deletedExtendedVehicle [#esxcreatedextendedvehicle--esxdeletedextendedvehicle] **Listen only.** Fired when an ExtendedVehicle object is created or deleted. | Name | Type | Description | |---|---|---| | `vehicle` | `table` | The ExtendedVehicle object (has `plate`). | ### esx:changedExtendedVehiclePlate [#esxchangedextendedvehicleplate] **Listen only.** Fired after an ExtendedVehicle plate change. | Name | Type | Description | |---|---|---| | `newPlate` | `string` | New plate. | | `oldPlate` | `string` | Previous plate. | ## Events sent by clients [#events-sent-by-clients] ### Handled by es_extended [#handled-by-es_extended] These net events are handled by the built-in inventory (not registered when `Config.CustomInventory` is set). ESX validates and rate-limits them. Do not register your own handlers that trust their arguments. | Event | Arguments | What ESX does | |---|---|---| | `esx:giveInventoryItem` | `target, itemType, itemName, itemCount` | Gives an item, money, weapon or ammo to a nearby player. | | `esx:removeInventoryItem` | `itemType, itemName, itemCount` | Drops something on the ground as a pickup. | | `esx:useItem` | `itemName` | Uses an item via `ESX.UseItem` if the player has it. | | `esx:onPickup` | `pickupId` | Picks up a pickup within 5 units. | | `esx:requestPickups` | none | Sends nearby pickups to the client. | | `esx:updateWeaponAmmo` | `weaponName, ammoCount` | Syncs ammo. Only lowers the stored ammo, never raises it. | ### Not handled by es_extended [#not-handled-by-es_extended] The client sends these so other resources can use them. ESX itself registers no server handler. **All arguments are client-controlled.** #### esx:onPlayerDeath [#esxonplayerdeath] | Field of `data` | Type | Description | |---|---|---| | `victimCoords` | `table` | `{ x, y, z }` rounded to 1 decimal. | | `killedByPlayer` | `boolean` | Whether another player caused the death. | | `deathCause` | `number` | Weapon hash from `GetPedCauseOfDeath`. | | `killerCoords` | `table?` | Killer position (player kills only). | | `distance` | `number?` | Distance to the killer (player kills only). | | `killerServerId` | `number?` | Killer server id (player kills only). | | `killerClientId` | `number?` | Killer client player index (player kills only). | ```lua title="server/main.lua" RegisterNetEvent('esx:onPlayerDeath', function(data) local victim = source if type(data) ~= 'table' or not data.killedByPlayer then return end local killer = tonumber(data.killerServerId) if not killer or not ESX.GetPlayerFromId(killer) then return end local distance = #(GetEntityCoords(GetPlayerPed(victim)) - GetEntityCoords(GetPlayerPed(killer))) if distance > 300.0 then return end -- claim does not match server state print(('Player %s was killed by %s'):format(victim, killer)) end) ``` #### Vehicle events [#vehicle-events] | Event | Arguments | |---|---| | `esx:enteringVehicle` | `plate, seat, netId` | | `esx:enteringVehicleAborted` | none | | `esx:enteredVehicle` | `plate, seat, displayName, netId` | | `esx:exitedVehicle` | `plate, seat, displayName, netId` | ```lua RegisterNetEvent('esx:enteredVehicle', function(plate, seat, displayName, netId) local vehicle = NetworkGetEntityFromNetworkId(netId) if vehicle == 0 or GetVehiclePedIsIn(GetPlayerPed(source), false) ~= vehicle then return end print(('%s entered %s (%s) in seat %s'):format(source, displayName, plate, seat)) end) ``` `netId` is the local entity handle when the vehicle is not networked, so always check the result of `NetworkGetEntityFromNetworkId`. ## Deprecated and removed [#deprecated-and-removed] | Event | Status | |---|---| | `esx:getSharedObject` | Deprecated. Still answers for old scripts. Use `exports['es_extended']:getSharedObject()`. | | `esx:playerJumping` | Removed. Not fired in 1.16. | ## Related [#related] - [Client events](/docs/es_extended/events/client) - [xPlayer](/docs/es_extended/server/xplayer) - [Server functions](/docs/es_extended/server/functions) - [Callbacks](/docs/es_extended/client/modules/callback) # es_extended (/docs/es_extended) `es_extended` is the core resource of ESX Legacy. It loads characters from the database, creates the server-side [xPlayer](/docs/es_extended/server/xplayer) object, manages money accounts, jobs, items, weapons and permissions, and exposes all of this through the shared `ESX` object that every other ESX resource uses. ## Overview [#overview] | Fact | Value | | --- | --- | | Resource | `es_extended` | | Version | ESX Legacy 1.16.4 | | Side | Server, client and shared | | Dependencies | `oxmysql`, `esx_lib` | | Export | `getSharedObject` (and `RunStaticPlayerMethod`, used by `ESX.Player`) | | Database tables | `users`, `jobs`, `job_grades`, `items` (plus `owned_vehicles` for extended vehicles) | | License | GPL-3.0 | ## Getting the ESX object [#getting-the-esx-object] There are two supported ways. Pick one per resource. ### Option 1: imports.lua (recommended) [#option-1-importslua-recommended] ```lua title="fxmanifest.lua" fx_version "cerulean" game "gta5" lua54 "yes" shared_script "@es_extended/imports.lua" client_script "client/main.lua" server_script "server/main.lua" ``` `imports.lua` sets a global `ESX` and adds extras: - On the client, `ESX.PlayerData` and `ESX.PlayerLoaded` stay in sync automatically, and an optional global `OnPlayerData(key, value, last)` function is called on changes. - On the server, `ESX.Player(source)` and `ESX.ExtendedPlayers()` return [static players](/docs/es_extended/server/xplayer#static-players-esxplayer). - `ESX.require`, `ESX.load` and `ESX.loadJson` load modules and JSON files when ox_lib is not installed. See [require](/docs/es_extended/client/imports/require). ### Option 2: the export [#option-2-the-export] ```lua title="server/main.lua" ESX = exports["es_extended"]:getSharedObject() ``` The export returns the `ESX` table at the moment you call it. Functions work normally, but data tables such as `ESX.PlayerData` on the client are not refreshed in your resource. Use getters like `ESX.GetPlayerData()` and `ESX.GetJobs()` instead. <Callout type="warn" title="Deprecated pattern"> `TriggerEvent("esx:getSharedObject", function(obj) ESX = obj end)` still works in 1.16.4 for backwards compatibility. Do not use it in new code. See [convert old ESX scripts](/docs/guides/convert-old-esx-scripts). </Callout> ## Resource structure [#resource-structure] <Files> <Folder name="es_extended" defaultOpen> <File name="fxmanifest.lua" /> <File name="imports.lua" /> <File name="locale.lua" /> <Folder name="shared" defaultOpen> <Folder name="config" defaultOpen> <File name="main.lua" /> <File name="weapons.lua" /> <File name="adjustments.lua" /> <File name="logs.lua" /> </Folder> <File name="main.lua" /> <File name="compat.lua" /> <Folder name="modules" /> </Folder> <Folder name="server"> <Folder name="classes" /> <Folder name="modules" /> <Folder name="bridge" /> <Folder name="migration" /> </Folder> <Folder name="client"> <Folder name="modules" /> <Folder name="imports" /> </Folder> <Folder name="locales" /> <Folder name="html" /> </Folder> </Files> | Path | Purpose | | --- | --- | | `shared/config/` | The only files you should edit. See [config](/docs/es_extended/config). `logs.lua` is loaded on the server only. | | `shared/main.lua` | Creates `ESX` and registers the `getSharedObject` export. | | `shared/modules/` | Shared helpers: config access, weapons, validation, random strings, debug. | | `shared/compat.lua` | Maps `ESX.Table`, `ESX.Math`, `ESX.SetTimeout` and similar to their esx_lib versions. | | `server/classes/` | The xPlayer class (split by topic) and the extended vehicle class. | | `server/modules/` | Player sessions and saving, commands, callbacks, jobs, items, pickups, paychecks, OneSync helpers, Discord logging. | | `server/bridge/` | Inventory bridges, currently ox_inventory. | | `server/migration/` | Automatic database migrations that run on start. | | `client/modules/` | Spawning, player data sync, adjustments, admin tools, death handling, UI wrappers. | | `client/imports/` | Optional modules loaded by `imports.lua` on the client. | | `locales/` | Translations, selected with the `esx:locale` convar. | ## Server, client and shared [#server-client-and-shared] | Side | What you use | Reference | | --- | --- | --- | | Server | `ESX.GetPlayerFromId`, `ESX.RegisterCommand`, `ESX.RegisterServerCallback`, `ESX.CreateJob`, ... | [Server functions](/docs/es_extended/server/functions) | | Server | Player methods: money, items, job, metadata | [xPlayer](/docs/es_extended/server/xplayer) | | Server | Entity spawning and area searches | [OneSync](/docs/es_extended/server/onesync) | | Client | `ESX.PlayerData`, notifications, `ESX.TriggerServerCallback`, game helpers | [Client functions](/docs/es_extended/client/functions) | | Both | `ESX.GetConfig`, `ESX.GetWeapon`, `ESX.Math`, `ESX.Table`, timeouts | [Shared functions](/docs/es_extended/shared) | Keep authority on the server. The client may ask, but the server checks distance, money, job and items before changing anything. ## Player lifecycle [#player-lifecycle] <Steps> <Step> ### Connecting [#connecting] The player joins. es_extended reads their identifier (`Config.Identifier`, default `license`). Without multicharacter, the character loads right away. With [esx_multicharacter](/docs/core/esx_multicharacter), it waits for the character selection. </Step> <Step> ### Loading [#loading] The `users` row is loaded (or created with the starting money and items), the xPlayer is built and stored in `ESX.Players`. The server event `esx:playerLoaded` fires with `playerId, xPlayer, isNew`. </Step> <Step> ### On the client [#on-the-client] The client receives `esx:playerLoaded` with the player data, spawns the ped and sets `ESX.PlayerLoaded = true`. From now on events like `esx:setJob` and `esx:setAccountMoney` keep `ESX.PlayerData` in sync. </Step> <Step> ### Playing [#playing] The character is saved every 10 minutes and paychecks run every `Config.PaycheckInterval`. </Step> <Step> ### Leaving [#leaving] On disconnect (or logout in multicharacter), `esx:playerDropped` fires with `playerId, reason`, the character is saved and removed from `ESX.Players`. On logout the client receives `esx:onPlayerLogout`. </Step> </Steps> ```lua title="server/main.lua" AddEventHandler("esx:playerLoaded", function(playerId, xPlayer, isNew) if isNew then xPlayer.addInventoryItem("phone", 1) end end) AddEventHandler("esx:playerDropped", function(playerId, reason) print(("Player %s left: %s"):format(playerId, reason)) end) ``` All events with their arguments: [server events](/docs/es_extended/events/server) and [client events](/docs/es_extended/events/client). ## Explore es_extended [#explore-es_extended] <Cards> <Card title="xPlayer" href="/docs/es_extended/server/xplayer">Every server-side player method: money, items, job, weapons, metadata.</Card> <Card title="Server functions" href="/docs/es_extended/server/functions">ESX.* on the server: players, callbacks, commands, jobs, items, vehicles.</Card> <Card title="Client functions" href="/docs/es_extended/client/functions">ESX.* on the client: player data, notifications, callbacks, UI.</Card> <Card title="Shared functions" href="/docs/es_extended/shared">Helpers available on both sides.</Card> <Card title="PlayerData" href="/docs/es_extended/playerdata">Which data ESX keeps per character and where to read it.</Card> <Card title="Commands" href="/docs/es_extended/commands">Built-in admin and player commands with permissions.</Card> <Card title="Config" href="/docs/es_extended/config">Every key in shared/config/main.lua.</Card> <Card title="Events" href="/docs/es_extended/events/server">Server and client events fired by es_extended.</Card> </Cards> ## Related [#related] - [Your first ESX resource](/docs/getting-started/first-resource) - [Coding practices](/docs/getting-started/coding-practices) - [esx_lib](/docs/core/esx_lib) - [Convert old ESX scripts](/docs/guides/convert-old-esx-scripts) # PlayerData (/docs/es_extended/playerdata) PlayerData is the information ESX keeps about the loaded character: identity, accounts, inventory, loadout, job, group and metadata. On the server it lives on the [xPlayer](/docs/es_extended/server/xplayer) object. On the client a copy is stored in `ESX.PlayerData` and kept up to date by ESX events. Read it on the client for UI and checks, but make every decision that matters (money, items, permissions) on the server. ## Overview [#overview] | Fact | Value | | --- | --- | | Server | `ESX.GetPlayerFromId(source)` returns the [xPlayer](/docs/es_extended/server/xplayer) | | Client | `ESX.PlayerData` (needs `@es_extended/imports.lua`) or `ESX.GetPlayerData()` | | Ready flag | `ESX.PlayerLoaded` (client) | | Sent to client | In `esx:playerLoaded`, then updated by events like `esx:setJob` and `esx:setAccountMoney` | | Saved columns | `accounts`, `job`, `job_grade`, `group`, `position`, `inventory`, `loadout`, `metadata` in `users` | ## Getting PlayerData [#getting-playerdata] <Tabs items={['Client', 'Server']}> <Tab value="Client"> ```lua title="fxmanifest.lua" shared_script "@es_extended/imports.lua" ``` ```lua title="client/main.lua" CreateThread(function() while not ESX.PlayerLoaded do Wait(250) end print("Job: " .. ESX.PlayerData.job.label) end) RegisterNetEvent("esx:setJob", function(job, lastJob) print(("Job changed from %s to %s"):format(lastJob.name, job.name)) end) ``` With the imports file, `ESX.PlayerData` is refreshed automatically in your resource. Without it, call `ESX.GetPlayerData()` each time you need fresh data, because the `ESX` object you got from the export is a copy. </Tab> <Tab value="Server"> ```lua title="server/main.lua" local xPlayer = ESX.GetPlayerFromId(source) if xPlayer then print(xPlayer.getName(), xPlayer.getJob().name, xPlayer.getAccount("bank").money) end ``` Use the getter methods on the server. They return the same tables as the raw fields but stay correct when ESX changes internals. </Tab> </Tabs> ## Field reference [#field-reference] "Client" means `ESX.PlayerData.<field>`, "Server" means the xPlayer object. | Field | Type | Client | Server | Description | | --- | --- | --- | --- | --- | | `identifier` | `string` | Yes | `getIdentifier()` | Character identifier, `char1:...` with multicharacter. | | `ssn` | `string` | Yes | `getSSN()` | Social security number `XXX-XX-XXXX`. | | `name` | `string` | Yes | `getName()` | Character name, or the FiveM name without an identity. | | `firstName` | `string` | Yes | `get("firstName")` | Default `"John"` until an identity exists. | | `lastName` | `string` | Yes | `get("lastName")` | Default `"Doe"`. | | `dateofbirth` | `string` | Yes | `get("dateofbirth")` | As stored by esx_identity, default `"01/01/2000"`. | | `sex` | `string` | Yes | `get("sex")` | `"m"` or `"f"`. Only set when the character has an identity. | | `height` | `number` | Yes | `get("height")` | Height in cm, default `120`. | | `group` | `string` | Yes | `getGroup()` | Permission group. | | `job` | `table` | Yes | `getJob()` | See [job](#job). | | `accounts` | `table[]` | Yes | `getAccounts()` | See [accounts](#accounts). | | `money` | `number` | Yes (snapshot) | `getMoney()` | Cash at load time. Not updated on the client afterwards. | | `inventory` | `table[]` | Yes | `getInventory()` | See [inventory](#inventory). | | `weight` | `number` | Yes (load time) | `getWeight()` | Carried weight. | | `maxWeight` | `number` | Yes | `getMaxWeight()` | Weight limit. | | `loadout` | `table[]` | Yes | `getLoadout()` | See [loadout](#loadout). | | `metadata` | `table` | Yes | `getMeta()` | Persistent custom data. | | `variables` | `table` | Yes | `get(key)` | Non-persistent custom data set with `xPlayer.set`. | | `coords` | `vector3` | Yes (live) | `getCoords()` | On the client this reads the ped's current position. | | `skin` | `table` | Yes | no | Saved skinchanger data. | | `dead` | `boolean` | Yes | no | Updated on death and respawn. | | `ped` | `number` | Yes | no | Current ped handle. | | `weapon` | `number` | Yes | no | Hash of the weapon in hand. | | `vehicle` | `number` | Yes | no | Current vehicle handle, if any. | | `seat` | `number` | Yes | no | Current seat index, if in a vehicle. | | `source` / `playerId` | `number` | no | Yes | Server ID. Use `GetPlayerServerId(PlayerId())` on the client. | | `license` | `string` | no | Yes | Identifier with type prefix. | | `admin` | `boolean` | no | Yes | Admin state at load. | ## Details [#details] ### job [#job] ```lua { name = "police", label = "LSPD", type = "leo", onDuty = true, grade = 2, grade_name = "sergeant", grade_label = "Sergeant", grade_salary = 900, skin_male = {}, skin_female = {}, } ``` `grade` and `grade_salary` are numbers. `onDuty` comes from `metadata.jobDuty`. The field list is described in [xPlayer.getJob](/docs/es_extended/server/xplayer#getjob). ### accounts [#accounts] An array, not a map: ```lua { { name = "bank", money = 50000, label = "Bank", round = true, index = 1 }, { name = "money", money = 500, label = "Cash", round = true, index = 2 }, { name = "black_money", money = 0, label = "Dirty Money", round = true, index = 3 }, } ``` The order follows `Config.Accounts` and is not guaranteed. Look accounts up by name: <Tabs items={['Client', 'Server']}> <Tab value="Client"> ```lua local bank = ESX.GetAccount("bank") print("Bank: $" .. (bank and bank.money or 0)) ``` </Tab> <Tab value="Server"> ```lua local bank = xPlayer.getAccount("bank") print("Bank: $" .. bank.money) ``` </Tab> </Tabs> ### inventory [#inventory] With the default inventory: an array of the items the player owns (count above `0`), sorted by label. ```lua { { name = "bread", label = "Bread", count = 3, weight = 1, usable = true, rare = false, canRemove = true }, { name = "water", label = "Water", count = 1, weight = 1, usable = true, rare = false, canRemove = true }, } ``` ```lua title="client/main.lua" local breadCount = ESX.SearchInventory("bread", true) -- 0 when the player has none ``` With ox_inventory, use ox_inventory's own exports to read items. ### loadout [#loadout] ```lua { { name = "WEAPON_PISTOL", label = "Pistol", ammo = 48, components = { "clip_default", "flashlight" }, tintIndex = 0 }, } ``` Empty with ox_inventory. ### metadata [#metadata] A free-form table that is saved to the database. ESX itself uses `health`, `armor`, `lastPlaytime` and `jobDuty`. Write it on the server with [`xPlayer.setMeta`](/docs/es_extended/server/xplayer#setmeta); the client copy updates automatically. ```lua -- client local stress = ESX.PlayerData.metadata.stress or 0 ``` ## Reacting to changes on the client [#reacting-to-changes-on-the-client] | Event | Updates | | --- | --- | | `esx:playerLoaded` | Whole `ESX.PlayerData`. | | `esx:setJob` | `job` | | `esx:setAccountMoney` | One entry in `accounts` | | `esx:addInventoryItem`, `esx:removeInventoryItem`, `esx:setInventory` | `inventory` | | `esx:setMaxWeight` | `maxWeight` | | `esx:setGroup` | `group` | | `esx:updatePlayerData` | `metadata` or `variables` | | `esx:onPlayerLogout` | Clears `ESX.PlayerData` and sets `ESX.PlayerLoaded = false` | With `@es_extended/imports.lua` you can also define a global `OnPlayerData` function. It is called with `key, value, lastValue` whenever es_extended changes a field: ```lua title="client/main.lua" function OnPlayerData(key, value) if key == "job" then updateJobBlips(value.name) end end ``` See [client events](/docs/es_extended/events/client) for the full list. ## Common issues [#common-issues] - **`attempt to index a nil value (field 'job')`**: you read `ESX.PlayerData` before `esx:playerLoaded`. Wait for `ESX.PlayerLoaded`. - **Stale data in a resource without imports**: the exported `ESX` object is a snapshot. Use `ESX.GetPlayerData()` or switch to `@es_extended/imports.lua`. - **`xPlayer.firstName` is nil**: on the server, identity fields are variables. Use `xPlayer.get("firstName")`. - **Inventory indexed by name returns nil**: `inventory` is an array. Use `ESX.SearchInventory` or `xPlayer.getInventoryItem`. ## Related [#related] - [xPlayer](/docs/es_extended/server/xplayer) - [Client functions](/docs/es_extended/client/functions) - [Client events](/docs/es_extended/events/client) - [Shared playerdata helpers](/docs/es_extended/shared/playerdata) # Server Functions (/docs/es_extended/server/functions) These are the functions on the shared `ESX` object that only exist on the server. Get the object with `ESX = exports['es_extended']:getSharedObject()` or `@es_extended/imports.lua`, then call them from any server script. Functions that work on both sides (`ESX.GetConfig`, `ESX.GetWeapon`, `ESX.Math`, `ESX.Table`, ...) are on the [shared functions](/docs/es_extended/shared) page. ## Overview [#overview] | Fact | Value | | --- | --- | | Side | Server | | Source | `es_extended/server/modules/*` | | Player object | [xPlayer](/docs/es_extended/server/xplayer) | | Entity helpers | [OneSync functions](/docs/es_extended/server/onesync) | | Version | ESX Legacy 1.16.4 | ## Global tables [#global-tables] | Table | Content | | --- | --- | | `ESX.Players` | `{ [source] = xPlayer }` for every loaded character. | | `ESX.Jobs` | `{ [jobName] = job }`, each with a `grades` table keyed by grade as string. | | `ESX.Items` | `{ [itemName] = { label, weight, rare, canRemove } }`. With ox_inventory this is ox_inventory's item list. | <Callout type="warn"> Do not read these tables from other resources. The `ESX` object you receive through the export is a copy made at that moment, so the tables can be empty or stale. Use `ESX.GetExtendedPlayers`, `ESX.GetJobs` and `ESX.GetItems` instead. </Callout> ## Players [#players] ### GetPlayerFromId [#getplayerfromid] ```lua ESX.GetPlayerFromId(source) ``` | Name | Type | Description | | --- | --- | --- | | `source` | `number` or `string` | Server ID. Converted with `tonumber`. | **Returns:** [`xPlayer`](/docs/es_extended/server/xplayer) or `nil` if the player has no loaded character. ```lua RegisterNetEvent("myshop:buy", function(itemName) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return end -- validate itemName and price here end) ``` ### GetPlayerFromIdentifier [#getplayerfromidentifier] ```lua ESX.GetPlayerFromIdentifier(identifier) ``` | Name | Type | Description | | --- | --- | --- | | `identifier` | `string` | Character identifier as stored in `users.identifier` (with `charX:` prefix when multicharacter is used). | **Returns:** `xPlayer` or `nil` if that character is not online. ```lua local xTarget = ESX.GetPlayerFromIdentifier(row.owner) if xTarget then xTarget.showNotification("Your vehicle was impounded") end ``` ### GetPlayerIdFromIdentifier [#getplayeridfromidentifier] ```lua ESX.GetPlayerIdFromIdentifier(identifier) ``` | Name | Type | Description | | --- | --- | --- | | `identifier` | `string` | Character identifier. | **Returns:** `number` (server ID) or `nil`. ### IsPlayerLoaded [#isplayerloaded] ```lua ESX.IsPlayerLoaded(source) ``` | Name | Type | Description | | --- | --- | --- | | `source` | `number` | Server ID. Must be a number, strings are not converted. | **Returns:** `boolean`, `true` if an xPlayer exists for this ID. ### GetExtendedPlayers [#getextendedplayers] ```lua ESX.GetExtendedPlayers(key, value, minimal) ``` | Name | Type | Description | | --- | --- | --- | | `key` | `string?` | Field to filter by. `"job"` compares `job.name`; any other key compares `xPlayer[key]`, for example `"group"`. | | `value` | `any` or `table` | Value to match. A list of values returns a table grouped by value. | | `minimal` | `boolean?` | `true` returns server IDs instead of xPlayer objects. | **Returns:** - No key: array of all xPlayers (or IDs). - Single value: array of matching xPlayers (or IDs). - List of values: `{ [value] = { xPlayer, ... } }`. ```lua -- All police officers for _, xPlayer in ipairs(ESX.GetExtendedPlayers("job", "police")) do xPlayer.showNotification("Robbery in progress at the Fleeca bank") end -- Grouped by job, only IDs local byJob = ESX.GetExtendedPlayers("job", { "police", "ambulance" }, true) print(#byJob.police, #byJob.ambulance) ``` ### GetNumPlayers [#getnumplayers] ```lua ESX.GetNumPlayers(key, value) ``` | Name | Type | Description | | --- | --- | --- | | `key` | `string?` | Same as `GetExtendedPlayers`. `"job"` uses a cached per-job counter. | | `value` | `any` or `table` | Value or list of values. | **Returns:** `number`, or `{ [value] = count }` when `value` is a list. Without arguments it returns `#GetPlayers()`, which also counts players who are still loading. ```lua if ESX.GetNumPlayers("job", "police") < 2 then return xPlayer.showNotification("Not enough police online", "error") end ``` ### GetPlayers [#getplayers] ```lua ESX.GetPlayers() ``` Alias of the FiveM native `GetPlayers()`. Returns all connected server IDs as **strings**, including players without a loaded character. **Returns:** `string[]` ### Player [#player] ```lua ESX.Player(src) ``` | Name | Type | Description | | --- | --- | --- | | `src` | `number` or `string` | Server ID, or a character identifier. | Only available in resources that load `@es_extended/imports.lua`. Returns a static proxy: every key you index is a function that calls the matching xPlayer method through the `RunStaticPlayerMethod` export. Fields like `.job` are not available, use getters. **Returns:** static player or `nil` if not loaded. ```lua local player = ESX.Player(source) if player then player.addAccountMoney("bank", 250, "Daily bonus") end ``` ### ExtendedPlayers [#extendedplayers] ```lua ESX.ExtendedPlayers(key, value) ``` Static-player version of [`GetExtendedPlayers`](#getextendedplayers). Same filters, returns static proxies. Only available through `@es_extended/imports.lua`. ```lua for _, medic in ipairs(ESX.ExtendedPlayers("job", "ambulance")) do medic.showNotification("New dispatch call") end ``` ### GetIdentifier [#getidentifier] ```lua ESX.GetIdentifier(playerId) ``` | Name | Type | Description | | --- | --- | --- | | `playerId` | `number` or `string` | Server ID. | Returns the player's identifier of the type set in `Config.Identifier` (convar `esx:identifier`, default `license`) **without** the `license:` prefix. In FXDK mode it returns `"ESX-DEBUG-LICENCE"`. Asserts if the player has no identifier of that type. **Returns:** `string` ```lua AddEventHandler("playerConnecting", function(_, _, deferrals) local identifier = ESX.GetIdentifier(source) print("Connecting: " .. identifier) end) ``` <Callout type="info"> With multicharacter, `users.identifier` is `char1:<identifier>`, so `ESX.GetIdentifier` does not equal `xPlayer.identifier`. Use `xPlayer.getIdentifier()` for database lookups. </Callout> ## Callbacks [#callbacks] Server callbacks are handled by [esx_lib](/docs/core/esx_lib). See the [callbacks guide](/docs/guides/callbacks) for the full flow with the client side. ### RegisterServerCallback [#registerservercallback] ```lua ESX.RegisterServerCallback(name, handler) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Unique callback name, up to 200 characters. | | `handler` | `function` | `function(source, cb, ...)`. Call `cb(...)` with the values to return. | The resource that registers a name first owns it. Registering the same name from another resource throws an error; registering again from the same resource replaces the handler. ```lua ESX.RegisterServerCallback("myshop:getBalance", function(source, cb) local xPlayer = ESX.GetPlayerFromId(source) cb(xPlayer and xPlayer.getAccount("bank").money or 0) end) ``` The client calls it with `ESX.TriggerServerCallback("myshop:getBalance", function(balance) end)`, see [client functions](/docs/es_extended/client/functions). ### DoesServerCallbackExist [#doesservercallbackexist] ```lua ESX.DoesServerCallbackExist(name) ``` **Returns:** `boolean`, whether a callback with this name is registered. ### TriggerClientCallback [#triggerclientcallback] ```lua ESX.TriggerClientCallback(player, name, cb, ...) ``` | Name | Type | Description | | --- | --- | --- | | `player` | `number` | Target server ID. | | `name` | `string` | Client callback name (registered with `ESX.RegisterClientCallback`). | | `cb` | `function` | Receives the values the client returns. | | `...` | `any` | Arguments for the client. | ```lua ESX.TriggerClientCallback(source, "myscript:getStreetName", function(street) print("Player is on " .. street) end) ``` ### AwaitClientCallback [#awaitclientcallback] ```lua ESX.AwaitClientCallback(player, name, ...) ``` Same as `TriggerClientCallback` but yields the current thread and returns the client's values directly. **Returns:** `...` (whatever the client returned) ```lua local street = ESX.AwaitClientCallback(source, "myscript:getStreetName") ``` ## Events [#events] ### TriggerClientEvent [#triggerclientevent] ```lua ESX.TriggerClientEvent(eventName, playerIds, ...) ``` | Name | Type | Description | | --- | --- | --- | | `eventName` | `string` | Client event name. | | `playerIds` | `number` or `number[]` | One server ID or an array of IDs. | | `...` | `any` | Event arguments. | For an array, the arguments are serialized once and sent to every player, which is cheaper than calling `TriggerClientEvent` in a loop. ```lua local officers = ESX.GetExtendedPlayers("job", "police", true) ESX.TriggerClientEvent("dispatch:client:alert", officers, { code = "10-31", coords = coords }) ``` ## Commands [#commands] ### RegisterCommand [#registercommand] ```lua ESX.RegisterCommand(name, group, cb, allowConsole, suggestion) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` or `string[]` | Command name, or a list of aliases that share the same handler. | | `group` | `string` or `string[]` | Group(s) allowed to use it. ESX adds `add_ace group.<group> command.<name> allow`. | | `cb` | `function` | `function(xPlayer, args, showError)`. `xPlayer` is `false` when run from the console. | | `allowConsole` | `boolean?` | Allow the server console to run it. | | `suggestion` | `table?` | `{ help, validate, arguments }` for chat suggestions and argument parsing. | Each entry in `suggestion.arguments` is `{ name, help, type, Validator? }`: | `type` | Parsed into `args[name]` | | --- | --- | | `number` | `tonumber(arg)`, error if not numeric | | `player` | the target's **xPlayer** (`me` = yourself), error if not online | | `playerId` | the target's server ID (`me` = yourself), error if not online | | `string` | the raw string; numeric strings are rejected | | `item` | the item name, error if it is not in `ESX.Items` | | `weapon` | the upper-case weapon name from `Config.Weapons` | | `coordinate` | the first number found in the argument | | `merge` | this argument and everything after it, joined with spaces | | `any` | the raw string | Set `validate = true` to require the exact number of arguments. `Validator = { validate = function(value) return true end, err = "message" }` adds a custom check per argument. `showError(msg)` shows a notification to the player, or prints to the console. Registering an existing name again overrides the previous handler and prints a warning. ```lua title="server/commands.lua" ESX.RegisterCommand("givecash", "admin", function(xPlayer, args, showError) args.target.addMoney(args.amount, "Admin gift") showError(("Gave $%d to %s"):format(args.amount, args.target.getName())) end, true, { help = "Give cash to a player", validate = true, arguments = { { name = "target", help = "Player ID", type = "player" }, { name = "amount", help = "Amount", type = "number", Validator = { validate = function(value) return value > 0 end, err = "Amount must be positive", } }, }, }) ``` The list of built-in commands is on the [commands page](/docs/es_extended/commands). ## Jobs [#jobs] ### GetJobs [#getjobs] ```lua ESX.GetJobs(jobType) ``` | Name | Type | Description | | --- | --- | --- | | `jobType` | `string`, `string[]` or `nil` | Only return jobs of this `type` (for example `"civ"`). | Waits until jobs are loaded, then returns `{ [jobName] = job }`. Each job contains the columns of its `jobs` row (`name`, `label`, `type`, ...) plus `grades`, keyed by grade as string. ```lua for name, job in pairs(ESX.GetJobs()) do print(name, job.label) end ``` ### DoesJobExist [#doesjobexist] ```lua ESX.DoesJobExist(job, grade) ``` | Name | Type | Description | | --- | --- | --- | | `job` | `string` | Job name. | | `grade` | `number` or `string` | Grade number. | Waits until jobs are loaded. **Returns:** `boolean` ```lua if ESX.DoesJobExist("police", 4) then xPlayer.setJob("police", 4) end ``` ### RefreshJobs [#refreshjobs] ```lua ESX.RefreshJobs() ``` Reloads every job and grade from the `jobs` and `job_grades` tables. Jobs without grades are skipped. If `unemployed` (or its grade `0`) is missing, a fallback is created. Fires `esx:jobsRefreshed`. ### RefreshJob [#refreshjob] ```lua ESX.RefreshJob(jobName) ``` | Name | Type | Description | | --- | --- | --- | | `jobName` | `string` | Job to reload from the database. | Reloads one job and calls `xPlayer.refreshJob()` for every online member. Fires `esx:jobRefreshed` (`jobName, job`). If the job or its grades are gone, it is removed, its members become `unemployed` and `esx:jobRemoved` fires. **Returns:** `boolean`, `true` if the job was refreshed. ```lua MySQL.update.await("UPDATE job_grades SET salary = ? WHERE job_name = ? AND grade = ?", { 900, "police", 2 }) ESX.RefreshJob("police") ``` ### CreateJob [#createjob] ```lua ESX.CreateJob(name, label, grades, jobType) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Job name. | | `label` | `string` | Display label. | | `grades` | `table[]` | `{ grade, name, label, salary, skin_male?, skin_female? }` entries. | | `jobType` | `string?` | Job type. Defaults to `"civ"`. | Inserts the job and any missing grades into the database in one transaction and adds them to `ESX.Jobs`. If the job exists, only new grades are added and the existing label and type are kept. Fires `esx:jobCreated`. **Returns:** `boolean`, `false` if arguments are missing, nothing new was inserted, or the transaction failed. ```lua ESX.CreateJob("baker", "Baker", { { grade = 0, name = "trainee", label = "Trainee", salary = 150 }, { grade = 1, name = "baker", label = "Baker", salary = 300 }, { grade = 2, name = "boss", label = "Owner", salary = 500 }, }) ``` See [create a job](/docs/guides/create-a-job). ## Items [#items] ### GetItems [#getitems] ```lua ESX.GetItems() ``` **Returns:** `{ [itemName] = { label, weight, rare, canRemove } }` ### GetItemLabel [#getitemlabel] ```lua ESX.GetItemLabel(item) ``` **Returns:** `string` or `nil` (prints a warning for unknown items). With ox_inventory the label comes from `exports.ox_inventory:Items(item)`. ```lua xPlayer.showNotification(("You received %s"):format(ESX.GetItemLabel("bread"))) ``` ### AddItems [#additems] ```lua ESX.AddItems(items) ``` | Name | Type | Description | | --- | --- | --- | | `items` | `table[]` | `{ name, label, weight?, rare?, canRemove? }` entries. `weight` defaults to `1`, `rare` to `false`, `canRemove` to `true`. | Inserts new items into the `items` table (`INSERT IGNORE`), registers them in `ESX.Items` and refreshes online inventories. Items that already exist or have invalid fields are skipped. Not available with ox_inventory. ```lua ESX.AddItems({ { name = "coffee", label = "Coffee", weight = 0.3 }, { name = "donut", label = "Donut", weight = 0.2 }, }) ``` ### RefreshItems [#refreshitems] ```lua ESX.RefreshItems() ``` Reloads all items from the `items` table and removes unknown items from online players. Not available with ox_inventory. **Returns:** `number`, the number of items loaded. ### RegisterUsableItem [#registerusableitem] ```lua ESX.RegisterUsableItem(item, cb) ``` | Name | Type | Description | | --- | --- | --- | | `item` | `string` | Item name. | | `cb` | `function` | `function(source, itemName, ...)`, called when the player uses the item. | ESX does **not** remove the item for you. Remove it inside the callback. ```lua ESX.RegisterUsableItem("bandage", function(source) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer or not xPlayer.removeInventoryItem("bandage", 1) then return end TriggerClientEvent("myhealth:client:heal", source, 25) end) ``` ### UseItem [#useitem] ```lua ESX.UseItem(source, item, ...) ``` Runs the registered usable callback for `item` as if the player used it. Does not check whether the player owns the item. Errors inside the callback are caught and printed. ### GetUsableItems [#getusableitems] ```lua ESX.GetUsableItems() ``` **Returns:** `{ [itemName] = true }` for every registered usable item. ### CreatePickup [#createpickup] ```lua ESX.CreatePickup(itemType, name, count, label, playerId, components, tintIndex, coords, bucket) ``` | Name | Type | Description | | --- | --- | --- | | `itemType` | `string` | `"item_standard"`, `"item_account"` or `"item_weapon"`. | | `name` | `string` | Item, account or weapon name. | | `count` | `number` | Amount, or ammo for weapons. | | `label` | `string` | Text shown on the pickup. | | `playerId` | `number?` | Player that dropped it. Used for position, routing bucket and per-player limits. | | `components` | `table?` | Weapon components (`item_weapon` only). | | `tintIndex` | `number?` | Weapon tint (`item_weapon` only). | | `coords` | `vector3`, `vector4` or `table` | Position. Required when `playerId` is not given. | | `bucket` | `number?` | Routing bucket. Defaults to the player's bucket, or `0`. | Pickups expire after 30 minutes. ESX limits them to 50 per player, 250 per 100 m grid cell and 5000 in total. Not available with ox_inventory. **Returns:** pickup ID (`number`) or `nil` when a limit is reached or no position is known. ```lua ESX.CreatePickup("item_standard", "bread", 3, "Bread x3", nil, nil, nil, vector3(215.0, -810.0, 30.7)) ``` ## Vehicles [#vehicles] ### GetVehicleType [#getvehicletype] ```lua ESX.GetVehicleType(model, player, cb) ``` | Name | Type | Description | | --- | --- | --- | | `model` | `string` or `number` | Model name or hash. | | `player` | `number?` | A player to ask when the type is not cached yet. | | `cb` | `function?` | Receives the type. Without it the function yields and returns the type. | Returns the server vehicle type for `CreateVehicleServerSetter`: `"automobile"`, `"bike"`, `"boat"`, `"heli"`, `"plane"`, `"submarine"`, `"trailer"` or `"train"`. Unknown models without a player return `nil`. The server console command `clearvehicletypes` clears the cache. ```lua local vehicleType = ESX.GetVehicleType("t20", source) ``` ### CreateExtendedVehicle [#createextendedvehicle] ```lua ESX.CreateExtendedVehicle(owner, plate, coords) ``` | Name | Type | Description | | --- | --- | --- | | `owner` | `string` | Owner identifier (`owned_vehicles.owner`). | | `plate` | `string` | Plate of a **stored** owned vehicle. | | `coords` | `vector4` | Spawn position and heading. | Spawns an owned vehicle from `owned_vehicles` with its saved properties, marks it as not stored and tracks it. Returns the existing object if that plate is already out. Fires `esx:createdExtendedVehicle`. **Returns:** extended vehicle object or `nil`. | Method | Description | | --- | --- | | `vehicle:isValid()` | `true` while the entity still exists and matches owner and plate. Deletes the record otherwise. | | `vehicle:getPlate()` / `getNetId()` / `getEntity()` / `getModelHash()` / `getOwner()` | Getters. | | `vehicle:setPlate(newPlate)` | Updates the database, entity and state bag. Returns `boolean`. | | `vehicle:setProps(props)` | Saves new properties and syncs them. Returns `boolean`. | | `vehicle:setOwner(identifier)` | Transfers ownership in the database. Returns `boolean`. | | `vehicle:delete(garageName?, isImpound?)` | Deletes the entity and marks the vehicle as stored (in `parking` or `pound`). | ```lua local vehicle = ESX.CreateExtendedVehicle(xPlayer.identifier, "ESX 123", vector4(215.8, -810.1, 30.7, 160.0)) if vehicle then print("Spawned with net ID " .. vehicle:getNetId()) end ``` ### GetExtendedVehicleFromPlate [#getextendedvehiclefromplate] ```lua ESX.GetExtendedVehicleFromPlate(plate) ``` **Returns:** the extended vehicle object for a spawned plate, or `nil`. ```lua local vehicle = ESX.GetExtendedVehicleFromPlate("ESX 123") if vehicle then vehicle:delete("legion_garage") end ``` ## Player function overrides [#player-function-overrides] ### RegisterPlayerFunctionOverrides [#registerplayerfunctionoverrides] ```lua ESX.RegisterPlayerFunctionOverrides(index, overrides) ``` | Name | Type | Description | | --- | --- | --- | | `index` | `string` | Name of the override set. | | `overrides` | `table` | `{ [methodName] = function(self) return function(...) end end }` | Each entry is a factory that receives the xPlayer and returns the new method. All registered sets are applied to every xPlayer created afterwards. ESX uses this for its ox_inventory bridge. ```lua ESX.RegisterPlayerFunctionOverrides("myOverrides", { getName = function(self) return function() return ("[%s] %s"):format(self.job.label, self.name) end end, }) ``` ### SetPlayerFunctionOverride [#setplayerfunctionoverride] ```lua ESX.SetPlayerFunctionOverride(index) ``` <Callout type="warn" title="No effect in 1.16.4"> This only stores `index` in `Config.PlayerFunctionOverride`, which es_extended no longer reads. Every registered override set is applied regardless. </Callout> ## Logging and debug [#logging-and-debug] ### DiscordLog [#discordlog] ```lua ESX.DiscordLog(name, title, color, message) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Webhook key in `Config.DiscordLogs.Webhooks`. Falls back to `default`. | | `title` | `string` | Embed title. | | `color` | `string` | Color key in `Config.DiscordLogs.Colors`. | | `message` | `string` | Embed description. | ```lua ESX.DiscordLog("UserActions", "Bank robbery", "red", ("%s started a robbery"):format(xPlayer.getName())) ``` See the [logs config](/docs/es_extended/config/logs). ### DiscordLogFields [#discordlogfields] ```lua ESX.DiscordLogFields(name, title, color, fields) ``` | Name | Type | Description | | --- | --- | --- | | `fields` | `table[]` | Discord embed fields `{ name, value, inline? }`. Values are converted with `tostring`. | Other parameters are the same as `DiscordLog`. ```lua ESX.DiscordLogFields("UserActions", "Item sold", "green", { { name = "Player", value = xPlayer.getName(), inline = true }, { name = "Item", value = "gold_bar", inline = true }, { name = "Price", value = 2500, inline = true }, }) ``` ### Trace [#trace] ```lua ESX.Trace(msg) ``` Prints `[TRACE] msg` to the server console only when `Config.EnableDebug` is `true`. ## Common issues [#common-issues] - **`ESX.Player` is nil**: you did not add `@es_extended/imports.lua` to `shared_scripts`. - **Empty `ESX.Jobs` in your resource**: read jobs with `ESX.GetJobs()`, which also waits until they are loaded. - **`AddItems`, `RefreshItems` or `CreatePickup` is nil**: ox_inventory is running, so the default inventory functions are not defined. ## Related [#related] - [xPlayer](/docs/es_extended/server/xplayer) - [OneSync functions](/docs/es_extended/server/onesync) - [Shared functions](/docs/es_extended/shared) - [Server events](/docs/es_extended/events/server) - [Callbacks guide](/docs/guides/callbacks) # OneSync Functions (/docs/es_extended/server/onesync) `ESX.OneSync` groups server-side helpers that need OneSync: area searches that the server could otherwise not do without asking a client, and functions that spawn networked entities owned by the server. Use them when the server must decide where entities are, for example for anti-cheat checks, job vehicles or NPCs that should survive their creator leaving. ## Overview [#overview] | Fact | Value | | --- | --- | | Side | Server | | Requires | OneSync (`set onesync on` in `server.cfg`) | | Area functions | Implemented in [esx_lib](/docs/core/esx_lib) (`xLib.onesync`), exposed on `ESX.OneSync` | | Spawn functions | `es_extended/server/modules/onesync.lua` | | Return values | Network IDs, not entity handles | ## Players [#players] ### GetPlayersInArea [#getplayersinarea] ```lua ESX.OneSync.GetPlayersInArea(source, maxDistance, ignore, routingBucket) ``` | Name | Type | Description | | --- | --- | --- | | `source` | `number` or `vector3` | A player ID (uses their ped position) or coordinates. | | `maxDistance` | `number?` | Radius. Default `100`. | | `ignore` | `table?` | `{ [playerId] = true }` for players to skip. | | `routingBucket` | `number?` | Only include players in this routing bucket. | **Returns:** array of `{ id, ped, coords, dist }`. `ped` is the network ID of the player's ped. Empty table if nobody is in range. ```lua local nearby = ESX.OneSync.GetPlayersInArea(source, 15.0, { [source] = true }) for i = 1, #nearby do TriggerClientEvent("myscript:client:hearShot", nearby[i].id, nearby[i].dist) end ``` ### GetClosestPlayer [#getclosestplayer] ```lua ESX.OneSync.GetClosestPlayer(source, maxDistance, ignore, routingBucket) ``` Same parameters as `GetPlayersInArea`. When `source` is a player ID, that player is skipped. **Returns:** a single `{ id, ped, coords, dist }` table, or an empty table if no player is in range. ```lua local closest = ESX.OneSync.GetClosestPlayer(source, 3.0) if closest.id then local xTarget = ESX.GetPlayerFromId(closest.id) -- e.g. search or cuff xTarget end ``` ## Entities in an area [#entities-in-an-area] The model filter is an allow-list: when you pass `{ [joaat("model")] = true }`, only entities with those models are returned. ### GetPedsInArea [#getpedsinarea] ```lua ESX.OneSync.GetPedsInArea(coords, maxDistance, modelFilter) ``` | Name | Type | Description | | --- | --- | --- | | `coords` | `vector3`, `table` or `number` | Center, or a player ID to use their position. | | `maxDistance` | `number?` | Radius. Without it, every matching ped on the server is returned. | | `modelFilter` | `table?` | `{ [modelHash] = true }`. Only these models are returned. | Player peds are excluded. **Returns:** `number[]`, network IDs. ```lua local peds = ESX.OneSync.GetPedsInArea(vector3(-47.0, -1757.0, 29.4), 30.0) print(("%d NPCs near the store"):format(#peds)) ``` ### GetObjectsInArea [#getobjectsinarea] ```lua ESX.OneSync.GetObjectsInArea(coords, maxDistance, modelFilter) ``` Same parameters and return value as `GetPedsInArea`, for objects. ```lua local atms = ESX.OneSync.GetObjectsInArea(source, 5.0, { [`prop_atm_01`] = true }) ``` ### GetVehiclesInArea [#getvehiclesinarea] ```lua ESX.OneSync.GetVehiclesInArea(coords, maxDistance, modelFilter) ``` Same parameters and return value as `GetPedsInArea`, for vehicles. The built-in `/dv` command uses it. ```lua for _, netId in ipairs(ESX.OneSync.GetVehiclesInArea(coords, 10.0)) do local vehicle = NetworkGetEntityFromNetworkId(netId) if DoesEntityExist(vehicle) then DeleteEntity(vehicle) end end ``` ## Closest entity [#closest-entity] These search within a fixed radius of 100 units. ### GetClosestPed [#getclosestped] ```lua ESX.OneSync.GetClosestPed(coords, modelFilter) ``` | Name | Type | Description | | --- | --- | --- | | `coords` | `vector3`, `table` or `number` | Center, or a player ID. | | `modelFilter` | `table?` | `{ [modelHash] = true }` allow-list. | **Returns:** `netId, distance, coords`. Player peds are skipped. If nothing is within 100 units, the distance stays `100` and the network ID does not point to a real entity, so check it with `DoesEntityExist(NetworkGetEntityFromNetworkId(netId))`. ### GetClosestObject [#getclosestobject] ```lua ESX.OneSync.GetClosestObject(coords, modelFilter) ``` Same as `GetClosestPed`, for objects. ### GetClosestVehicle [#getclosestvehicle] ```lua ESX.OneSync.GetClosestVehicle(coords, modelFilter) ``` Same as `GetClosestPed`, for vehicles. ```lua local netId, distance = ESX.OneSync.GetClosestVehicle(source) if distance < 5.0 then local vehicle = NetworkGetEntityFromNetworkId(netId) print("Closest plate: " .. GetVehicleNumberPlateText(vehicle)) end ``` ## Spawning entities [#spawning-entities] All spawn functions work in two modes. With a callback, they return immediately and call `cb(netId)` later (`cb(false)` on failure). Without a callback, they yield the current thread and return the network ID; a failure then raises an error, so wrap the call in `pcall` if you need to handle it. ### SpawnVehicle [#spawnvehicle] ```lua ESX.OneSync.SpawnVehicle(model, coords, heading, properties, cb, vehicleType) ``` | Name | Type | Description | | --- | --- | --- | | `model` | `string` or `number` | Model name or hash. | | `coords` | `vector3` or `table` | Spawn position. | | `heading` | `number` | Heading. | | `properties` | `table` | Vehicle properties. Stored in the `VehicleProperties` entity state bag, which the client applies. Use `{}` for none. | | `cb` | `function?` | `function(netId)`. You can also pass the vehicle type string here and skip the callback. | | `vehicleType` | `string?` | `"automobile"`, `"bike"`, `"boat"`, `"heli"`, `"plane"`, `"submarine"`, `"trailer"` or `"train"`. Resolved with [`ESX.GetVehicleType`](/docs/es_extended/server/functions#getvehicletype) when omitted. | Uses `CreateVehicleServerSetter` and sets the orphan mode so the vehicle is not deleted when its owner leaves. **Returns:** network ID when called without a callback. ```lua local netId = ESX.OneSync.SpawnVehicle("police", vector3(452.0, -1019.0, 28.4), 90.0, { plate = "LSPD 01" }, "automobile") local vehicle = NetworkGetEntityFromNetworkId(netId) TaskWarpPedIntoVehicle(GetPlayerPed(source), vehicle, -1) ``` <Callout type="warn"> Without `vehicleType`, ESX must ask a client for unknown models. If no player is online, the spawn fails. Pass the type for vehicles spawned at server start. </Callout> ### SpawnObject [#spawnobject] ```lua ESX.OneSync.SpawnObject(model, coords, heading, cb) ``` | Name | Type | Description | | --- | --- | --- | | `model` | `string` or `number` | Model name or hash. | | `coords` | `vector3` or `table` | Position. | | `heading` | `number` | Heading. | | `cb` | `function?` | `function(netId)`. | **Returns:** network ID when called without a callback. ```lua ESX.OneSync.SpawnObject("prop_roadcone02a", coords, 0.0, function(netId) if not netId then return end FreezeEntityPosition(NetworkGetEntityFromNetworkId(netId), true) end) ``` ### SpawnPed [#spawnped] ```lua ESX.OneSync.SpawnPed(model, coords, heading, cb) ``` Same parameters as `SpawnObject`, creates a networked ped. ```lua local netId = ESX.OneSync.SpawnPed("s_m_y_cop_01", vector3(441.0, -978.0, 30.7), 180.0) ``` ### SpawnPedInVehicle [#spawnpedinvehicle] ```lua ESX.OneSync.SpawnPedInVehicle(model, vehicle, seat, cb) ``` | Name | Type | Description | | --- | --- | --- | | `model` | `string` or `number` | Ped model. | | `vehicle` | `number` | Vehicle **entity handle** (not a network ID). | | `seat` | `number` | Seat index, `-1` is the driver. | | `cb` | `function?` | `function(netId)`. | **Returns:** network ID of the ped when called without a callback. ```lua local vehNet = ESX.OneSync.SpawnVehicle("taxi", coords, heading, {}, "automobile") local vehicle = NetworkGetEntityFromNetworkId(vehNet) ESX.OneSync.SpawnPedInVehicle("a_m_y_stlat_01", vehicle, -1) ``` ## Common issues [#common-issues] - **Working with the wrong handle**: every function returns network IDs. Use `NetworkGetEntityFromNetworkId` before calling entity natives. - **Entities in other dimensions**: only the player functions filter by routing bucket. Area and closest-entity searches cover all buckets. - **Spawned ped or object disappears**: only `SpawnVehicle` calls `SetEntityOrphanMode(entity, 2)`. Call it yourself on peds and objects that must persist when no player is near. ## Related [#related] - [Server functions](/docs/es_extended/server/functions) - [xPlayer](/docs/es_extended/server/xplayer) - [esx_lib](/docs/core/esx_lib) - [Client game module](/docs/es_extended/client/modules/game) # xPlayer (/docs/es_extended/server/xplayer) `xPlayer` is the server-side object that represents one loaded ESX character. You get it with `ESX.GetPlayerFromId(source)` and use its methods to change money, items, job, group, weapons and metadata. Every change goes through these methods so that es_extended can sync the client, fire events and save the data to the database. ## Overview [#overview] | Fact | Value | | --- | --- | | Side | Server only | | Defined in | `es_extended/server/classes/player.lua` and `server/classes/player/*.lua` | | Created by | es_extended when a character loads (`CreateExtendedPlayer`) | | Stored in | `ESX.Players[source]`, also indexed by identifier | | Saved | Every 10 minutes, on `/save` and `/saveall`, and when the player drops | | Version | ESX Legacy 1.16.4 | <Callout type="info" title="ox_inventory changes some methods"> When the `ox_inventory` resource exists, es_extended sets `Config.CustomInventory = "ox"` and replaces the inventory, weapon and some account methods with versions that forward to ox_inventory. Each affected method below has an "ox_inventory" note. See the [ox_inventory guide](/docs/guides/ox-inventory). </Callout> ## Getting an xPlayer [#getting-an-xplayer] ```lua title="server/main.lua" local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return -- player is not loaded (still connecting, in character selection, or gone) end print(xPlayer.getName(), xPlayer.getJob().name) ``` Other ways to get a player object: | Function | Returns | | --- | --- | | [`ESX.GetPlayerFromId(source)`](/docs/es_extended/server/functions#getplayerfromid) | `xPlayer` or `nil` | | [`ESX.GetPlayerFromIdentifier(identifier)`](/docs/es_extended/server/functions#getplayerfromidentifier) | `xPlayer` or `nil` | | [`ESX.GetExtendedPlayers(key, value)`](/docs/es_extended/server/functions#getextendedplayers) | array of `xPlayer`, optionally filtered | | [`ESX.Player(source)`](/docs/es_extended/server/functions#player) | a static proxy (see below) | ### Static players (`ESX.Player`) [#static-players-esxplayer] `ESX.Player(src)` and `ESX.ExtendedPlayers()` exist only in resources that load `@es_extended/imports.lua`. They return a lightweight proxy instead of the real object. Every key you index on the proxy becomes a function that calls the method through the `RunStaticPlayerMethod` export: ```lua local player = ESX.Player(source) if player then player.addAccountMoney("bank", 500, "Quest reward") local job = player.getJob() -- returns a copy, not a live reference end ``` <Callout type="warn" title="Only methods work on static players"> `player.job`, `player.name` and other fields do not exist on the proxy. Indexing any key returns a function. Use the getters (`getJob()`, `getName()`) instead. Tables returned through the export are copies, so changing them has no effect on the player. </Callout> ## Properties [#properties] The object also exposes raw fields. Read them if you like, but change state only through methods, otherwise the client is not updated and nothing is saved. | Field | Type | Description | | --- | --- | --- | | `source` / `playerId` | `number` | Server ID of the player. | | `identifier` | `string` | Database identifier. With multicharacter it looks like `char1:abc123...`. | | `license` | `string` | Full identifier with prefix, for example `license:abc123...`. Used for ACE principals. | | `ssn` | `string` | Social security number in `XXX-XX-XXXX` format, generated on character creation. | | `name` | `string` | Character name (`"First Last"`) or the FiveM name when no identity exists. | | `group` | `string` | Permission group, for example `user` or `admin`. | | `accounts` | `table[]` | Array of accounts: `{ name, money, label, round, index }`. | | `inventory` | `table[]` | Items with a count above 0, sorted by label (default inventory). | | `loadout` | `table[]` | Weapons: `{ name, label, ammo, components, tintIndex }`. Empty with ox_inventory. | | `job` | `table` | Current job, see [getJob](#getjob). | | `weight` / `maxWeight` | `number` | Carried weight and limit. `maxWeight` starts at `Config.MaxWeight`. | | `coords` | `table` | Last known `{ x, y, z, heading }`. | | `metadata` | `table` | Persistent custom data, see [Metadata](#metadata). | | `variables` | `table` | Non-persistent custom data set with [`set`](#set). | | `lastPlaytime` | `number` | Saved playtime in seconds when the character loaded. | | `paycheckEnabled` | `boolean` | Whether the player receives paychecks. | | `admin` | `boolean` | Admin state when the object was created. Use [`isAdmin()`](#isadmin) for the live value. | | `spawned` | `boolean` | Set to `true` once the client reports its first spawn. Players are not saved before that. | Character identity data (`firstName`, `lastName`, `dateofbirth`, `sex`, `height`) is **not** a field on the server object. It is stored as variables, so read it with `xPlayer.get("firstName")`. See [PlayerData](/docs/es_extended/playerdata). ## Identity and session [#identity-and-session] ### getIdentifier [#getidentifier] ```lua xPlayer.getIdentifier() ``` Returns the database identifier of the character (the `identifier` column in `users`). **Returns:** `string` ```lua local identifier = xPlayer.getIdentifier() MySQL.update("UPDATE users SET phone_number = ? WHERE identifier = ?", { number, identifier }) ``` ### getSSN [#getssn] ```lua xPlayer.getSSN() ``` Returns the character's social security number, for example `"482-17-3390"`. ESX generates a unique SSN when the character is created. **Returns:** `string` ### getSource [#getsource] ```lua xPlayer.getSource() ``` Returns the player's server ID. `xPlayer.getPlayerId()` is an alias. **Returns:** `number` ### getPlayerId [#getplayerid] ```lua xPlayer.getPlayerId() ``` Alias of [getSource](#getsource). **Returns:** `number` ### getName [#getname] ```lua xPlayer.getName() ``` Returns the character name. With esx_identity or multicharacter this is `"Firstname Lastname"`, otherwise the FiveM player name. **Returns:** `string` ### setName [#setname] ```lua xPlayer.setName(newName) ``` | Name | Type | Description | | --- | --- | --- | | `newName` | `string` | The new display name. | Changes the name on the object and in the `name` state bag. It does **not** update `firstname` / `lastname` in the database, so the change is lost on reconnect. ```lua xPlayer.setName("John Doe") ``` ### getPlayTime [#getplaytime] ```lua xPlayer.getPlayTime() ``` Returns the total playtime of the character in seconds: the saved playtime plus the time online in this session. The value is persisted in `metadata.lastPlaytime`. **Returns:** `number` ```lua local hours = math.floor(xPlayer.getPlayTime() / 3600) xPlayer.showNotification(("You have played %d hours"):format(hours)) ``` ### isCurrent [#iscurrent] ```lua xPlayer.isCurrent() ``` Returns `true` while this object is still the active entry in `ESX.Players`. Use it after a `Wait` or an awaited database query to make sure the player has not dropped or switched characters in the meantime. **Returns:** `boolean` ```lua local result = MySQL.single.await("SELECT * FROM my_table WHERE owner = ?", { xPlayer.identifier }) if not xPlayer.isCurrent() then return -- player left while we were waiting end ``` ### isAdmin [#isadmin] ```lua xPlayer.isAdmin() ``` Returns `true` if the player is allowed the ACE `command`, if `sv_lan` is `"true"`, or if the player's group is listed in `Config.AdminGroups`. **Returns:** `boolean` <Callout type="warn"> `Config.AdminGroups` is deprecated in favor of `Config.CommandPermissions`. For your own permission checks, prefer ACE (`IsPlayerAceAllowed`) or [`getGroup`](#getgroup). See [admin permissions](/docs/guides/admin-permissions). </Callout> ### kick [#kick] ```lua xPlayer.kick(reason) ``` | Name | Type | Description | | --- | --- | --- | | `reason` | `string` | Message shown to the player. | Drops the player from the server. ESX saves the character in the `playerDropped` handler. ```lua xPlayer.kick("You have been kicked for exploiting.") ``` ### triggerEvent [#triggerevent] ```lua xPlayer.triggerEvent(eventName, ...) ``` | Name | Type | Description | | --- | --- | --- | | `eventName` | `string` | Client event name. Must be a string, otherwise the call asserts. | | `...` | `any` | Arguments passed to the client handler. | Shortcut for `TriggerClientEvent(eventName, xPlayer.source, ...)`. ```lua xPlayer.triggerEvent("myjob:client:startShift", { route = 3 }) ``` ### executeCommand [#executecommand] ```lua xPlayer.executeCommand(command) ``` | Name | Type | Description | | --- | --- | --- | | `command` | `string` | The command line to run, without the leading `/`. | Makes the player's client run `ExecuteCommand(command)`. The command runs with the player's own permissions. Throws an error if `command` is not a string. ```lua xPlayer.executeCommand("e sit") ``` ## Position [#position] ### getCoords [#getcoords] ```lua xPlayer.getCoords(vector, heading) ``` | Name | Type | Description | | --- | --- | --- | | `vector` | `boolean?` | `true` returns a `vector3` (or `vector4` with `heading`). Otherwise a plain table. | | `heading` | `boolean?` | Include the heading (`w` in a vector4, `heading` in a table). | Reads the live ped position when the ped exists, otherwise the last cached position. Falls back to a random `Config.DefaultSpawns` entry if nothing valid is known. **Returns:** `vector3`, `vector4` or `{ x, y, z, heading? }` ```lua local coords = xPlayer.getCoords(true) if #(coords - vector3(441.8, -982.0, 30.7)) < 10.0 then print("Player is at Mission Row PD") end ``` ### setCoords [#setcoords] ```lua xPlayer.setCoords(coordinates) ``` | Name | Type | Description | | --- | --- | --- | | `coordinates` | `vector3`, `vector4` or `table` | Target position. Tables need `x`, `y`, `z` and may have `heading` or `w`. | Teleports the ped on the server side. Returns `false` if the coordinates are invalid (non-finite or beyond `Config.PlayerCoordinateLimit`) or the ped does not exist. Returns nothing on success. ```lua xPlayer.setCoords(vector4(-269.4, -955.3, 31.2, 205.0)) ``` ## Permission group [#permission-group] ### getGroup [#getgroup] ```lua xPlayer.getGroup() ``` Returns the player's permission group, for example `"user"`, `"admin"` or `"owner"`. **Returns:** `string` ```lua if xPlayer.getGroup() ~= "user" then print(xPlayer.getName() .. " is staff") end ``` ### setGroup [#setgroup] ```lua xPlayer.setGroup(newGroup) ``` | Name | Type | Description | | --- | --- | --- | | `newGroup` | `string` | The new group name. | Replaces the ACE principal `identifier.<license>` → `group.<old>` with `group.<new>`, updates the `group` state bag and fires `esx:setGroup` on the server (`source, group, lastGroup`) and on the client (`group, lastGroup`). The group is saved to the `users` table. ```lua xPlayer.setGroup("admin") ``` ## Variables [#variables] Variables are temporary key/value pairs on the player. They are not saved to the database. ESX itself uses them for identity data (`firstName`, `lastName`, `dateofbirth`, `sex`, `height`). ### set [#set] ```lua xPlayer.set(key, value, replicate) ``` | Name | Type | Description | | --- | --- | --- | | `key` | `string` | Variable name. | | `value` | `any` | Value to store. | | `replicate` | `boolean?` | Defaults to `true`. When not `false`, the whole `variables` table is sent to the client as `ESX.PlayerData.variables`. | ```lua xPlayer.set("lastRobbery", os.time(), false) -- server-only flag ``` ### get [#get] ```lua xPlayer.get(key) ``` | Name | Type | Description | | --- | --- | --- | | `key` | `string` | Variable name. | **Returns:** `any` (the stored value or `nil`) ```lua local first, last = xPlayer.get("firstName"), xPlayer.get("lastName") ``` ### updatePlayerData [#updateplayerdata] <Callout type="error" title="Removed"> `xPlayer.updatePlayerData` no longer exists in ESX Legacy 1.16.4. `set`, `setMeta` and `clearMeta` send `esx:updatePlayerData` to the client themselves. To push other data, use `xPlayer.triggerEvent("esx:updatePlayerData", key, value)`. </Callout> ## Money and accounts [#money-and-accounts] Every character has the accounts defined in [`Config.Accounts`](/docs/es_extended/config#accounts). By default these are `money` (cash), `bank` and `black_money`. Account names are case-insensitive in `getAccount`. See the [money and accounts guide](/docs/guides/money-and-accounts). <Callout type="warn" title="Invalid amounts throw errors"> With the default inventory, `setAccountMoney`, `addAccountMoney` and `removeAccountMoney` call Lua `error()` for unknown accounts and for amounts that are not valid numbers. `add` and `remove` also throw for amounts of `0` or below. Always validate user input (for example from NUI or events) before you pass it on. The ox_inventory versions return `false` instead. </Callout> ### getAccounts [#getaccounts] ```lua xPlayer.getAccounts(minimal) ``` | Name | Type | Description | | --- | --- | --- | | `minimal` | `boolean?` | `true` returns `{ [accountName] = money }` instead of the full array. | **Returns:** `table[]` of `{ name, money, label, round, index }`, or a name → money map. ```lua local balances = xPlayer.getAccounts(true) print(balances.bank, balances.money, balances.black_money) ``` ### getAccount [#getaccount] ```lua xPlayer.getAccount(account) ``` | Name | Type | Description | | --- | --- | --- | | `account` | `string` | Account name, for example `"bank"`. | **Returns:** `{ name, money, label, round, index }` or `nil` if the account does not exist. ```lua local bank = xPlayer.getAccount("bank") if bank and bank.money >= 2500 then -- player can afford it end ``` ### getMoney [#getmoney] ```lua xPlayer.getMoney() ``` Returns the balance of the `money` (cash) account, or `0`. **Returns:** `number` ### setMoney [#setmoney] ```lua xPlayer.setMoney(money) ``` | Name | Type | Description | | --- | --- | --- | | `money` | `number` | New cash balance. Asserts that it is a number, then rounds it. | Shortcut for `setAccountMoney("money", money)`. **Returns:** `true` on success. ### addMoney [#addmoney] ```lua xPlayer.addMoney(money, reason) ``` | Name | Type | Description | | --- | --- | --- | | `money` | `number` | Amount of cash to add. Rounded before use. | | `reason` | `string?` | Reason passed to the `esx:addAccountMoney` event, useful for logs. | Shortcut for `addAccountMoney("money", money, reason)`. **Returns:** `true` on success, `false` on overflow. ```lua xPlayer.addMoney(150, "Sold fish") ``` ### removeMoney [#removemoney] ```lua xPlayer.removeMoney(money, reason) ``` | Name | Type | Description | | --- | --- | --- | | `money` | `number` | Amount of cash to remove. Rounded before use. | | `reason` | `string?` | Reason passed to the `esx:removeAccountMoney` event. | Shortcut for `removeAccountMoney("money", money, reason)`. **Returns:** `true` on success, `false` if the player does not have enough cash. ### setAccountMoney [#setaccountmoney] ```lua xPlayer.setAccountMoney(accountName, money, reason) ``` | Name | Type | Description | | --- | --- | --- | | `accountName` | `string` | Account name. | | `money` | `number` | New balance, `0` or higher. Rounded if the account has `round = true`. | | `reason` | `string?` | Defaults to `"unknown"`. | Sets the balance to an exact value. Fires `esx:setAccountMoney` on the server (`source, accountName, money, reason`) and on the client (the updated account table). **Returns:** `true` on success. Throws for negative or invalid amounts and unknown accounts. ```lua xPlayer.setAccountMoney("black_money", 0, "Money laundered") ``` ### addAccountMoney [#addaccountmoney] ```lua xPlayer.addAccountMoney(accountName, money, reason) ``` | Name | Type | Description | | --- | --- | --- | | `accountName` | `string` | Account name. | | `money` | `number` | Amount to add, greater than `0`. | | `reason` | `string?` | Defaults to `"Unknown"`. | Fires `esx:addAccountMoney` on the server (`source, accountName, money, reason`) and syncs the account to the client. **Returns:** `true` on success, `false` if the new balance would overflow. Throws for amounts of `0` or below. ```lua local reward = 1200 if reward > 0 then xPlayer.addAccountMoney("bank", reward, "Delivery job") end ``` ### removeAccountMoney [#removeaccountmoney] ```lua xPlayer.removeAccountMoney(accountName, money, reason) ``` | Name | Type | Description | | --- | --- | --- | | `accountName` | `string` | Account name. | | `money` | `number` | Amount to remove, greater than `0`. Converted with `tonumber`. | | `reason` | `string?` | Defaults to `"Unknown"`. | Balances never go negative. Fires `esx:removeAccountMoney` on the server. **Returns:** `true` on success, `false` if the balance is too low or rounding turned the amount into `0`. Throws for amounts of `0` or below. ```lua local price = 500 if xPlayer.removeAccountMoney("bank", price, "Bought a phone") then xPlayer.addInventoryItem("phone", 1) else xPlayer.showNotification("Not enough money in your bank account", "error") end ``` <Callout type="info" title="ox_inventory"> Accounts listed in the `inventory:accounts` convar (default `["money", "black_money"]`, `black_money` is always included) are stored as ox_inventory items. For those, the account methods call `exports.ox_inventory` `SetItem` / `AddItem` / `RemoveItem`. All three return `false` instead of throwing for invalid input. Amounts below `1` are rejected. </Callout> ## Paycheck [#paycheck] ### togglePaycheck [#togglepaycheck] ```lua xPlayer.togglePaycheck(toggle) ``` | Name | Type | Description | | --- | --- | --- | | `toggle` | `boolean` | `false` stops paychecks for this player until set back to `true`. | Not saved. Every new session starts with paychecks enabled. ```lua xPlayer.togglePaycheck(false) -- e.g. while the player is in jail ``` ### isPaycheckEnabled [#ispaycheckenabled] ```lua xPlayer.isPaycheckEnabled() ``` **Returns:** `boolean` ## Job [#job] ### getJob [#getjob] ```lua xPlayer.getJob() ``` Returns the live job table. | Field | Type | Description | | --- | --- | --- | | `id` | `number?` | Copied from an `id` column in `jobs`. The default schema has none, so this is usually `nil`. | | `name` | `string` | Job name, for example `"police"`. | | `label` | `string` | Display label. | | `type` | `string` | Job type from the `jobs.type` column (default `"civ"`). | | `onDuty` | `boolean` | Duty state. | | `grade` | `number` | Grade number. | | `grade_name` | `string` | Grade name, for example `"boss"`. | | `grade_label` | `string` | Grade label. | | `grade_salary` | `number` | Salary per paycheck for this grade. | | `skin_male` | `table` | Male outfit for the grade. | | `skin_female` | `table` | Female outfit for the grade. | ```lua local job = xPlayer.getJob() if job.name == "police" and job.onDuty then -- allow cuffing end ``` ### setJob [#setjob] ```lua xPlayer.setJob(newJob, grade, onDuty) ``` | Name | Type | Description | | --- | --- | --- | | `newJob` | `string` | Job name. Must exist in `ESX.Jobs`. | | `grade` | `number` or `string` | Grade number. Converted with `tostring`. | | `onDuty` | `boolean?` | Duty state. Defaults to `Config.DefaultJobDuty`. Always `false` for `unemployed`. | If the job or grade does not exist, ESX prints a warning and does nothing. On success it stores the duty state in `metadata.jobDuty`, updates the `job` state bag and fires `esx:setJob` on the server (`source, job, lastJob`) and the client (`job, lastJob`). ```lua if ESX.DoesJobExist("mechanic", 0) then xPlayer.setJob("mechanic", 0) end -- Toggle duty without changing the job local job = xPlayer.getJob() xPlayer.setJob(job.name, job.grade, not job.onDuty) ``` ### refreshJob [#refreshjob] ```lua xPlayer.refreshJob() ``` Reloads label, grade name, salary and outfits from `ESX.Jobs` while keeping the grade and duty state. Fires `esx:jobDataRefreshed`. [`ESX.RefreshJob(jobName)`](/docs/es_extended/server/functions#refreshjob) calls this for every player in that job, so you rarely need it directly. **Returns:** `true`, or `false` if the grade no longer exists. In that case the player is set to `unemployed` grade `0`. ## Inventory [#inventory] These methods describe the default es_extended inventory. Items must exist in the `items` table (or be added with [`ESX.AddItems`](/docs/es_extended/server/functions#additems)). See [adding items](/docs/guides/add-items). ### getInventory [#getinventory] ```lua xPlayer.getInventory(minimal) ``` | Name | Type | Description | | --- | --- | --- | | `minimal` | `boolean?` | `true` returns `{ [itemName] = count }`. | **Returns:** an array of items the player owns (count above `0`), sorted by label. Each item is `{ name, count, label, weight, usable, rare, canRemove }`. ```lua for _, item in ipairs(xPlayer.getInventory()) do print(item.label, item.count) end ``` <Callout type="info" title="ox_inventory"> `getInventory()` returns ox_inventory's slot table. `getInventory(true)` returns an array of `{ name, count, slot, metadata }`. </Callout> ### getInventoryItem [#getinventoryitem] ```lua xPlayer.getInventoryItem(itemName) ``` | Name | Type | Description | | --- | --- | --- | | `itemName` | `string` | Item name. | **Returns:** the item table. For known items the player does not own, a new entry with `count = 0`. `nil` for items that do not exist. ```lua local bread = xPlayer.getInventoryItem("bread") if bread and bread.count >= 2 then -- player has at least 2 bread end ``` <Callout type="info" title="ox_inventory"> Signature becomes `getInventoryItem(name, metadata)` and returns `exports.ox_inventory:GetItem(source, name, metadata)`. </Callout> ### hasItem [#hasitem] ```lua xPlayer.hasItem(itemName) ``` | Name | Type | Description | | --- | --- | --- | | `itemName` | `string` | Item name. | **Returns:** `item, count` if the player has at least one, otherwise `false`. ```lua local item, count = xPlayer.hasItem("lockpick") if item then print(("Player has %d lockpicks"):format(count)) end ``` With ox_inventory the signature is `hasItem(name, metadata)`. ### addInventoryItem [#addinventoryitem] ```lua xPlayer.addInventoryItem(itemName, count) ``` | Name | Type | Description | | --- | --- | --- | | `itemName` | `string` | Item name. | | `count` | `number` | Amount to add. Rounded; values of `0` or below are ignored. | Fires `esx:onAddInventoryItem` on the server (`source, itemName, newCount`) and `esx:addInventoryItem` on the client. **Returns:** `true` on success, `false` for invalid counts or unknown items. ```lua if xPlayer.canCarryItem("water", 3) then xPlayer.addInventoryItem("water", 3) else xPlayer.showNotification("You cannot carry that much", "error") end ``` <Callout type="warn"> The default inventory does not check weight here. Call [`canCarryItem`](#cancarryitem) first. </Callout> With ox_inventory the signature is `addInventoryItem(name, count, metadata, slot)`; `count` defaults to `1`. ### removeInventoryItem [#removeinventoryitem] ```lua xPlayer.removeInventoryItem(itemName, count) ``` | Name | Type | Description | | --- | --- | --- | | `itemName` | `string` | Item name. | | `count` | `number` | Amount to remove. Rounded. Must be greater than `0`. | Fires `esx:onRemoveInventoryItem` on the server (`source, itemName, newCount`). **Returns:** `true` on success, `false` if the player does not have the item or not enough of it. Throws for counts of `0` or below. ```lua if xPlayer.removeInventoryItem("lockpick", 1) then TriggerClientEvent("myscript:client:pickLock", xPlayer.source) end ``` With ox_inventory the signature is `removeInventoryItem(name, count, metadata, slot)`. ### setInventoryItem [#setinventoryitem] ```lua xPlayer.setInventoryItem(itemName, count) ``` | Name | Type | Description | | --- | --- | --- | | `itemName` | `string` | Item name. | | `count` | `number` | The exact count the player should end up with, `0` or higher. | Adds or removes the difference using the methods above. **Returns:** the result of `addInventoryItem` / `removeInventoryItem`, `nil` if the count is already correct, `false` for unknown items or negative counts. ```lua xPlayer.setInventoryItem("bandage", 0) -- remove all bandages ``` With ox_inventory the signature is `setInventoryItem(name, count, metadata)`. ### canCarryItem [#cancarryitem] ```lua xPlayer.canCarryItem(itemName, count) ``` | Name | Type | Description | | --- | --- | --- | | `itemName` | `string` | Item name. | | `count` | `number` | Amount to test. | **Returns:** `true` if `weight + itemWeight * count` stays within `maxWeight`. `false` for unknown items. With ox_inventory the signature is `canCarryItem(name, count, metadata)`. ### canSwapItem [#canswapitem] ```lua xPlayer.canSwapItem(firstItem, firstItemCount, testItem, testItemCount) ``` | Name | Type | Description | | --- | --- | --- | | `firstItem` | `string` | Item the player gives away. | | `firstItemCount` | `number` | Amount given away. | | `testItem` | `string` | Item the player receives. | | `testItemCount` | `number` | Amount received. | **Returns:** `true` if the player owns enough of `firstItem` and the weight after the swap fits. ```lua if xPlayer.canSwapItem("fish", 10, "packaged_fish", 5) then xPlayer.removeInventoryItem("fish", 10) xPlayer.addInventoryItem("packaged_fish", 5) end ``` ### getWeight [#getweight] ```lua xPlayer.getWeight() ``` **Returns:** `number`, the current inventory weight. ### getMaxWeight [#getmaxweight] ```lua xPlayer.getMaxWeight() ``` **Returns:** `number`, the weight limit (starts at `Config.MaxWeight`). ### setMaxWeight [#setmaxweight] ```lua xPlayer.setMaxWeight(newWeight) ``` | Name | Type | Description | | --- | --- | --- | | `newWeight` | `number` | New limit. Must be a finite number, `0` or higher. | Not saved; resets to `Config.MaxWeight` on the next load. Returns `false` for invalid values. With ox_inventory, the value is also forwarded to `SetMaxWeight`. ```lua -- backpack equipped xPlayer.setMaxWeight(xPlayer.getMaxWeight() + 20) ``` ### syncInventory [#syncinventory] ```lua xPlayer.syncInventory(weight, maxWeight, items, money) ``` Only exists with ox_inventory. ox_inventory calls it to copy its weight, items and account balances into the xPlayer. Do not call it yourself. ## Weapons [#weapons] Weapon methods only work with the default inventory. With ox_inventory, weapons are items: these methods do nothing, `getLoadout()` returns `{}`, and `hasWeapon` / `hasWeaponComponent` return `false`. Weapon names are the upper-case names from [`Config.Weapons`](/docs/es_extended/config/weapons), for example `"WEAPON_PISTOL"`. Component names are the short ESX names from that config, for example `"clip_extended"` or `"suppressor"`. ### getLoadout [#getloadout] ```lua xPlayer.getLoadout(minimal) ``` | Name | Type | Description | | --- | --- | --- | | `minimal` | `boolean?` | `true` returns `{ [weaponName] = { ammo, tintIndex?, components? } }`. Default clips are left out. | **Returns:** array of `{ name, label, ammo, components, tintIndex }` or the minimal map. ```lua for _, weapon in ipairs(xPlayer.getLoadout()) do print(weapon.label, weapon.ammo) end ``` ### getWeapon [#getweapon] ```lua xPlayer.getWeapon(weaponName) ``` | Name | Type | Description | | --- | --- | --- | | `weaponName` | `string` | Weapon name. | **Returns:** `loadoutIndex, weapon` (the live loadout entry) or `nil, nil`. ```lua local _, weapon = xPlayer.getWeapon("WEAPON_PISTOL") if weapon then print("Pistol ammo: " .. weapon.ammo) end ``` ### hasWeapon [#hasweapon] ```lua xPlayer.hasWeapon(weaponName) ``` **Returns:** `boolean` ### addWeapon [#addweapon] ```lua xPlayer.addWeapon(weaponName, ammo) ``` | Name | Type | Description | | --- | --- | --- | | `weaponName` | `string` | Weapon name. Must exist in `Config.Weapons`, otherwise the label lookup asserts. | | `ammo` | `number` | Starting ammo. | Gives the weapon to the ped and shows an inventory notification. **Returns:** `true`, or `false` if the player already has the weapon. ```lua if not xPlayer.hasWeapon("WEAPON_STUNGUN") then xPlayer.addWeapon("WEAPON_STUNGUN", 0) end ``` ### removeWeapon [#removeweapon] ```lua xPlayer.removeWeapon(weaponName) ``` Removes the weapon, its components and its ammo from the ped. **Returns:** `true`, or `false` if the player did not have it. ### addWeaponAmmo [#addweaponammo] ```lua xPlayer.addWeaponAmmo(weaponName, ammoCount) ``` | Name | Type | Description | | --- | --- | --- | | `weaponName` | `string` | Weapon name. | | `ammoCount` | `number` | Ammo to add. | **Returns:** `true`, or `false` if the player does not have the weapon. ### removeWeaponAmmo [#removeweaponammo] ```lua xPlayer.removeWeaponAmmo(weaponName, ammoCount) ``` Subtracts ammo. There is no lower bound, so check the current ammo first if it matters. **Returns:** `true`, or `false` if the player does not have the weapon. ### updateWeaponAmmo [#updateweaponammo] ```lua xPlayer.updateWeaponAmmo(weaponName, ammoCount) ``` Sets the stored ammo to an exact value without touching the ped (ESX uses it to sync ammo reported by the client). Throwable weapons (`throwable = true` in the config) are removed when the ammo reaches `0`. **Returns:** `true`, or `false` if the player does not have the weapon. ### addWeaponComponent [#addweaponcomponent] ```lua xPlayer.addWeaponComponent(weaponName, weaponComponent) ``` | Name | Type | Description | | --- | --- | --- | | `weaponName` | `string` | Weapon name. | | `weaponComponent` | `string` | Component name from `Config.Weapons`, for example `"suppressor"`. | **Returns:** `true`, or `false` if the player lacks the weapon, the component does not exist for it, or it is already attached. ```lua xPlayer.addWeaponComponent("WEAPON_PISTOL", "flashlight") ``` ### removeWeaponComponent [#removeweaponcomponent] ```lua xPlayer.removeWeaponComponent(weaponName, weaponComponent) ``` **Returns:** `true`, or `false` if nothing was removed. ### hasWeaponComponent [#hasweaponcomponent] ```lua xPlayer.hasWeaponComponent(weaponName, weaponComponent) ``` **Returns:** `boolean` ### setWeaponTint [#setweapontint] ```lua xPlayer.setWeaponTint(weaponName, weaponTintIndex) ``` | Name | Type | Description | | --- | --- | --- | | `weaponName` | `string` | Weapon name. | | `weaponTintIndex` | `number` | Tint index. Must exist in the weapon's `tints` table. | **Returns:** `true`, or `false` if the player lacks the weapon or the tint is invalid. ```lua xPlayer.setWeaponTint("WEAPON_PISTOL", 2) -- gold ``` ### getWeaponTint [#getweapontint] ```lua xPlayer.getWeaponTint(weaponName) ``` **Returns:** `number`, the tint index, or `0` if the player does not have the weapon. ## Metadata [#metadata] Metadata is a persistent table saved in the `metadata` column of `users`. Changes are synced to `ESX.PlayerData.metadata` on the client. <Callout type="info" title="Keys used by ESX"> ESX writes `health`, `armor`, `lastPlaytime` and `jobDuty` into metadata. Do not overwrite them with unrelated data. </Callout> ### getMeta [#getmeta] ```lua xPlayer.getMeta(index, subIndex) ``` | Name | Type | Description | | --- | --- | --- | | `index` | `string?` | Top-level key. Omit it to get the whole metadata table. | | `subIndex` | `string` or `string[]` | Key (or list of keys) inside a table value. | **Returns:** the value, a `{ [key] = value }` table when `subIndex` is a list, or `nil` if the key does not exist (an error when `Config.EnableDebug` is on). ```lua local stress = xPlayer.getMeta("stress") or 0 local licenses = xPlayer.getMeta("licenses", { "driver", "weapon" }) ``` ### setMeta [#setmeta] ```lua xPlayer.setMeta(index, value, subValue) ``` | Name | Type | Description | | --- | --- | --- | | `index` | `string` | Top-level key. | | `value` | `number`, `string` or `table` | Value to store. When `subValue` is given, this is the key inside the table instead. | | `subValue` | `any` | Optional. Stores `metadata[index][value] = subValue`, creating the table if needed. | ```lua xPlayer.setMeta("stress", 35) xPlayer.setMeta("licenses", "driver", true) -- metadata.licenses.driver = true ``` <Callout type="warn"> Booleans are rejected as a top-level value, and `false` as `subValue` is treated as "no subValue". To store a false flag, use `clearMeta` or a number. </Callout> ### clearMeta [#clearmeta] ```lua xPlayer.clearMeta(index, subValues) ``` | Name | Type | Description | | --- | --- | --- | | `index` | `string` | Top-level key. | | `subValues` | `string` or `string[]` | Optional. Only remove these keys inside the table. | ```lua xPlayer.clearMeta("stress") xPlayer.clearMeta("licenses", { "weapon", "pilot" }) ``` ## Notifications [#notifications] These methods trigger client events, so the notification shows on the player's screen. ### showNotification [#shownotification] ```lua xPlayer.showNotification(msg, notifyType, length, title, position) ``` | Name | Type | Description | | --- | --- | --- | | `msg` | `string` | Message text. | | `notifyType` | `string?` | `"info"` (default), `"success"`, `"error"` or `"warning"` with esx_notify. | | `length` | `number?` | Duration in ms. Default `5000`. | | `title` | `string?` | Optional title. | | `position` | `string?` | esx_notify position, for example `"top-right"`. | ```lua xPlayer.showNotification("Vehicle stored", "success", 3000) ``` ### showAdvancedNotification [#showadvancednotification] ```lua xPlayer.showAdvancedNotification(sender, subject, msg, textureDict, iconType, flash, saveToBrief, hudColorIndex) ``` | Name | Type | Description | | --- | --- | --- | | `sender` | `string` | Title line. | | `subject` | `string` | Subtitle line. | | `msg` | `string` | Message text. | | `textureDict` | `string` | Picture, for example `"CHAR_BANK_MAZE"`. | | `iconType` | `number` | GTA feed icon type. | | `flash` | `boolean` | Flash the notification. | | `saveToBrief` | `boolean?` | Keep it in the pause-menu brief. Default `true`. | | `hudColorIndex` | `number?` | Background color index. | ```lua xPlayer.showAdvancedNotification("Maze Bank", "Transfer", "You received $500", "CHAR_BANK_MAZE", 9, false) ``` ### showHelpNotification [#showhelpnotification] ```lua xPlayer.showHelpNotification(msg, thisFrame, beep, duration) ``` | Name | Type | Description | | --- | --- | --- | | `msg` | `string` | Help text. Supports input tokens like `~INPUT_CONTEXT~`. | | `thisFrame` | `boolean?` | Show for one frame only (not useful from the server). | | `beep` | `boolean?` | Play a sound. Default `true`. | | `duration` | `number?` | Duration in ms. Default `-1`. | ## Internal methods [#internal-methods] `beginAccountOperation`, `endAccountOperation`, `beginAccountShutdown` and `getPendingAccountOperations` exist on every xPlayer. es_extended uses them to finish pending money operations before the final save when a player drops. They are not meant for resource code. ## Common issues [#common-issues] - **`attempt to index a nil value (local 'xPlayer')`**: the player is not loaded yet or has left. Always check the result of `ESX.GetPlayerFromId`. - **Money "disappears" after a restart**: you changed `xPlayer.accounts[i].money` directly. Use the account methods. - **Items over the weight limit**: `addInventoryItem` does not check weight with the default inventory. Use `canCarryItem`. - **Weapon methods do nothing**: you run ox_inventory. Give weapons as items with `exports.ox_inventory:AddItem`. - **Trusting client input**: never pass amounts from a client event straight into `addAccountMoney`. Validate type, range and the player's distance on the server. ## Related [#related] - [Server functions](/docs/es_extended/server/functions) - [PlayerData](/docs/es_extended/playerdata) - [Server events](/docs/es_extended/events/server) - [Money and accounts guide](/docs/guides/money-and-accounts) - [Create a job](/docs/guides/create-a-job) # Shared Functions (/docs/es_extended/shared) These functions are defined in `es_extended/shared` and exist on both the client and the server `ESX` object. They cover config access, weapon data from `Config.Weapons`, input validation and a few utilities. Math, table and timeout helpers have their own pages. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` | | Side | Shared (client and server) | | Source | `shared/main.lua`, `shared/modules/*.lua`, `shared/compat.lua` | Related shared pages: [Math](/docs/es_extended/shared/math), [Table](/docs/es_extended/shared/table), [Timeout](/docs/es_extended/shared/timeout), [Client PlayerData](/docs/es_extended/shared/playerdata). ## Getting the ESX object [#getting-the-esx-object] ```lua ESX = exports['es_extended']:getSharedObject() ``` The `getSharedObject` export returns the `ESX` table. The `@es_extended/imports.lua` shared script does this for you and also keeps `ESX.PlayerData` in sync on the client. <Callout type="warn" title="Deprecated"> The event `esx:getSharedObject` still answers for old scripts, but new code should use the export or the imports file. See [Convert old ESX scripts](/docs/guides/convert-old-esx-scripts). </Callout> ## Config [#config] ### ESX.GetConfig [#esxgetconfig] ```lua ESX.GetConfig(key) ``` | Name | Type | Description | |---|---|---| | `key` | `string?` | Config key. Without it the whole `Config` table is returned. | **Returns:** the value of `Config[key]`, or the full config table. ```lua if ESX.GetConfig('Multichar') then print('Multicharacter is enabled') end local inventory = ESX.GetConfig('CustomInventory') -- false or 'ox' ``` See [Configuration](/docs/es_extended/config) for all keys. ## Weapons [#weapons] Weapon data comes from `Config.Weapons` ([weapon config](/docs/es_extended/config/weapons)). Each entry has `name`, `label`, `ammo` (`label`, `hash`), `tints` and `components` (`name`, `label`, `hash`). ### ESX.GetWeapon [#esxgetweapon] ```lua ESX.GetWeapon(weaponName) ``` | Name | Type | Description | |---|---|---| | `weaponName` | `string` | Weapon name, case-insensitive (`weapon_pistol` works). | **Returns:** `index` in `Config.Weapons` and the weapon `table`. Raises `Invalid weapon name!` for unknown weapons. ```lua local _, weapon = ESX.GetWeapon('WEAPON_PISTOL') print(weapon.label, weapon.ammo.label) ``` ### ESX.GetWeaponFromHash [#esxgetweaponfromhash] ```lua ESX.GetWeaponFromHash(weaponHash) ``` | Name | Type | Description | |---|---|---| | `weaponHash` | `number \| string` | Weapon hash, or a name that is hashed with `joaat`. | **Returns:** weapon `table` or `nil`. Does not raise errors. ```lua local weapon = ESX.GetWeaponFromHash(GetSelectedPedWeapon(PlayerPedId())) if weapon then print('Holding', weapon.label) end ``` ### ESX.GetWeaponList [#esxgetweaponlist] ```lua ESX.GetWeaponList(byHash) ``` | Name | Type | Description | |---|---|---| | `byHash` | `boolean?` | When `true`, return a table keyed by weapon hash. | **Returns:** `Config.Weapons` (array) or the hash-keyed table. ### ESX.GetWeaponLabel [#esxgetweaponlabel] ```lua ESX.GetWeaponLabel(weaponName) ``` **Returns:** `string` label (empty string if the entry has no label). Raises `Invalid weapon name!` for unknown weapons. ```lua print(ESX.GetWeaponLabel('weapon_carbinerifle')) -- Carbine Rifle ``` ### ESX.GetWeaponComponent [#esxgetweaponcomponent] ```lua ESX.GetWeaponComponent(weaponName, weaponComponent) ``` | Name | Type | Description | |---|---|---| | `weaponName` | `string` | Weapon name. | | `weaponComponent` | `string` | Component name from the config, for example `suppressor`. | **Returns:** component table (`name`, `label`, `hash`) or `nil`. Raises for unknown weapons. ```lua local suppressor = ESX.GetWeaponComponent('WEAPON_PISTOL', 'suppressor') if suppressor then GiveWeaponComponentToPed(PlayerPedId(), `WEAPON_PISTOL`, suppressor.hash) end ``` ## Validation [#validation] ### ESX.ValidateType [#esxvalidatetype] ```lua ESX.ValidateType(value, ...) ``` | Name | Type | Description | |---|---|---| | `value` | `any` | Value to check. | | `...` | `string` | Allowed Lua type names, for example `'string', 'number'`. | **Returns:** `true`, or `false` and an error message such as `bad value (string expected, got number)`. With no types given it returns `true`. ```lua title="server/main.lua" RegisterNetEvent('shop:buy', function(item, amount) local ok, err = ESX.ValidateType(amount, 'number') if not ok then return print(('[shop] %s from %s'):format(err, source)) end end) ``` ### ESX.AssertType [#esxasserttype] ```lua ESX.AssertType(value, ...) ``` Same as `ValidateType`, but raises an error instead of returning `false`. **Returns:** `true`. ### ESX.IsFunctionReference [#esxisfunctionreference] ```lua ESX.IsFunctionReference(val) ``` **Returns:** `true` for functions and for tables with a callable `__call` metamethod (function references received from other resources). ```lua local function runCallback(cb, ...) if ESX.IsFunctionReference(cb) then return cb(...) end end ``` ### ESX.IsValidLocaleString [#esxisvalidlocalestring] ```lua ESX.IsValidLocaleString(str, allowDigits) ``` Checks that a string only contains characters allowed for names, for example in character creation. | Name | Type | Description | |---|---|---| | `str` | `string` | Text to check. Must be valid UTF-8. | | `allowDigits` | `boolean?` | Also allow `0-9`. | **Returns:** `boolean` Always allowed: `A-Z`, `a-z`, space, `-` and Latin extended letters (accents). Extra ranges are added for `Config.Locale` values `el` (Greek), `sr` (Cyrillic), `he` (Hebrew), `ar` (Arabic) and `zh-cn` (Chinese), and for every charset enabled in `Config.ValidCharacterSets`. ```lua print(ESX.IsValidLocaleString('Jean-Luc')) -- true print(ESX.IsValidLocaleString('John_Doe')) -- false print(ESX.IsValidLocaleString('Agent 47', true)) -- true ``` ## Utilities [#utilities] ### ESX.GetRandomString [#esxgetrandomstring] ```lua ESX.GetRandomString(length) ``` | Name | Type | Description | |---|---|---| | `length` | `number` | Number of characters. Fractions are rounded up, `0` or less returns `''`. | **Returns:** `string` of random `0-9`, `A-Z` and `a-z` characters. It uses `math.random`, so do not use it for secrets. ```lua local reference = ESX.GetRandomString(8) -- e.g. "aZ3k9QpL" ``` ### ESX.Await [#esxawait] ```lua ESX.Await(cb, errMessage, timeout, interval) ``` Calls `cb` until it returns a non-`nil` value. Must run in a thread. | Name | Type | Description | |---|---|---| | `cb` | `function` | Condition. Return any non-`nil` value to stop waiting. `false` counts as a result. | | `errMessage` | `string?` | Error text on timeout. Default `failed to resolve callback`. | | `timeout` | `number \| false?` | Max wait in ms. Default `5000`. `false` waits forever. | | `interval` | `integer?` | Wait between checks in ms. Default `0`. | **Returns:** the first non-`nil` value of `cb`. Raises an error on timeout. ```lua local ped = ESX.Await(function() local p = PlayerPedId() return DoesEntityExist(p) and p or nil end, 'Player ped did not load', 10000, 100) ``` <Callout type="info"> Return `nil`, not `false`, while you are still waiting. `ESX.Await` stops on `false` because it only checks for `nil`. </Callout> ### ESX.DumpTable [#esxdumptable] ```lua ESX.DumpTable(table, nb) ``` Turns a table into a readable string for debugging. | Name | Type | Description | |---|---|---| | `table` | `any` | Value to dump. Non-tables are returned with `tostring`. | | `nb` | `number?` | Starting indentation level. Default `0`. | **Returns:** `string` ```lua print(ESX.DumpTable(ESX.GetConfig('Accounts'))) ``` ### ESX.Round [#esxround] ```lua ESX.Round(value, numDecimalPlaces) ``` Shortcut for [ESX.Math.Round](/docs/es_extended/shared/math). ## Related [#related] - [Math functions](/docs/es_extended/shared/math) - [Table functions](/docs/es_extended/shared/table) - [Timeout functions](/docs/es_extended/shared/timeout) - [Server functions](/docs/es_extended/server/functions) - [Client functions](/docs/es_extended/client/functions) # Math (/docs/es_extended/shared/math) `ESX.Math` holds a few number and string helpers that ESX uses for money, plates and headings. They work on client and server. In 1.16 they are implemented in [esx_lib](/docs/core/esx_lib) (`xLib.math`) and mapped onto `ESX.Math`. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` (implemented in `esx_lib`) | | Side | Shared | | Mapping | `es_extended/shared/compat.lua` | ## Functions [#functions] ### ESX.Math.Round [#esxmathround] ```lua ESX.Math.Round(value, numDecimalPlaces) ``` Rounds half up (`2.5` becomes `3`). | Name | Type | Description | |---|---|---| | `value` | `number` | Number to round. Must be a number, strings raise an error. | | `numDecimalPlaces` | `number?` | Decimal places. Default `0`. | **Returns:** `number` ```lua print(ESX.Math.Round(5.444)) -- 5 print(ESX.Math.Round(5.456, 1)) -- 5.5 ``` `ESX.Round(value, numDecimalPlaces)` is the same function. ### ESX.Math.GroupDigits [#esxmathgroupdigits] ```lua ESX.Math.GroupDigits(value) ``` Adds a thousands separator. The separator comes from the `locale_digit_grouping_symbol` entry of the active ESX locale. | Name | Type | Description | |---|---|---| | `value` | `number \| string` | Number to format. | **Returns:** `string` ```lua local bank = ESX.GetAccount('bank') ESX.ShowNotification(('Balance: $%s'):format(ESX.Math.GroupDigits(bank.money))) -- Balance: $1,250,000 ``` ### ESX.Math.Trim [#esxmathtrim] ```lua ESX.Math.Trim(value) ``` Removes whitespace at the beginning and end. Spaces in the middle stay. Mostly used for `GetVehicleNumberPlateText`, which pads plates to 8 characters. | Name | Type | Description | |---|---|---| | `value` | `string \| number` | Value to trim. Numbers are converted with `tostring`. | **Returns:** `string` ```lua local plate = GetVehicleNumberPlateText(vehicle) -- " ESX 12 " print(ESX.Math.Trim(plate)) -- "ESX 12" ``` ### ESX.Math.Random [#esxmathrandom] ```lua ESX.Math.Random(minRange, maxRange) ``` | Name | Type | Description | |---|---|---| | `minRange` | `number?` | Lower bound. Default `1`. | | `maxRange` | `number?` | Upper bound. Default `10`. | **Returns:** `number`, a random integer from `math.random(minRange, maxRange)`. ```lua local payout = ESX.Math.Random(150, 300) ``` ### ESX.Math.GetHeadingFromCoords [#esxmathgetheadingfromcoords] ```lua ESX.Math.GetHeadingFromCoords(origin, target) ``` Returns the GTA heading (0-360 degrees) that points from `origin` to `target` on the XY plane. | Name | Type | Description | |---|---|---| | `origin` | `table` | Plain table with `x` and `y`. | | `target` | `table` | Plain table with `x` and `y`. | **Returns:** `number` <Callout type="warn"> Both arguments are type-checked as `table`. Passing a `vector3` raises an error. Convert vectors first, for example `{ x = coords.x, y = coords.y }`. </Callout> ```lua local from = GetEntityCoords(ESX.PlayerData.ped) local to = vector3(441.0, -981.0, 30.7) local heading = ESX.Math.GetHeadingFromCoords({ x = from.x, y = from.y }, { x = to.x, y = to.y }) SetEntityHeading(ESX.PlayerData.ped, heading) ``` <Callout type="info" title="More in esx_lib"> `xLib.math` has more helpers that are not mapped to `ESX.Math`, for example `clamp`, `lerp`, `remap`, `tryNumber`, `toInteger`, `hexToRGBA` and `toHex`. Load `@esx_lib/imports.lua` to use them. </Callout> ## Related [#related] - [Shared functions](/docs/es_extended/shared) - [Table functions](/docs/es_extended/shared/table) - [Money and accounts guide](/docs/guides/money-and-accounts) - [esx_lib](/docs/core/esx_lib) # Client PlayerData (/docs/es_extended/shared/playerdata) `ESX.PlayerData` is the client-side copy of the local character: identifier, job, accounts, inventory, loadout, metadata and a few live values like the ped. The server sends it on `esx:playerLoaded` and pushes updates through secure events. This page explains the fields and how to keep your copy in sync. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` | | Side | Client (server code uses the [xPlayer](/docs/es_extended/server/xplayer) object) | | Kept in sync by | `@es_extended/imports.lua` | For the server-side player model and database columns see [Player data](/docs/es_extended/playerdata). ## Getting PlayerData in your resource [#getting-playerdata-in-your-resource] Load the imports file in your manifest. It fetches the `ESX` object, fills `ESX.PlayerData` on `esx:playerLoaded`, applies every `esx:setPlayerData` update coming from `es_extended`, and clears the data on logout. ```lua title="fxmanifest.lua" shared_scripts { '@esx_lib/imports.lua', '@es_extended/imports.lua', } ``` <Callout type="warn"> Since the `ESX` object comes from an export, your resource gets a **copy** of the table. If you only call `exports['es_extended']:getSharedObject()` once, `ESX.PlayerData` is a snapshot and does not update. Use the imports file, or `ESX.GetPlayerData()`, which always returns the current data from `es_extended`. </Callout> ## Fields [#fields] Values after `esx:playerLoaded`. Some keys are added later by ESX on the client. | Key | Type | Description | |---|---|---| | `identifier` | `string` | Character identifier, for example `char1:abc...` with multicharacter. | | `ssn` | `string` | Social security number of the character. | | `name` | `string` | Character name (`firstName lastName` when esx_identity data exists, else the FiveM name). | | `firstName`, `lastName` | `string` | Character names. | | `dateofbirth`, `sex`, `height` | `string`, `string`, `number` | Identity data when available. | | `group` | `string` | Permission group, for example `user` or `admin`. | | `job` | `table` | `id`, `name`, `label`, `type`, `grade`, `grade_name`, `grade_label`, `grade_salary`, `skin_male`, `skin_female`. | | `accounts` | `table[]` | Each `{ name, money, label, round, index }`. Use [ESX.GetAccount](/docs/es_extended/client/functions). | | `money` | `number` | Cash at load time. Use `ESX.GetAccount('money')` for the current value. | | `inventory` | `table[]` | Built-in inventory items (`name`, `count`, `label`, `weight`, `usable`, `rare`, `canRemove`). | | `weight`, `maxWeight` | `number` | Current and maximum inventory weight. | | `loadout` | `table[]` | Weapons: `name`, `label`, `ammo`, `components`, `tintIndex`. Built-in inventory only. | | `metadata` | `table` | Character metadata (health, armor, jobDuty and custom keys). | | `variables` | `table` | Values set with `xPlayer.set` (replicated). | | `coords` | `vector3` | Live position of the ped, computed on read. | | `ped` | `number` | Current player ped. Updated when the ped changes. | | `dead` | `boolean` | `true` after death, `false` after spawn. | | `weapon` | `number` | Hash of the selected weapon. | | `vehicle` | `number \| false` | Vehicle the player is entering or sitting in. | | `seat` | `number \| false` | Seat index in that vehicle. | With `Config.CustomInventory` (ox_inventory), `inventory`, `weight` and `loadout` are not maintained by ESX. Use the inventory's own exports. ```lua title="client/main.lua" RegisterNetEvent('esx:playerLoaded', function(xPlayer) print(('Hello %s, you work as %s (%s)'):format( xPlayer.firstName, xPlayer.job.label, xPlayer.job.grade_label )) end) ``` ## Reacting to changes [#reacting-to-changes] ### OnPlayerData [#onplayerdata] The imports file defines a global function `OnPlayerData(key, val, last)` that does nothing by default. Override it in your resource to react to every PlayerData update from `es_extended`. | Name | Type | Description | |---|---|---| | `key` | `string` | Changed key, for example `job`, `accounts`, `dead`, `ped`. | | `val` | `any` | New value. | | `last` | `any` | Previous value. | ```lua title="client/main.lua" function OnPlayerData(key, val, last) if key == 'job' and val.name ~= (last and last.name) then RefreshJobBlips(val.name) elseif key == 'dead' and val then CloseAllMyMenus() end end ``` ### esx:setPlayerData [#esxsetplayerdata] `ESX.SetPlayerData` fires the local event `esx:setPlayerData` with `(key, val, last)`. Listen to it if you do not use the imports file. The imports file only accepts this event when it comes from `es_extended`, so other resources cannot fake updates into your copy. ```lua AddEventHandler('esx:setPlayerData', function(key, val) if key == 'accounts' then UpdateHudMoney(val) end end) ``` ### Specific events [#specific-events] For single values you can also listen to the secure net events that change them: `esx:setJob`, `esx:setAccountMoney`, `esx:setGroup`, `esx:setInventory`, `esx:setMaxWeight` and `esx:updatePlayerData`. See [Client events](/docs/es_extended/events/client). ## Common issues [#common-issues] - **`ESX.PlayerData.job` is nil in a thread at resource start.** Wait for `ESX.IsPlayerLoaded()` first. - **Changing `ESX.PlayerData` does nothing on the server.** It is a client cache. Change data on the server through xPlayer. - **Money in `PlayerData.money` is outdated.** Read the `money` account instead. ## Related [#related] - [Player data (server and database)](/docs/es_extended/playerdata) - [Client functions](/docs/es_extended/client/functions) - [Client events](/docs/es_extended/events/client) - [xPlayer](/docs/es_extended/server/xplayer) # Table (/docs/es_extended/shared/table) `ESX.Table` collects small helpers for working with Lua tables: searching, filtering, mapping, copying and joining. They exist on client and server. In 1.16 they are implemented in [esx_lib](/docs/core/esx_lib) (`xLib.table`) and mapped onto `ESX.Table`. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` (implemented in `esx_lib`) | | Side | Shared | | Mapping | `es_extended/shared/compat.lua` | "Array" below means a table with keys `1..n`. Functions marked *array* only look at that part and ignore other keys. ## Size and lookup [#size-and-lookup] ### ESX.Table.SizeOf [#esxtablesizeof] ```lua ESX.Table.SizeOf(t) ``` **Returns:** `number` of keys, counted with `pairs`. Use it for tables with string keys. ```lua print(ESX.Table.SizeOf({ police = 4, ambulance = 2 })) -- 2 ``` ### ESX.Table.Set [#esxtableset] ```lua ESX.Table.Set(t) ``` *Array.* **Returns:** a set `{ [value] = true }` for fast membership checks. ```lua local allowed = ESX.Table.Set({ 'police', 'sheriff', 'ambulance' }) if allowed[ESX.PlayerData.job.name] then OpenEmergencyMenu() end ``` ### ESX.Table.IndexOf [#esxtableindexof] ```lua ESX.Table.IndexOf(t, value) ``` *Array.* **Returns:** first index of `value`, or `-1`. ### ESX.Table.LastIndexOf [#esxtablelastindexof] ```lua ESX.Table.LastIndexOf(t, value) ``` *Array.* **Returns:** last index of `value`, or `-1`. ### ESX.Table.Find [#esxtablefind] ```lua ESX.Table.Find(t, cb) ``` *Array.* **Returns:** the first element for which `cb(element)` is truthy, or `nil`. ```lua local bread = ESX.Table.Find(ESX.PlayerData.inventory, function(item) return item.name == 'bread' end) ``` ### ESX.Table.FindIndex [#esxtablefindindex] ```lua ESX.Table.FindIndex(t, cb) ``` *Array.* **Returns:** index of the first match, or `-1`. ### ESX.Table.TableContains [#esxtabletablecontains] ```lua ESX.Table.TableContains(tab, val) ``` | Name | Type | Description | |---|---|---| | `tab` | `table` | Table to search (all values, not only the array part). | | `val` | `any \| table` | Value, or a table of values. With a table, returns `true` if **any** of them is in `tab`. | **Returns:** `boolean` ```lua local emergencyJobs = { 'police', 'ambulance' } print(ESX.Table.TableContains(emergencyJobs, ESX.PlayerData.job.name)) ``` ## Transform [#transform] ### ESX.Table.Filter [#esxtablefilter] ```lua ESX.Table.Filter(tbl, filter) ``` | Name | Type | Description | |---|---|---| | `tbl` | `table` | Table to filter. | | `filter` | `function` | `function(value, key)`. Return `true` to keep the entry. | **Returns:** a new table with the kept entries **under their original keys**. ```lua local stacks = ESX.Table.Filter(ESX.PlayerData.inventory, function(item) return item.count > 0 end) local list = ESX.Table.ToArray(stacks) -- re-index to 1..n ``` ### ESX.Table.Map [#esxtablemap] ```lua ESX.Table.Map(t, cb) ``` *Array.* **Returns:** a new array with `cb(element, index)` for each element. ```lua local names = ESX.Table.Map(ESX.PlayerData.inventory, function(item) return item.label end) ``` ### ESX.Table.Reverse [#esxtablereverse] ```lua ESX.Table.Reverse(t) ``` *Array.* **Returns:** a new array in reverse order. ### ESX.Table.Clone [#esxtableclone] ```lua ESX.Table.Clone(t) ``` **Returns:** a deep copy, including nested tables and the metatable. Non-table values are returned as they are. ```lua local defaults = { hunger = 100, thirst = 100 } local status = ESX.Table.Clone(defaults) status.hunger = 50 -- defaults.hunger stays 100 ``` ### ESX.Table.Concat [#esxtableconcat] ```lua ESX.Table.Concat(t1, t2) ``` **Returns:** a deep copy of `t1` with the array elements of `t2` appended. Neither input is changed. ### ESX.Table.Join [#esxtablejoin] ```lua ESX.Table.Join(t, sep) ``` *Array.* Joins elements into a string. | Name | Type | Description | |---|---|---| | `t` | `table` | Array of strings or numbers. | | `sep` | `string?` | Separator. Default `,`. | **Returns:** `string` ```lua print(ESX.Table.Join({ 'bread', 'water', 'phone' }, ', ')) -- bread, water, phone ``` ### ESX.Table.ToArray [#esxtabletoarray] ```lua ESX.Table.ToArray(t) ``` **Returns:** a new array with all values of `t` (order follows `pairs` and is not guaranteed). ### ESX.Table.Sort [#esxtablesort] ```lua ESX.Table.Sort(t, order) ``` Returns an **iterator** over `t` sorted by key. It does not sort `t` in place. | Name | Type | Description | |---|---|---| | `t` | `table` | Table to iterate. | | `order` | `function?` | `function(t, keyA, keyB)` returning `true` if `keyA` comes first. Default: sort keys ascending. | **Returns:** iterator `function` yielding `key, value`. ```lua local salaries = { police = 300, ambulance = 250, mechanic = 200 } for job, salary in ESX.Table.Sort(salaries, function(t, a, b) return t[a] > t[b] end) do print(job, salary) end ``` ### ESX.Table.Wipe [#esxtablewipe] ```lua ESX.Table.Wipe(t) ``` Removes every key from `t` in place, keeping the same table reference. <Callout type="warn"> The `ESX.Table` functions run inside `es_extended`. When you call them from another resource, your table is copied on the way in. Functions that return a new value work as expected, but `ESX.Table.Wipe` only clears that copy. Use `table.wipe(t)` or `xLib.table.wipe(t)` in your own resource instead. </Callout> ## Debugging [#debugging] `ESX.DumpTable(t)` prints tables in readable form. It is documented on [Shared functions](/docs/es_extended/shared). ## Related [#related] - [Shared functions](/docs/es_extended/shared) - [Math functions](/docs/es_extended/shared/math) - [Timeout functions](/docs/es_extended/shared/timeout) - [esx_lib](/docs/core/esx_lib) # Timeout (/docs/es_extended/shared/timeout) `ESX.SetTimeout` runs a function once after a delay, like FiveM's `SetTimeout`, but returns an id so you can cancel it with `ESX.ClearTimeout`. Both work on client and server. ## Overview [#overview] | | | |---|---| | Resource | `es_extended` (implemented in `esx_lib` as `xLib.timeout`) | | Side | Shared | ## Functions [#functions] ### ESX.SetTimeout [#esxsettimeout] ```lua ESX.SetTimeout(msec, cb) ``` | Name | Type | Description | |---|---|---| | `msec` | `number` | Delay in milliseconds. | | `cb` | `function` | Function to run. Raises an error if it is not callable. | **Returns:** `number`, the timeout id. ```lua title="server/main.lua" local handcuffTimers = {} RegisterNetEvent('police:cuffed', function(target) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer or xPlayer.job.name ~= 'police' then return end handcuffTimers[target] = ESX.SetTimeout(10 * 60 * 1000, function() handcuffTimers[target] = nil TriggerClientEvent('police:forceUncuff', target) end) end) ``` ### ESX.ClearTimeout [#esxcleartimeout] ```lua ESX.ClearTimeout(id) ``` | Name | Type | Description | |---|---|---| | `id` | `number` | Id returned by `ESX.SetTimeout`. | **Returns:** nothing ```lua RegisterNetEvent('police:uncuffed', function(target) if handcuffTimers[target] then ESX.ClearTimeout(handcuffTimers[target]) handcuffTimers[target] = nil end end) ``` ## Common issues [#common-issues] - **Timers live in es_extended.** `ESX.SetTimeout` runs inside `es_extended`, so restarting `es_extended` drops all pending timeouts. If your own resource restarts first, its callbacks can no longer run. - **Clearing twice is harmless**, but clearing an id that already ran leaves a small entry behind. Set your stored id to `nil` after the callback runs, as in the example. ## Related [#related] - [Shared functions (ESX.Await)](/docs/es_extended/shared) - [Table functions](/docs/es_extended/shared/table) - [Performance guide](/docs/guides/performance) # ESX Legacy FAQ (/docs/faq) Quick answers to the questions people ask most about ESX Legacy. The answers above are short on purpose; the links below lead to the pages with full explanations, examples and edge cases. ## Installing and updating [#installing-and-updating] - Install with txAdmin in about 15 minutes: [Installation](/docs/getting-started/installation) - Install without txAdmin, or understand what the recipe does: [Manual installation](/docs/getting-started/manual-installation) - Every `server.cfg` line explained, including OneSync and the database string: [server.cfg](/docs/getting-started/server-cfg) - Update to a new release without losing data: [Updating](/docs/getting-started/updating) - Fix a specific error message: [Troubleshooting](/docs/getting-started/troubleshooting) ## Running a server [#running-a-server] - Admin groups, ACE permissions and custom groups: [Admin permissions](/docs/guides/admin-permissions) - Built-in commands such as `/giveaccountmoney`, `/setjob` and `/car`: [Commands](/docs/es_extended/commands) - Accounts, starting money and how money is stored: [Money and accounts](/docs/guides/money-and-accounts) - Adding items to the default inventory: [Add items](/docs/guides/add-items) - Switching to ox_inventory: [ox_inventory guide](/docs/guides/ox-inventory) - Creating jobs with grades and salaries: [Create a job](/docs/guides/create-a-job) - Characters and slots: [esx_multicharacter](/docs/core/esx_multicharacter) ## Writing scripts [#writing-scripts] - A complete first resource with a command and a callback: [Your first ESX resource](/docs/getting-started/first-resource) - Every method of the player object: [xPlayer](/docs/es_extended/server/xplayer) - Client and server callbacks: [Callbacks](/docs/guides/callbacks) - Database queries with oxmysql: [oxmysql guide](/docs/guides/oxmysql) - Updating scripts written for old ESX: [Convert old ESX scripts](/docs/guides/convert-old-esx-scripts) ## Choosing a framework [#choosing-a-framework] ESX and QBCore solve the same problem in different ways. Our [ESX vs QBCore](/docs/guides/esx-vs-qbcore) comparison covers the player object, inventory, ecosystem and how hard it is to move scripts between the two. ## About this site [#about-this-site] esx-fivem.com is an independent community documentation and is not affiliated with the ESX team. For official announcements and support, use the [official documentation](https://docs.esx-framework.org/en), the [ESX GitHub](https://github.com/esx-framework) and the official Discord. Read more in the [introduction](/docs). # Lua Annotations for ESX (/docs/getting-started/annotations) Annotations are special comments that tell the Lua language server (LuaLS, the "Lua" extension by sumneko) what types your functions take and return. Your editor then shows autocomplete, parameter hints and warnings, for example when you pass a string where a number is expected or forget to check a value that can be `nil`. They cost nothing at runtime: FiveM treats them as ordinary comments. ESX Legacy itself is annotated, so with the right setup you get full autocomplete for `xPlayer`, accounts, jobs and items in your own scripts. ## Editor setup [#editor-setup] 1. Install [Lua by sumneko](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) and [CfxLua IntelliSense](https://marketplace.visualstudio.com/items?itemName=overextended.cfxlua-vscode). 2. Point the language server at your copy of `es_extended` so it reads the ESX types: ```json title=".vscode/settings.json" { "Lua.runtime.version": "Lua 5.4", "Lua.workspace.library": [ "C:/FXServer/esx-server/resources/[core]/es_extended" ], "Lua.diagnostics.globals": ["ESX", "Config"] } ``` Use the real path to your server's `es_extended` folder. On Linux or macOS use a normal Unix path. ## Annotation basics [#annotation-basics] An annotation is a comment line that starts with `---` followed by a tag such as `@param`. It applies to the code directly below it. | Tag | Purpose | Example | | --- | --- | --- | | `@param` | Type of a function parameter. Add `?` for optional. | `---@param amount number` | | `@return` | Type of a return value, optionally with a name. | `---@return boolean success` | | `@class` | Declares a table type. Can extend another class. | `---@class ShopItem` | | `@field` | A field of a class. | `---@field price number` | | `@type` | Type of a variable. | `---@type xPlayer?` | | `@alias` | A name for a type or a set of literal values. | `---@alias Account 'money'\|'bank'` | | `@cast` | Tell the server a variable has a narrower type from here on. | `---@cast xPlayer xPlayer` | | `@deprecated` | Marks a function as deprecated; callers get a strikethrough. | `---@deprecated` | | `@diagnostic` | Disable a warning for a line or file. | `---@diagnostic disable-next-line: undefined-global` | Common types: `string`, `number`, `integer`, `boolean`, `table`, `function`, `nil`, `any`, arrays like `string[]`, maps like `table<string, number>`, unions like `string|number`, and FiveM's `vector3`. ## Documenting a function [#documenting-a-function] ```lua title="server/utils.lua" --- Pays a player from a society account. --- Returns false when the player is not loaded or the amount is invalid. ---@param playerId number Server ID of the player ---@param amount integer Amount in dollars, must be positive ---@param reason? string Shown in logs, defaults to "Payment" ---@return boolean success local function payPlayer(playerId, amount, reason) if amount <= 0 then return false end local xPlayer = ESX.GetPlayerFromId(playerId) if not xPlayer then return false end xPlayer.addAccountMoney('bank', amount, reason or 'Payment') return true end ``` Now hovering `payPlayer` shows the description, and calling `payPlayer('1', 500)` gets a warning because `'1'` is a string. ## Using the ESX types [#using-the-esx-types] `es_extended` defines these classes in `server/classes/player.lua`: | Class | Describes | | --- | --- | | `xPlayer` | The full server-side player object (extends `StaticPlayer`) | | `StaticPlayer` | The method interface of a player | | `ESXAccount` | An account: `name`, `money`, `label`, `round`, `index` | | `ESXJob` | A job: `name`, `label`, `grade`, `grade_name`, `grade_label`, `grade_salary`, `onDuty`, ... | | `ESXItem`, `ESXInventoryItem` | An item and an item with `count` | | `ESXInventoryWeapon` | A weapon in the loadout with `ammo`, `components`, `tintIndex` | Use them in your own annotations: ```lua title="server/main.lua" ---@param xPlayer xPlayer ---@return boolean local function isOnDutyPolice(xPlayer) local job = xPlayer.job ---@type ESXJob return job.name == 'police' and job.onDuty == true end RegisterNetEvent('my_police:action', function() local xPlayer = ESX.GetPlayerFromId(source) ---@type xPlayer? if not xPlayer then return end if isOnDutyPolice(xPlayer) then -- autocomplete works here: xPlayer.getAccount, xPlayer.showNotification, ... end end) ``` Marking the variable as `xPlayer?` (nullable) makes the language server warn you if you use it before the `nil` check, which is exactly the bug that causes most `attempt to index a nil value` errors. ## Declaring your own classes [#declaring-your-own-classes] Describe the shape of your config or data tables once and get checks everywhere you use them. ```lua title="config.lua" ---@class ShopItem ---@field name string Item name from the items table ---@field label string ---@field price integer ---@class ShopConfig ---@field coords vector3 ---@field items ShopItem[] ---@type ShopConfig Config = { coords = vector3(25.7, -1347.3, 29.5), items = { { name = 'water', label = 'Water', price = 25 }, { name = 'bread', label = 'Bread', price = 15 }, }, } ``` A typo like `prcie = 15` now shows a warning instead of a silent `nil` at runtime. ### Aliases for fixed values [#aliases-for-fixed-values] ```lua ---@alias NotifyType 'info'|'success'|'error' ---@param message string ---@param notifyType? NotifyType local function notify(message, notifyType) ESX.ShowNotification(message, notifyType or 'info') end ``` The editor now suggests only the three valid notification types. ## Tips [#tips] - Annotate public functions and config tables first. That is where most type mistakes happen. - Keep descriptions short. One line about *what* and anything surprising about *when* it returns `nil`. - Do not disable diagnostics for a whole workspace to silence one warning. Use `---@diagnostic disable-next-line` where needed. - The full annotation reference is in the [LuaLS wiki](https://luals.github.io/wiki/annotations/). ## Related [#related] - [Coding practices](/docs/getting-started/coding-practices) - [Development tools](/docs/getting-started/tools) - [xPlayer reference](/docs/es_extended/server/xplayer) # Coding Practices for ESX (/docs/getting-started/coding-practices) These are the conventions and techniques that keep ESX scripts readable, fast and hard to exploit. They apply to any FiveM Lua resource, with examples that use the ESX API. If you are new to ESX, build [your first resource](/docs/getting-started/first-resource) first and come back here. ## Style [#style] ### Naming [#naming] | Kind | Convention | Example | | --- | --- | --- | | Local variables and functions | `camelCase` | `local playerCount`, `local function getClosestShop()` | | Globals (avoid them) | `PascalCase` | `Config`, `MyResource` | | Constants | `UPPER_SNAKE_CASE` | `local MAX_DISTANCE <const> = 3.0` | | Events | `resource:action` | `my_shop:buyItem` | Prefix every event and callback with your resource name. Two scripts that both register `buyItem` will fight each other; `my_shop:buyItem` will not. ### Formatting [#formatting] Pick one indentation width (the ESX core uses 4 spaces) and stay consistent. Use a formatter such as [StyLua](https://github.com/JohnnyMorganz/StyLua) so you never argue about it again. ### Comments and annotations [#comments-and-annotations] Comment *why* something is done, not *what* the next line does. For functions other people will call, add LuaLS annotations so editors can show types and catch mistakes. See [Annotations](/docs/getting-started/annotations). ## Locals over globals [#locals-over-globals] ```lua MyCounter = 0 -- global: visible to every file in the resource local myCounter = 0 -- local: visible only in this file or block ``` Use `local` for everything unless you deliberately share a value between files of the same resource. Locals are faster to access, cannot be overwritten by another file by accident and are freed when they go out of scope. Functions are values too, so `local function` is the default. A local function must be defined **before** the code that calls it: ```lua -- Wrong: startThread is nil at the time it is called startThread() local function startThread() end -- Right local function startThread() end startThread() ``` ## Don't repeat yourself [#dont-repeat-yourself] If you copy the same five lines into three event handlers, move them into a function. In ESX scripts the usual candidates are "get the player and check the job" blocks: ```lua title="server/main.lua" ---@return table? xPlayer local function getPoliceOfficer(source) local xPlayer = ESX.GetPlayerFromId(source) if xPlayer and xPlayer.job.name == 'police' then return xPlayer end end RegisterNetEvent('my_police:cuff', function(targetId) local officer = getPoliceOfficer(source) if not officer then return end -- ... end) ``` ## Loops and threads [#loops-and-threads] ### Always wait [#always-wait] A `while` loop without `Wait` never yields. On the client the game freezes, on the server every resource stops. Every loop that repeats must call `Wait`. ### Wait as long as you can [#wait-as-long-as-you-can] `Wait(0)` runs every frame. Only code that must react every frame needs it: drawing markers or text, or reading controls with `IsControlJustPressed`. Everything else can run much less often. A common pattern is a dynamic sleep that is long when the player is far away and short when close: ```lua title="client/main.lua" local SHOP_COORDS <const> = vector3(25.7, -1347.3, 29.5) CreateThread(function() while true do local sleep = 1000 local distance = #(GetEntityCoords(PlayerPedId()) - SHOP_COORDS) if distance < 10.0 then sleep = 0 DrawMarker(1, SHOP_COORDS.x, SHOP_COORDS.y, SHOP_COORDS.z - 1.0, 0, 0, 0, 0, 0, 0, 1.0, 1.0, 0.5, 50, 150, 255, 150, false, false, 2, false, nil, nil, false) if distance < 1.5 and IsControlJustPressed(0, 38) then -- E TriggerEvent('my_shop:open') end end Wait(sleep) end end) ``` ESX also ships a helper for this pattern, documented under [Point](/docs/es_extended/client/imports/point). ### React to events instead of polling [#react-to-events-instead-of-polling] The classic mistake is a thread that checks the job every frame forever: ```lua title="client/main.lua" -- Wasteful: runs every frame for every player, even non-police CreateThread(function() while true do Wait(0) if ESX.PlayerData.job and ESX.PlayerData.job.name == 'police' then if IsControlJustPressed(0, 38) then -- ... end end end end) ``` Only run the loop while the player actually has the job, and restart it when the job changes: ```lua title="client/main.lua" local policeThreadRunning = false local function isPolice() return ESX.PlayerData.job and ESX.PlayerData.job.name == 'police' end local function startPoliceThread() if policeThreadRunning then return end policeThreadRunning = true CreateThread(function() while isPolice() do if IsControlJustPressed(0, 38) then -- ... end Wait(0) end policeThreadRunning = false end) end -- Job changed while playing RegisterNetEvent('esx:setJob', function(job, lastJob) if job.name == 'police' then startPoliceThread() end end) -- Character loaded (or resource restarted while already in game) RegisterNetEvent('esx:playerLoaded', function() if isPolice() then startPoliceThread() end end) if ESX.PlayerLoaded and isPolice() then startPoliceThread() end ``` `esx:setJob` reaches the client with the new job and the previous job. The `imports.lua` file keeps `ESX.PlayerData.job` in sync, so the loop condition sees the change on its own. For key presses, consider `RegisterKeyMapping` with a command instead of a loop at all. It costs nothing while idle and players can rebind the key. ## Cache expensive values [#cache-expensive-values] Store values that do not change often instead of asking the game again and again. ```lua title="client/main.lua" local playerId = PlayerId() -- never changes during a session local serverId = GetPlayerServerId(playerId) local ped = PlayerPedId() -- The ped changes on respawn or model change; ESX tells you when AddEventHandler('esx:playerPedChanged', function(newPed) ped = newPed end) ``` With `@es_extended/imports.lua` loaded, `ESX.PlayerData.ped` is also kept current, and `ESX.PlayerData.coords` returns the live position of that ped. Cache only what you use and clear entries you no longer need. A table that grows by one entry per player who ever joined is a memory leak. ## Use current natives and Lua idioms [#use-current-natives-and-lua-idioms] | Outdated | Use instead | Why | | --- | --- | --- | | `GetPlayerPed(-1)` | `PlayerPedId()` | Direct, no lookup by player index | | `GetDistanceBetweenCoords(x1, y1, z1, x2, y2, z2, true)` | `#(vec1 - vec2)` | Vector math runs in Lua without a native call | | `GetHashKey('adder')` | `` `adder` `` or `joaat('adder')` | Backtick strings are hashed at compile time in CfxLua | | `table.insert(t, v)` | `t[#t + 1] = v` | Avoids a function call in hot loops | | `Citizen.CreateThread`, `Citizen.Wait` | `CreateThread`, `Wait` | Shorter aliases, same behavior | | `TriggerEvent('esx:getSharedObject', cb)` | `@es_extended/imports.lua` | The event-based getter is legacy | <Callout type="warn" title="Do not replace table.remove with nil"> Some guides suggest `t[i] = nil` instead of `table.remove(t, i)`. For arrays that is a bug: it leaves a hole, after which `#t` and `ipairs` stop at the wrong place. Use `table.remove` for arrays. Setting `nil` is only correct for keyed tables like `players[source] = nil`. </Callout> ## Lua 5.4 [#lua-54] Add `lua54 'yes'` to your `fxmanifest.lua`. All ESX core resources use it. Useful features: - **`<const>`** marks a local as constant. Assigning to it is a compile error. ```lua local MAX_PLAYERS <const> = 64 ``` - **`<close>`** runs the value's `__close` metamethod when it goes out of scope, useful for cleanup. - **Integer division** `//` and native **bitwise operators** `&`, `|`, `~`, `<<`, `>>`. - **`goto continue`** to skip to the next loop iteration. - A faster VM and a generational garbage collector. CfxLua also adds extras on top of Lua 5.4, such as compound assignment (`count += 1`) and backtick hashing. They work in FiveM but not in a plain Lua interpreter. ## Server security [#server-security] Performance problems slow a server down; security problems empty its economy. The rules: 1. **Every net event is public.** Any client can trigger any `RegisterNetEvent` with any data. Validate types, ranges and permissions on the server. 2. **The server decides values.** Never accept prices, amounts of money or item names chosen by the client when the server can look them up. 3. **Check context.** Is the player near the shop (`xPlayer.getCoords(true)`)? Do they have the job? Is the target player close? 4. **Rate-limit expensive actions** and log suspicious ones. ```lua title="server/main.lua" local SHOP_COORDS <const> = vector3(25.7, -1347.3, 29.5) local PRICES <const> = { water = 25, bread = 15 } RegisterNetEvent('my_shop:buy', function(itemName) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return end local price = PRICES[itemName] if not price then return end -- unknown item: ignore if #(xPlayer.getCoords(true) - SHOP_COORDS) > 5.0 then return end if xPlayer.canCarryItem(itemName, 1) and xPlayer.removeAccountMoney('money', price, 'Shop purchase') then xPlayer.addInventoryItem(itemName, 1) end end) ``` ## Related [#related] - [Annotations](/docs/getting-started/annotations) - [Performance guide](/docs/guides/performance) - [Callbacks](/docs/guides/callbacks) - [Client functions](/docs/es_extended/client/functions) # Your First ESX Resource (/docs/getting-started/first-resource) This tutorial builds a small but complete ESX resource from scratch: a `/balance` command that shows a player's money, and a `/buywater` command that charges the player on the server and gives them an item. Along the way you learn the pieces almost every ESX script uses: the manifest, the `ESX` object, `xPlayer`, commands, notifications and server callbacks. You need a running ESX Legacy server. If you do not have one yet, follow [Installation](/docs/getting-started/installation) first. ## What we build [#what-we-build] | Feature | Side | ESX API used | | --- | --- | --- | | `/balance` shows cash and bank | server | `ESX.RegisterCommand`, `xPlayer.getMoney`, `xPlayer.getAccount`, `xPlayer.showNotification` | | `/buywater` buys a water bottle | client + server | `ESX.TriggerServerCallback`, `ESX.RegisterServerCallback`, `xPlayer.removeAccountMoney`, `xPlayer.addInventoryItem` | | Welcome message on spawn | client | `esx:playerLoaded`, `ESX.ShowNotification` | ## Set up your editor [#set-up-your-editor] Any editor works, but Visual Studio Code with these extensions catches most mistakes before you restart the server: - [Lua](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) by sumneko: the Lua language server (autocomplete, diagnostics). - [CfxLua IntelliSense](https://marketplace.visualstudio.com/items?itemName=overextended.cfxlua-vscode) by overextended: FiveM natives and Lua 5.4 syntax used by FiveM. - [ESX VSCode](https://marketplace.visualstudio.com/items?itemName=Knoblauchbrot.esx-vscode): snippets and completion for ESX functions. If the Lua extension reports globals like `ESX` or `CreateThread` as undefined, add this to your VS Code settings: ```json title=".vscode/settings.json" { "Lua.diagnostics.disable": ["undefined-global"], "Lua.diagnostics.globals": ["ESX", "Config"] } ``` ## Folder structure [#folder-structure] Create a folder `my_first_script` inside your `resources` folder (or a category folder such as `resources/[local]`). <Files> <Folder name="my_first_script" defaultOpen> <File name="fxmanifest.lua" /> <File name="config.lua" /> <Folder name="client" defaultOpen> <File name="main.lua" /> </Folder> <Folder name="server" defaultOpen> <File name="main.lua" /> </Folder> </Folder> </Files> ## Build it [#build-it] <Steps> <Step> ### Write the manifest [#write-the-manifest] `fxmanifest.lua` tells FiveM which files to load and where. ```lua title="fxmanifest.lua" fx_version 'cerulean' game 'gta5' lua54 'yes' name 'my_first_script' author 'Your Name' version '1.0.0' shared_scripts { '@es_extended/imports.lua', -- defines the global ESX on client and server 'config.lua', } client_scripts { 'client/main.lua', } server_scripts { 'server/main.lua', } dependency 'es_extended' ``` - `@es_extended/imports.lua` runs `ESX = exports['es_extended']:getSharedObject()` inside your resource. On the client it also keeps `ESX.PlayerData` up to date and sets `ESX.PlayerLoaded`. - `lua54 'yes'` enables Lua 5.4. All ESX core resources use it. - `dependency 'es_extended'` makes FiveM refuse to start your script without ESX, instead of failing later with nil errors. - Load `imports.lua` **before** your own files, otherwise `ESX` is nil while they run. </Step> <Step> ### Add a config file [#add-a-config-file] ```lua title="config.lua" Config = {} Config.WaterPrice = 25 -- price in cash Config.WaterItem = 'water' -- item name from the items table ``` The config is shared, so the client sees it too. That is fine for prices, but never put webhooks, API keys or other secrets in a shared file. Put them in a server-only file instead. </Step> <Step> ### Write the server side [#write-the-server-side] ```lua title="server/main.lua" -- /balance: show cash and bank to the player who typed it ESX.RegisterCommand('balance', 'user', function(xPlayer, args, showError) if not xPlayer then return print('This command can only be used in game.') end local cash = xPlayer.getMoney() local bank = xPlayer.getAccount('bank') xPlayer.showNotification(('Cash: $%s | Bank: $%s'):format( ESX.Math.GroupDigits(cash), ESX.Math.GroupDigits(bank and bank.money or 0) ), 'info') end, false, { help = 'Show your cash and bank balance' }) -- Callback used by /buywater. The client asks, the server decides. ESX.RegisterServerCallback('my_first_script:buyWater', function(source, cb) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return cb(false, 'Player not loaded') end if not xPlayer.canCarryItem(Config.WaterItem, 1) then return cb(false, 'You cannot carry more') end -- removeAccountMoney returns false if the player cannot afford it if not xPlayer.removeAccountMoney('money', Config.WaterPrice, 'Bought water') then return cb(false, 'Not enough cash') end xPlayer.addInventoryItem(Config.WaterItem, 1) cb(true) end) ``` Things to notice: - `ESX.RegisterCommand(name, group, cb, allowConsole, suggestion)` passes the `xPlayer` of the caller to your function. Because `allowConsole` is `false`, the console cannot run it, but the `xPlayer` check is still a good habit: from the console `xPlayer` is `false`. - The second argument is the group that may use the command. ESX commands are always restricted, so with `'admin'` normal players would get "access denied". Use `'user'` for everyone or a table like `{ 'admin', 'owner' }`. - The callback receives `source` first, then the function `cb` to answer with, then any extra arguments the client sent. - All money logic happens on the server. The client only asks; it never says how much to pay. </Step> <Step> ### Write the client side [#write-the-client-side] ```lua title="client/main.lua" -- Runs once when the player's character has loaded RegisterNetEvent('esx:playerLoaded', function(xPlayer) ESX.ShowNotification(('Welcome back, %s!'):format(xPlayer.firstName or GetPlayerName(PlayerId())), 'success') end) -- /buywater: ask the server to buy a water bottle RegisterCommand('buywater', function() if not ESX.IsPlayerLoaded() then return end ESX.TriggerServerCallback('my_first_script:buyWater', function(success, reason) if success then ESX.ShowNotification(('You bought water for $%d'):format(Config.WaterPrice), 'success') else ESX.ShowNotification(reason or 'Purchase failed', 'error') end end) end, false) TriggerEvent('chat:addSuggestion', '/buywater', ('Buy a bottle of water for $%d'):format(Config.WaterPrice)) ``` `ESX.ShowNotification(message, type, length, title, position)` uses `esx_notify`. Valid types are `info`, `success` and `error`. If you prefer code without nested functions, ESX also has an awaitable version that must run inside a thread or command handler: ```lua title="client/main.lua" RegisterCommand('buywater2', function() local success, reason = ESX.AwaitServerCallback('my_first_script:buyWater') ESX.ShowNotification(success and 'Bought water' or reason, success and 'success' or 'error') end, false) ``` </Step> <Step> ### Start and test [#start-and-test] Add the resource to `server.cfg` **below** the ESX lines: ```bash title="server.cfg" ensure es_extended ensure [core] ensure [esx_addons] ensure my_first_script ``` Then start it without restarting the server: type `ensure my_first_script` in the server console or txAdmin live console. In game, type `/balance`, then `/buywater` and open your inventory (F2 by default). After every change, run `ensure my_first_script` again (or `restart my_first_script`). </Step> </Steps> ## Getting the player on the server [#getting-the-player-on-the-server] Most server code starts by turning a `source` into an `xPlayer`: ```lua title="server/main.lua" RegisterNetEvent('my_first_script:doSomething', function() local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return end -- not loaded yet or already dropped print(xPlayer.getIdentifier(), xPlayer.getName(), xPlayer.job.name, xPlayer.getGroup()) end) ``` `ESX.GetPlayerFromId` returns `nil` until the player has picked a character, so always check the result. The full list of methods is in the [xPlayer reference](/docs/es_extended/server/xplayer). ## Security basics for your first script [#security-basics-for-your-first-script] - **Never trust the client.** A cheater can trigger any `RegisterNetEvent` with any arguments. Prices, amounts, item names and target IDs must be decided or validated on the server. - **Check distance and job** on the server before rewarding something, for example with `xPlayer.getCoords(true)` and `xPlayer.job.name`. - **Prefer callbacks over "give me X" events.** A server event named `my_script:giveMoney` that takes an amount is the classic exploit. ## Common mistakes [#common-mistakes] | Symptom | Cause | | --- | --- | | `attempt to index a nil value (global 'ESX')` | `@es_extended/imports.lua` missing in the manifest, or listed after your files, or your resource starts before `es_extended`. | | `attempt to call a nil value (field 'RegisterCommand')` on the client | `ESX.RegisterCommand` is server-only. Use the native `RegisterCommand` on the client. | | Command says "access denied" | Wrong group in `ESX.RegisterCommand`, or the `resource.es_extended` ACE lines are missing from `server.cfg`. | | Callback never answers | The name differs between client and server, or the server function returned without calling `cb`. | | `Item "water" was used but does not exist` | The item is not in the `items` table. See [Add items](/docs/guides/add-items). | ## Next steps [#next-steps] - Learn the conventions that keep scripts fast in [Coding practices](/docs/getting-started/coding-practices). - Read how callbacks work in detail in [Callbacks](/docs/guides/callbacks). - Build a full job with grades and salaries in [Create a job](/docs/guides/create-a-job). ## Related [#related] - [xPlayer reference](/docs/es_extended/server/xplayer) - [Server functions](/docs/es_extended/server/functions) - [Client functions](/docs/es_extended/client/functions) - [Commands](/docs/es_extended/commands) # Install ESX Legacy with txAdmin (/docs/getting-started/installation) The fastest and most reliable way to install ESX Legacy is the official **txAdmin recipe**. txAdmin ships with every FiveM server build. The recipe downloads ESX and its dependencies, imports the SQL schema into your database and writes a ready-to-use `server.cfg`. A fresh install takes about 10 to 15 minutes. If you cannot or do not want to use txAdmin, follow [Manual installation](/docs/getting-started/manual-installation) instead. ## Before you begin [#before-you-begin] | Requirement | Notes | | --- | --- | | cfx.re account | Needed to log in to txAdmin and to create a server license key. Sign up at [cfx.re](https://cfx.re). | | MariaDB | ESX needs a MySQL-compatible database. Install [MariaDB](https://mariadb.org/download/) and remember the root password you set. | | Windows or Linux machine | This guide shows Windows. On Linux the txAdmin part is identical. | | Disk space | Plan for several GB: server build, resources, database and backups. | | Open port | Players connect on port `30120` (TCP and UDP) by default. | <Callout type="warn" title="Do not use XAMPP"> XAMPP bundles an old MySQL/MariaDB with defaults meant for local web development. It is a common source of crashes and lost data on FiveM servers. Install MariaDB on its own. </Callout> ## What the recipe installs [#what-the-recipe-installs] The ESX Legacy recipe (`recipe.yaml` in [esx-recipes](https://github.com/esx-framework/esx-recipes)) runs these tasks in order: 1. Downloads the default cfx resources into `resources/[cfx-default]` and removes their `chat` resource (the server uses the built-in system chat instead). 2. Downloads `server.cfg` and the server icon `esxLogo.png` from the recipe repository. 3. Connects to your database and imports `[SQL]/legacy.sql` from `esx_core`. 4. Downloads `esx_core` into `resources/[core]` and the official addons into `resources/[esx_addons]`. 5. Downloads standalone resources into `resources/[standalone]`: `oxmysql`, `ox_lib`, `pma-voice`, `bob74_ipl`, `sd-phone` and `sd-phone-props`. 6. Enables OneSync (the recipe sets `$onesync: on`). The result is a complete roleplay server with jobs, shops, banking, housing, vehicles and a phone. ## Installation [#installation] <Steps> <Step> ### Download the server build [#download-the-server-build] Download the latest FiveM server build ("artifacts") for Windows. You can use the official build list at [runtime.fivem.net](https://runtime.fivem.net/artifacts/fivem/build_server_windows/master/) or the community mirror [artifacts.jgscripts.com](https://artifacts.jgscripts.com), which shows known-broken builds. ![Server artifacts download page](/images/tutorial/artifactspreview.png) On artifacts.jgscripts.com, click **Windows** to download the recommended build. ![Windows download button](/images/tutorial/server7z.png) Extract the archive to a permanent folder, for example `C:\FXServer\server`. ![Extracting the server files](/images/tutorial/extract.png) <Callout type="warn"> Pick the final location now. txAdmin stores absolute paths, so moving the folder later means updating them by hand. Avoid OneDrive or other synced folders. </Callout> </Step> <Step> ### Start FXServer and link your account [#start-fxserver-and-link-your-account] Open the extracted folder and run `FXServer.exe`. If your antivirus flags it, add an exception for the server folder. ![FXServer.exe in the server folder](/images/tutorial/fxserver.png) A browser tab opens with the txAdmin setup page. If it does not, the console prints the URL (usually `http://localhost:40120`) and a PIN. Click **Link Account** and log in with your cfx.re account. ![Link your cfx.re account](/images/tutorial/linkacc.png) Create a backup password. You need it if the cfx.re login is ever unavailable. ![Create a backup password](/images/tutorial/backuppass.png) </Step> <Step> ### Name the server and pick the ESX recipe [#name-the-server-and-pick-the-esx-recipe] Enter a server name. It shows up in the FiveM server list and in `sv_hostname`. ![Server name](/images/tutorial/esxservername.png) As deployment type choose **Popular Recipes**, then select **ESX Legacy**. ![Popular recipes](/images/tutorial/recipes.png) ![ESX Legacy recipe](/images/tutorial/esx-legacy.png) </Step> <Step> ### Choose the data folder [#choose-the-data-folder] txAdmin asks where to put the server data (`resources`, `server.cfg`, cache). Change the default to a short dedicated path such as `C:\FXServer\esx-server`. ![Change the server data path](/images/tutorial/change-path.png) Create the folder in Explorer first if txAdmin complains that it does not exist, then save the path. ![Save the server data path](/images/tutorial/save-path.png) <Callout type="info"> Keep the server build (`FXServer.exe`) and the server data in separate folders. Then you can replace the build during an update without touching your resources. </Callout> </Step> <Step> ### Open the recipe deployer and enter the database [#open-the-recipe-deployer-and-enter-the-database] Continue to the **Recipe Deployer**. ![Go to the recipe deployer](/images/tutorial/gotorecipedeployer.png) txAdmin asks for your database connection. Make sure the MariaDB service is running, then enter: | Field | Typical value | | --- | --- | | Host | `localhost` | | Port | `3306` | | Username | `root` (or a dedicated user, see below) | | Password | the password you chose when installing MariaDB | | Database name | for example `es_extended` or `esx_legacy` | txAdmin builds the `mysql_connection_string` from these values and writes it into `server.cfg`. </Step> <Step> ### Add your license key [#add-your-license-key] On the **Review Recipe** page, paste a server license key. ![Review recipe page](/images/tutorial/reviewrecipe.png) Create a key at [portal.cfx.re](https://portal.cfx.re/servers/registration-keys) with the same cfx.re account. One key per server. <Callout type="error" title="Keep the key private"> The license key identifies your server. Never post it in screenshots, support tickets or public repositories. If it leaks, revoke it on the portal and create a new one. </Callout> </Step> <Step> ### Run the recipe [#run-the-recipe] Click **Run Recipe**. The download of the addons can take several minutes. When it finishes, txAdmin shows the generated `server.cfg`. Check the values (max players, locale, tags), then click **Next** and start the server. ![Setup complete](/images/tutorial/done.png) </Step> </Steps> ## After the installation [#after-the-installation] 1. **Watch the first start.** The console should show `oxmysql` connecting, `es_extended` starting and the ESX migrations running. A message like `Server restart required!` after migrations is expected on the first start; restart the server once. 2. **Join your server.** In FiveM press F8 and type `connect localhost:30120`. You should see the ESX loading screen, then character creation from `esx_multicharacter` and `esx_identity`. 3. **Make yourself admin.** txAdmin adds your account as a master admin in `server.cfg`. To get the ESX `admin` group in game, see [Admin permissions](/docs/guides/admin-permissions). 4. **Create a dedicated database user.** Running a public server as `root` is a bad idea. Create a user that only has rights on your ESX database and update the connection string. See [server.cfg](/docs/getting-started/server-cfg#database-connection). 5. **Set up backups.** Back up the database and the `resources` folder before every change. txAdmin can schedule restarts, but it does not back up your database. ## Choosing the language [#choosing-the-language] ESX picks its language from the `esx:locale` convar. If that is not set, it uses the language you selected in txAdmin (`txAdmin-locale`), and falls back to English. To force a language, uncomment this line in `server.cfg`: ```bash title="server.cfg" setr esx:locale "de" ``` ## Common problems [#common-problems] | Problem | Cause and fix | | --- | --- | | Antivirus deletes or blocks `FXServer.exe` | False positive. Add the server folder to the antivirus exclusions and extract again. | | Recipe fails at `connect_database` | MariaDB is not running or the credentials are wrong. Check the MariaDB service and try logging in with HeidiSQL using the same values. | | Recipe fails while downloading addons | GitHub rate limit or a slow connection. Wait a few minutes and run the recipe again. | | License key rejected | The key belongs to another account or was revoked. Create a new one on the portal. | | Server not in the server list | Port `30120` is blocked (firewall or router) or the key is invalid. Players can still join with `connect <ip>:30120`. | | Players are kicked with "ESX Requires Onesync Infinity" | OneSync was switched off in the txAdmin settings. Set it back to `on`. | For more errors, see [Troubleshooting](/docs/getting-started/troubleshooting). ## Related [#related] - [Manual installation without txAdmin](/docs/getting-started/manual-installation) - [server.cfg for ESX explained](/docs/getting-started/server-cfg) - [Your first ESX resource](/docs/getting-started/first-resource) - [Updating ESX Legacy](/docs/getting-started/updating) # Manual Installation (without txAdmin) (/docs/getting-started/manual-installation) This page shows how to install ESX Legacy without the txAdmin recipe. Use it when you deploy with your own scripts or Docker, when you add ESX to an existing server, or when you want to know exactly what the recipe does. If you just want a working server, the [txAdmin recipe](/docs/getting-started/installation) is faster. ## Overview [#overview] You need four things: 1. A FiveM server build and a `server.cfg` 2. A MariaDB/MySQL database with the ESX schema from `legacy.sql` 3. The resources: `oxmysql`, `esx_lib`, `es_extended` and the other `[core]` resources, plus optional addons 4. The correct start order in `server.cfg` ## Step by step [#step-by-step] <Steps> <Step> ### Prepare the server build [#prepare-the-server-build] Download the latest server build for your OS from [runtime.fivem.net](https://runtime.fivem.net/artifacts/fivem/) and extract it into its own folder, for example `C:\FXServer\server` or `/opt/fxserver/server`. ESX Legacy refuses player connections on builds older than **10188**, so do not use an old build. Create a separate data folder next to it, for example `C:\FXServer\esx-server`. Your `server.cfg` and `resources` folder go there. </Step> <Step> ### Create the database [#create-the-database] Create an empty database with the `utf8mb4` character set. In HeidiSQL, DBeaver or the MariaDB console: ```sql CREATE DATABASE `es_extended` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- Optional but recommended: a dedicated user instead of root CREATE USER 'esx'@'localhost' IDENTIFIED BY 'change-me'; GRANT ALL PRIVILEGES ON `es_extended`.* TO 'esx'@'localhost'; FLUSH PRIVILEGES; ``` `legacy.sql` does not contain a `CREATE DATABASE` statement. You must create the database first and import the file into it. </Step> <Step> ### Download ESX [#download-esx] Download these repositories (use the latest release, or the `main` branch if you know what you are doing): | Repository | What you need from it | | --- | --- | | [esx-framework/esx_core](https://github.com/esx-framework/esx_core/releases) | the `[core]` folder and `[SQL]/legacy.sql` | | [esx-framework/ESX-Legacy-Addons](https://github.com/esx-framework/ESX-Legacy-Addons) | the `[esx_addons]` folder (optional) | | [overextended/oxmysql](https://github.com/overextended/oxmysql/releases/latest) | the release zip `oxmysql.zip`, not the source code | <Callout type="warn" title="Use the oxmysql release zip"> The oxmysql source code on GitHub must be built before it runs. Always download `oxmysql.zip` from the releases page. A source download starts but fails with missing file errors. </Callout> The recipe also adds `ox_lib`, `pma-voice` (voice chat), `bob74_ipl` (interiors), `sd-phone` and `sd-phone-props`. They are not required by `es_extended`, but you probably want voice and interiors on a roleplay server. </Step> <Step> ### Import legacy.sql [#import-legacysql] Import `[SQL]/legacy.sql` into the database you created. In HeidiSQL: select the database, then **File > Run SQL file**. From the command line: ```bash mysql -u esx -p es_extended < "legacy.sql" ``` The file creates the core tables (`users`, `items`, `jobs`, `job_grades`, `licenses`, `owned_vehicles` and more) and the tables used by the official addons (`addon_account`, `billing`, `datastore`, `vehicles`, `banking` and others). <Callout type="info"> Most statements are plain `CREATE TABLE`, so importing the file a second time into the same database fails with "table already exists". Import once into an empty database. Later schema changes are handled by the ESX migrations (see [Updating](/docs/getting-started/updating)). </Callout> </Step> <Step> ### Copy the resources [#copy-the-resources] Put the folders into `resources`. Folder names in square brackets are *categories*: FiveM searches inside them, and you can start a whole category with `ensure [name]`. <Files> <Folder name="esx-server" defaultOpen> <File name="server.cfg" /> <Folder name="resources" defaultOpen> <Folder name="[core]" defaultOpen> <Folder name="es_extended" /> <Folder name="esx_lib" /> <Folder name="esx_multicharacter" /> <Folder name="esx_identity" /> <Folder name="esx_skin" /> <Folder name="skinchanger" /> <Folder name="esx_notify" /> <Folder name="esx_context" /> <Folder name="..." /> </Folder> <Folder name="[esx_addons]"> <Folder name="esx_policejob" /> <Folder name="esx_banking" /> <Folder name="..." /> </Folder> <Folder name="[standalone]" defaultOpen> <Folder name="oxmysql" /> <Folder name="ox_lib" /> <Folder name="pma-voice" /> <Folder name="bob74_ipl" /> </Folder> </Folder> </Folder> </Files> Check that each resource folder directly contains its `fxmanifest.lua`. A common mistake is a double folder like `oxmysql/oxmysql/fxmanifest.lua` after extracting a zip, or a version suffix like `es_extended-main`. The folder name is the resource name, so `es_extended` must be called exactly that. </Step> <Step> ### Write server.cfg [#write-servercfg] Start from the `server.cfg` that ships in `esx_core`. The important part is the order at the end: ```bash title="server.cfg" set onesync on set mysql_connection_string "mysql://esx:change-me@localhost:3306/es_extended?charset=utf8mb4" ## Default resources ensure chat ensure spawnmanager ensure hardcap ## Database driver and library first ensure oxmysql ensure esx_lib ## ESX core ensure es_extended ensure [core] ## Standalone resources and addons ensure [standalone] ensure [esx_addons] ``` `oxmysql` lives inside `[standalone]`, so `ensure [standalone]` lists it a second time. The official recipe uses the same layout. The explicit `ensure oxmysql` line is what guarantees it starts before `es_extended`, so keep `oxmysql` and `esx_lib` above `es_extended` as shown. Add your license key (`sv_licenseKey`), endpoints and the admin ACEs. The full file is explained in [server.cfg for ESX](/docs/getting-started/server-cfg). </Step> <Step> ### Start the server [#start-the-server] From the data folder, start the build and point it at your config: ```bash # Windows (run from C:\FXServer\esx-server) C:\FXServer\server\FXServer.exe +exec server.cfg # Linux (run from /opt/fxserver/esx-server) bash /opt/fxserver/server/run.sh +exec server.cfg ``` On the first start you should see oxmysql connect to your database, then `es_extended` run its migrations. If it prints `Server restart required!`, stop and start the server once. </Step> </Steps> ## Why the start order matters [#why-the-start-order-matters] `es_extended` declares `oxmysql` and `esx_lib` as dependencies and loads `@oxmysql/lib/MySQL.lua` and `@esx_lib/imports.lua` from them. Every other ESX resource loads `@es_extended/imports.lua`, which calls `exports['es_extended']:getSharedObject()`. If a resource starts before `es_extended`, that export does not exist yet and you get errors like `No such export getSharedObject in resource es_extended` or `attempt to index a nil value (global 'ESX')`. FiveM starts the dependencies listed in a manifest automatically, but only if it can find them. Keeping the explicit order in `server.cfg` makes failures obvious and keeps your own scripts (which often forget to declare dependencies) behind the core. | Order | Resource | Why | | --- | --- | --- | | 1 | `oxmysql` | Database connection used by everything else | | 2 | `esx_lib` | Library loaded by `es_extended` | | 3 | `es_extended` | Creates the `ESX` object | | 4 | `[core]` | UI, identity, multicharacter, skin | | 5 | `[standalone]`, `[esx_addons]` | Jobs, shops and third-party scripts | | 6 | your own resources | Always after the core | ## Running without the addons [#running-without-the-addons] The addons are optional. A minimal server only needs `oxmysql`, `esx_lib`, `es_extended` and the `[core]` folder. If you leave out addons, you can still keep their tables in the database. Unused tables do nothing. If you remove a `[core]` resource, check what depends on it first. For example `esx_multicharacter` declares `esx_context`, `esx_identity` and `esx_skin` as dependencies and will not start without them. ## Common issues [#common-issues] - **`Couldn't find resource es_extended`**: the folder is named differently or sits one level too deep. Check `resources/[core]/es_extended/fxmanifest.lua` exists. - **`Table 'es_extended.users' doesn't exist`**: `legacy.sql` was imported into another database than the one in your connection string. - **oxmysql cannot connect**: see [Troubleshooting](/docs/getting-started/troubleshooting#oxmysql-cannot-connect-to-the-database). ## Related [#related] - [Install with txAdmin](/docs/getting-started/installation) - [server.cfg for ESX](/docs/getting-started/server-cfg) - [Database schema](/docs/guides/database-schema) - [oxmysql guide](/docs/guides/oxmysql) # server.cfg for ESX Legacy (/docs/getting-started/server-cfg) `server.cfg` is the file FXServer executes on startup. For ESX Legacy it must do four things: enable OneSync, tell `oxmysql` how to reach the database, start the resources in the right order and grant admin permissions. This page walks through a complete ESX `server.cfg` and lists every convar that ESX actually reads. ## Complete example [#complete-example] This is a cleaned-up version of the file the official recipe generates. Replace the placeholder values. ```bash title="server.cfg" ## Network endpoint_add_tcp "0.0.0.0:30120" endpoint_add_udp "0.0.0.0:30120" sv_endpointPrivacy true ## Server identity sv_licenseKey "your-license-key" sv_hostname "My Server | ESX Legacy" sets sv_projectName "My Server" sets sv_projectDesc "Serious roleplay on ESX Legacy" sets tags "esx, esx legacy, roleplay" sets locale "en-US" load_server_icon esxLogo.png sv_maxclients 48 sv_enforceGameBuild 3258 ## Required by ESX set onesync on set mysql_connection_string "mysql://esx:change-me@localhost:3306/es_extended?charset=utf8mb4" set resources_useSystemChat true ## ESX options setr esx:locale "en" #setr esx:identifier "license" ## Security convars from the recipe sv_enableNetworkedSounds false sv_enableNetworkedScriptEntityStates false sv_enableNetworkedPhoneExplosions false sv_filterRequestControl 2 ## Admin permissions add_principal group.admin group.user add_ace group.admin command allow add_ace group.admin command.quit deny add_ace resource.es_extended command.add_ace allow add_ace resource.es_extended command.add_principal allow add_ace resource.es_extended command.remove_principal allow add_ace resource.es_extended command.stop allow add_principal identifier.license:0123456789abcdef0123456789abcdef01234567 group.admin ## pma-voice setr voice_useNativeAudio true setr voice_useSendingRangeOnly true setr voice_enableRadioAnim 1 setr voice_enableUi 0 ## esx_whitelist (only if you use it) set discord:botToken "" set whitelist:webhook "" ## Start order ensure chat ensure oxmysql ensure esx_lib ensure es_extended ensure [core] ensure [standalone] ensure [esx_addons] ## your own resources below ``` ## OneSync [#onesync] ```bash title="server.cfg" set onesync on ``` ESX Legacy needs OneSync **Infinity**, which is what `on` means. When a player connects, `es_extended` checks the `onesync` convar and rejects the connection with `ESX Requires Onesync Infinity to work` if it is `off` or `legacy`. The server-side helpers in [ESX.OneSync](/docs/es_extended/server/onesync) also depend on it. If you run the server through txAdmin, OneSync is controlled in **Settings > FXServer** (the recipe enables it). A value set by txAdmin on the command line overrides `server.cfg`. ## Database connection [#database-connection] `oxmysql` reads `mysql_connection_string`. Two formats work: ```bash title="server.cfg" # URI format (recommended) set mysql_connection_string "mysql://esx:change-me@localhost:3306/es_extended?charset=utf8mb4" # Key-value format set mysql_connection_string "user=esx;password=change-me;host=localhost;port=3306;database=es_extended" ``` | Part | Meaning | | --- | --- | | `esx` / `change-me` | database user and password. With no password, use `mysql://root@localhost/es_extended`. | | `localhost:3306` | host and port of MariaDB | | `es_extended` | database name, the one you imported `legacy.sql` into | | `charset=utf8mb4` | needed for names and messages with emoji or non-Latin characters | `esx_multicharacter` also reads this convar to find the database name (it parses the path of the URI or the `database=` key). If the convar is empty, it stops with `unable to determine database from mysql_connection_string`. <Callout type="warn" title="Special characters in passwords"> In the URI format, characters like `@`, `:`, `/`, `#` or `?` in the password break parsing. URL-encode them (`@` becomes `%40`) or use the key-value format. Also never paste your connection string into Discord or GitHub; it contains the password. </Callout> Useful oxmysql convars such as `mysql_ui` (in-game query viewer for admins) and `mysql_slow_query_warning` are documented by [overextended](https://overextended.dev/oxmysql). See also our [oxmysql guide](/docs/guides/oxmysql). ## Start order [#start-order] ```bash title="server.cfg" ensure chat ensure oxmysql ensure esx_lib ensure es_extended ensure [core] ensure [standalone] ensure [esx_addons] ``` - `oxmysql` and `esx_lib` must run before `es_extended`, which loads files from both. - `es_extended` must run before every resource that uses `ESX`. - `[core]` is a category folder. FiveM starts all resources inside it and respects their declared dependencies. - Put your own resources **after** the core and addons. `chat` refers to the chat that is built into recent server builds; `set resources_useSystemChat true` tells FXServer to use it. The recipe deletes the old `chat` resource from `[cfx-default]` for that reason, and `esx_chat_theme` in `[core]` styles it. ## Admin permissions (ACE) [#admin-permissions-ace] ESX has two permission layers that work together: 1. **FiveM ACE principals** decide who may run which command. `ESX.RegisterCommand` adds `add_ace group.<name> command.<command> allow` for every group a command allows. 2. **The ESX group** (`user`, `admin` and any you add) is stored in the `group` column of `users` and returned by `xPlayer.getGroup()`. When a group is set, ESX also runs `add_principal identifier.<license> group.<group>` for that player, so the ACE layer follows the ESX group. The lines from the recipe: ```bash title="server.cfg" add_principal group.admin group.user # admins inherit everything users can do add_ace group.admin command allow # admins may run every command add_ace group.admin command.quit deny # ...except shutting down the server add_ace resource.es_extended command.add_ace allow add_ace resource.es_extended command.add_principal allow add_ace resource.es_extended command.remove_principal allow add_ace resource.es_extended command.stop allow ``` The four `resource.es_extended` lines are required. ESX runs `add_ace` and `add_principal` itself to grant command permissions to groups and to link each player to their ESX group. Without these lines those calls are denied, so a player whose ESX group is `admin` (set with `/setgroup` or in the database) gets no command permissions unless you also listed them in `server.cfg`. To make yourself admin, add your license as a principal of `group.admin`: ```bash title="server.cfg" add_principal identifier.license:0123456789abcdef0123456789abcdef01234567 group.admin ``` Players who have the `command` ACE get the `admin` group when their character is **created**. For an existing character, run `setgroup <id> admin` in the server console once. Details and custom groups: [Admin permissions](/docs/guides/admin-permissions). <Callout type="error" title="sv_lan makes everyone admin"> `es_extended` treats every player as admin when `sv_lan` is `true`. That is convenient for local testing, but never leave it on for a public server. </Callout> ## Convars read by ESX [#convars-read-by-esx] These are all the convars read by `es_extended` and the `[core]` resources in 1.16.4. ### es_extended [#es_extended] | Convar | Default | Description | | --- | --- | --- | | `esx:locale` | not set | Language code, e.g. `en`, `de`, `fr`. Use `setr`. If not set, ESX uses `txAdmin-locale`, then `en`. | | `esx:localeFallback` | `true` | When a translation key is missing, show the English text. Set to `false` while writing a new translation to see missing keys. | | `esx:identifier` | `license` | Identifier type used to store players (`license`, `steam`, `discord`, `fivem`, ...). Use `setr`. Changing it on a live server makes all existing players appear as new players. | | `onesync` | `off` | Must be `on`. | | `sv_lan` | not set | `true` grants admin to everyone (see above). | | `sv_enforceGameBuild` | `1604` | Read and returned to clients through a callback. Set it to the game build your server uses. | | `sv_projectName`, `sv_maxClients` | | Used for the Discord rich presence text. | | `inventory:accounts` | `["money"]` | Only with `ox_inventory`: which ESX accounts are stored as inventory items. Defined by ox_inventory. | | `mysql_connection_string` | | Read by `oxmysql` and by `esx_multicharacter`. | ### esx_lib [#esx_lib] | Convar | Default | Description | | --- | --- | --- | | `xLib:debug` | `false` | Extra debug output from esx_lib. | | `xLib:callbackTimeout` / `esx:callbackTimeout` | not set | Timeout for ESX callbacks. `xLib:` takes priority. | | `xLib:callbackMaxPayloadBytes` | `65536` | Maximum callback payload size (minimum 1024). | | `xLib:streamingTimeout` | `10000` | Milliseconds to wait when loading models, animations and other assets. | | `xLib:nuiDeferTimeout` | `10000` | Milliseconds to wait for NUI pages to be ready. | | `esx:ui:primaryColor`, `esx:ui:secondaryColor`, `esx:ui:backgroundColor`, `esx:ui:accentColor`, `esx:ui:logoUrl` | ESX brand colors | Theme for ESX UIs (for example `esx_whitelist`). Use `setr`. | ### Other core resources [#other-core-resources] | Convar | Read by | Description | | --- | --- | --- | | `discord:botToken` | `esx_whitelist` | Discord bot token for whitelist verification. Keep it secret; use `set`, never `setr`. | | `whitelist:webhook` | `esx_whitelist` | Optional Discord webhook for whitelist logs. | | `esx:locale` | `esx_multicharacter`, `esx_skin`, `skinchanger`, `esx_inventory`, `esx_whitelist` | Same language convar, default `en`. | <Callout type="info" title="set, setr and sets"> `set` is server-only. `setr` is also sent to clients, which is required for anything read in client or shared files. `sets` is public and shows up in the server list. Never use `setr` or `sets` for tokens, webhooks or passwords. </Callout> ## Common mistakes [#common-mistakes] - **Wrong quotes.** Windows editors sometimes insert smart quotes. Use plain `"`. - **Editing the wrong file.** txAdmin may be configured to use a `server.cfg` in another folder. Check **Settings > FXServer > CFG Path**. - **Your resource above `es_extended`.** It starts before ESX exists. Move it below `ensure [esx_addons]`. - **Two `ensure` lines for different copies of the same resource.** FiveM loads the first folder it finds; delete old copies like `es_extended_old`. ## Related [#related] - [Admin permissions](/docs/guides/admin-permissions) - [es_extended config](/docs/es_extended/config) - [Manual installation](/docs/getting-started/manual-installation) - [Troubleshooting](/docs/getting-started/troubleshooting) # Tools for ESX Development (/docs/getting-started/tools) This page collects tools that make developing and running an ESX Legacy server easier: editor extensions for writing scripts, database clients for managing player data, and community web tools for assets like clothing and props. None of them are required, but the first two groups save time every day. <Callout type="info" title="Third-party tools"> The tools below are made by independent developers. They are not maintained or audited by the ESX team or by this site. Review generated or merged files before you put them on a live server, and never upload your license key, database credentials or tokens to an online tool. </Callout> ## Editor [#editor] | Tool | What it does | | --- | --- | | [Visual Studio Code](https://code.visualstudio.com/) | Free editor that most FiveM developers use. | | [Lua (sumneko)](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) | Lua language server: autocomplete, diagnostics, go to definition and [annotations](/docs/getting-started/annotations). | | [CfxLua IntelliSense](https://marketplace.visualstudio.com/items?itemName=overextended.cfxlua-vscode) | Adds FiveM natives and the CfxLua syntax extensions (backtick hashes, compound assignment) to the language server. | | [ESX VSCode](https://marketplace.visualstudio.com/items?itemName=Knoblauchbrot.esx-vscode) | Snippets and completion for ESX functions and variables. | | [StyLua](https://github.com/JohnnyMorganz/StyLua) | Code formatter for Lua, so indentation and spacing stay consistent. | ## Server and database [#server-and-database] | Tool | What it does | | --- | --- | | [txAdmin](https://txadm.in/) | Web panel shipped with every FiveM server build: deploy the [ESX recipe](/docs/getting-started/installation), live console, scheduled restarts, player management, bans. | | [HeidiSQL](https://www.heidisql.com/) | Windows database client for MariaDB/MySQL. Import `legacy.sql`, browse the `users` table, export backups. | | [DBeaver](https://dbeaver.io/) | Cross-platform database client, useful on Linux and macOS. | | [oxmysql UI](https://overextended.dev/oxmysql) | With `set mysql_ui true`, admins can open an in-game view of query times to find slow queries. | | [Cfx.re docs](https://docs.fivem.net/) | Official native reference, resource manifest documentation and server commands. | ## Community web tools [#community-web-tools] These are the tools listed by the official ESX documentation. All have a free tier. | Tool | What it is used for | | --- | --- | | [Durty Cloth Tool](https://gta.clothing/) | Prepare and merge custom clothing for FiveM servers. | | [Forge (Plebmasters)](https://forge.plebmasters.de/) | Search GTA V game assets such as peds, vehicles, props and their names and hashes. Handy when you need the exact model name for a script or config. | | [Smart Merge V](https://smartmerge.de/) | A Plebmasters tool for combining many add-on resources into fewer ones. | | [SwisserAI](https://ai.swisser.dev/) | An AI assistant focused on FiveM scripting. Treat its output like any generated code: read and test it before use. | ## Tips for choosing tools [#tips-for-choosing-tools] - Prefer tools that work on your own files offline when the data is sensitive. Your database dump contains player identifiers. - When an AI tool or online generator gives you ESX code, compare it with this documentation. Many generate the deprecated `esx:getSharedObject` event pattern or functions that do not exist in ESX Legacy. - Merged clothing and vehicle packs can be large. Test the client load time after adding them; slow downloads are a common cause of players stuck while joining. ## Related [#related] - [Your first ESX resource](/docs/getting-started/first-resource) - [Annotations](/docs/getting-started/annotations) - [Coding practices](/docs/getting-started/coding-practices) # Troubleshooting ESX Legacy (/docs/getting-started/troubleshooting) This page lists the errors ESX Legacy server owners and developers run into most often, what causes them and how to fix them. Search the page (Ctrl+F) for the exact text of your error. ## How to find the real error [#how-to-find-the-real-error] Before you change anything, find the **first** error: 1. **Server console** or txAdmin **Live Console**: server-side errors, resource start failures, database errors. 2. **F8 console** in game: client-side script errors. 3. Scroll up to the first red line after the server started. One root cause (for example the database being down) produces dozens of follow-up errors in other resources. A Lua error tells you the resource, file and line: `@my_script/server/main.lua:12: attempt to index a nil value`. Open that file at that line. ## "attempt to index a nil value (global 'ESX')" [#attempt-to-index-a-nil-value-global-esx] Also seen as `ESX is nil` or `attempt to index a nil value (field 'PlayerData')`. The resource uses `ESX` but never received the shared object. Causes, from most to least common: 1. **Missing import.** Add it to the resource's `fxmanifest.lua`, above your own files: ```lua title="fxmanifest.lua" shared_scripts { '@es_extended/imports.lua', 'config.lua', } ``` 2. **Wrong start order.** The resource starts before `es_extended`. Move its `ensure` line below the ESX lines in `server.cfg` and add `dependency 'es_extended'` to its manifest. 3. **Old ESX script.** It uses `TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)` inside a thread with a wait loop. Replace that block with the import above. See [Convert old ESX scripts](/docs/guides/convert-old-esx-scripts). 4. **es_extended is not running at all.** Check the console for why it failed to start (see below). ## "No such export getSharedObject in resource es_extended" [#no-such-export-getsharedobject-in-resource-es_extended] Same root cause as above: `es_extended` is not started yet, failed to start, or the folder is not called exactly `es_extended`. Fix the start order or the start failure first. ## "Couldn't load resource es_extended" / es_extended does not start [#couldnt-load-resource-es_extended--es_extended-does-not-start] `es_extended` declares `esx_lib` and `oxmysql` as dependencies. If FiveM cannot find one, it refuses to start `es_extended`, and every ESX resource fails after it. Check: - `resources/[core]/esx_lib` exists. It is new in recent releases; servers updated by copying only `es_extended` often miss it. - `oxmysql` exists and comes from the **release zip**, not the GitHub source code. - There is exactly one `es_extended` folder and its `fxmanifest.lua` sits directly inside it (not `es_extended/es_extended/fxmanifest.lua` or `es_extended-main`). - Your server build is current. Old builds miss natives that `es_extended` requires. ## oxmysql cannot connect to the database [#oxmysql-cannot-connect-to-the-database] Players get kicked with `[ESX] OxMySQL Was Unable To Connect to your database`. The server console shows the actual database error from oxmysql at startup. Typical messages: | Error | Meaning | Fix | | --- | --- | --- | | `ECONNREFUSED` | Nothing listens on that host and port. | Start MariaDB (Windows: Services, `MariaDB`). Check host and port `3306`. | | `ER_ACCESS_DENIED_ERROR` | Wrong user or password. | Test the same credentials in HeidiSQL. Check for special characters in the password (URL-encode them). | | `ER_BAD_DB_ERROR` / `Unknown database` | The database name does not exist. | Create it, or fix the name in the connection string. | | `ER_NO_SUCH_TABLE` / `Table '...users' doesn't exist` | Connected, but `legacy.sql` was never imported into this database. | Import `[SQL]/legacy.sql` into the database named in the string. | The connection string formats are explained in [server.cfg](/docs/getting-started/server-cfg#database-connection). ## "ESX Requires Onesync Infinity to work" [#esx-requires-onesync-infinity-to-work] The server runs with OneSync `off` or `legacy`. Set `set onesync on` in `server.cfg`. On txAdmin, check **Settings > FXServer**: txAdmin's OneSync setting overrides the config file. ## "ESX Requires a minimum Artifact version of 10188" [#esx-requires-a-minimum-artifact-version-of-10188] Your FiveM server build is too old. Download a current build and replace the server binaries (not your data folder). ## "Native 0x... is not supported" or crashes after a build update [#native-0x-is-not-supported-or-crashes-after-a-build-update] The server build has a known problem. Switch to the latest build, or to a known-good one listed on [artifacts.jgscripts.com](https://artifacts.jgscripts.com), which marks broken builds. ## Players are stuck on the loading screen [#players-are-stuck-on-the-loading-screen] The loading screen stays until the character loads. If it never disappears: 1. **Check F8 and the server console** while the player is stuck. A script error during load (often in a HUD or a custom spawn script) stops the chain. 2. **Database errors.** If the `users` query fails, the player never loads. See the oxmysql section above. 3. **Missing [core] resources.** Without `esx_multicharacter` or `esx_identity`, character creation cannot finish. Check that `ensure [core]` is in `server.cfg` and all core resources started. 4. **Conflicting spawn or loading screen resources.** Two resources that both try to spawn the player or shut down the loading screen block each other. Remove duplicates (for example a second loading screen or spawn manager from another framework). 5. **Game build mismatch.** A resource that streams assets for a newer game build than `sv_enforceGameBuild` can hang the client while loading. ## Multicharacter problems [#multicharacter-problems] ### "Unable to start Multicharacter - unable to determine database from mysql_connection_string" [#unable-to-start-multicharacter---unable-to-determine-database-from-mysql_connection_string] `esx_multicharacter` reads the database name from `mysql_connection_string`. The convar is empty or in an unexpected format. Use one of the two formats from [server.cfg](/docs/getting-started/server-cfg#database-connection) and make sure the database name is included. ### Old characters disappear after installing multicharacter [#old-characters-disappear-after-installing-multicharacter] With multicharacter, identifiers in `users` get a prefix per slot (by default `char1:`, `char2:` and so on, set by `Config.Prefix`). Characters created before multicharacter was installed do not have the prefix, so they are not listed. Back up the database, then update the old rows, for example: ```sql UPDATE `users` SET `identifier` = CONCAT('char1:', `identifier`) WHERE `identifier` NOT LIKE 'char%:%'; ``` Other tables that store identifiers (such as `owned_vehicles.owner`) need the same change. ### "Error code: identifier-active" or "There is already an active session for this character" [#error-code-identifier-active-or-there-is-already-an-active-session-for-this-character] Another session with the same identifier is still registered, usually after a crash or a quick reconnect, or two people share an account. Wait for the old session to time out or restart the server. If it happens often, check for scripts that error in `playerDropped` and prevent cleanup. ## Admin commands say "access denied" [#admin-commands-say-access-denied] - Your license is not in `group.admin`, or your character was created before you added it. See [Admin permissions](/docs/guides/admin-permissions). - The four `add_ace resource.es_extended ...` lines are missing from `server.cfg`, so ESX cannot grant permissions. - You are typing an ox_inventory command name on a server with the default inventory or vice versa: `/giveitem` and `/giveweapon` from ESX only exist without ox_inventory. ## "Item "xyz" was used but does not exist!" [#item-xyz-was-used-but-does-not-exist] A script tries to give, remove or check an item that is not in the `items` table. Add the item (see [Add items](/docs/guides/add-items)), then run `/refreshitems` or restart. If you use ox_inventory, items are defined in its data files instead. ## "Server restart required!" repeats [#server-restart-required-repeats] ESX ran database migrations and needs one restart. Restart the server. If it shows again on every start, a migration fails before it is recorded: scroll up for `Failed migration` and fix that error. See [Updating](/docs/getting-started/updating#how-esx-migrations-work). ## Still stuck? [#still-stuck] - Search the [esx_core issues](https://github.com/esx-framework/esx_core/issues) for your exact error message. - Ask in the official [ESX Discord](https://discord.esx-framework.org/). Include the full error, your ESX version, server build number and what you changed last. - Never share your license key, database password or bot tokens when asking for help. ## Related [#related] - [server.cfg for ESX](/docs/getting-started/server-cfg) - [Updating ESX Legacy](/docs/getting-started/updating) - [esx_multicharacter](/docs/core/esx_multicharacter) - [FAQ](/docs/faq) # Updating ESX Legacy (/docs/getting-started/updating) Updating ESX Legacy is mostly a file swap: replace the core resources with the new release, start the server and let ESX migrate the database. What makes an update *safe* is everything around that swap: a backup you can restore, a list of your own changes, and a test run before players join. This page describes that process. The current release is **1.16.4** (October 2026). Releases and changelogs are published at [github.com/esx-framework/esx_core/releases](https://github.com/esx-framework/esx_core/releases). ## Before you update [#before-you-update] ### Read the release notes [#read-the-release-notes] Open the releases page and read every release between your version and the new one. Look for: - removed or renamed functions and config keys (old scripts may depend on them) - new dependencies (for example `esx_lib`, which `es_extended` now requires) - manual SQL steps or changed convars <Callout type="info" title="Version numbers in fxmanifest"> The `version` field in `fxmanifest.lua` is not always bumped with every release. In the 1.16.4 source, the core manifests still say `1.16.3`. Rely on the release tag and your download date, not only on the manifest. </Callout> ### Back up everything [#back-up-everything] 1. **Database.** Export the full database, for example with HeidiSQL (**Tools > Export database as SQL**) or from the command line: ```bash mysqldump -u esx -p --single-transaction --routines es_extended > esx_backup_2026-10-20.sql ``` 2. **Resources.** Copy the whole `resources` folder and `server.cfg`. A zip with the date in the name is enough. 3. **Check the backup.** Open the SQL file and confirm it contains `CREATE TABLE` and `INSERT` statements for `users`. A backup you never tested is a hope, not a backup. ### List your own changes [#list-your-own-changes] If you edited files inside `es_extended` or other core resources, those edits will be overwritten. Find them before you update: - If your resources are in git, `git diff` shows them. - Otherwise compare your folder with the release you originally installed, using a diff tool such as WinMerge or Meld. Move customizations out of core files where possible: settings belong in config files, new features in your own resources. The fewer edits you carry in the core, the easier every update gets. ## Update process [#update-process] <Steps> <Step> ### Stop the server [#stop-the-server] Stop it in txAdmin or the console. Do not swap files while it runs. </Step> <Step> ### Replace the core resources [#replace-the-core-resources] Download the release of `esx_core` and replace each resource in `resources/[core]` with the new version. Replace whole folders instead of copying files on top, so removed files do not stay behind. Keep your edited config files aside and merge your values into the **new** config files afterwards. New releases often add config keys; an old config file can be missing them. If you use the official addons, update `[esx_addons]` from [ESX-Legacy-Addons](https://github.com/esx-framework/ESX-Legacy-Addons) the same way. Also update `oxmysql` to its latest release. </Step> <Step> ### Update the server build [#update-the-server-build] Use a recent FiveM server build. ESX refuses player connections on builds older than 10188, and new ESX releases are tested against current builds. </Step> <Step> ### Start the server and let migrations run [#start-the-server-and-let-migrations-run] On startup `es_extended` runs its database migrations. Each ESX version that changed the schema (for example `v1.13.3`, `v1.13.5`, `v1.14.1`, `v1.16.0`) has a migration that runs once. You see: ```txt [INFO] Running migrations for ESX version v1.16.0 [SUCCESS] Successfully completed migrations for ESX version v1.16.0 [INFO] Ran migrations for 1 ESX version(s). Server restart required! ``` When the restart message appears, ESX keeps repeating it until you restart. Restart the server once; the migrations are recorded and do not run again. If a migration fails, the console prints `Failed migration ['<version>.<name>']` with the database error. Fix the cause (often duplicate data or a missing permission for your database user) and start again. </Step> <Step> ### Test before players join [#test-before-players-join] Join with a test account and check the basics: character selection, money and inventory, a job action, a shop purchase, saving after reconnect. Watch the server console and F8 console for errors. </Step> </Steps> ## How ESX migrations work [#how-esx-migrations-work] Migrations live in `es_extended/server/migration/<version>/`. When one completes, ESX stores a flag in the resource KVP store (`esx_migration:<version>`), so it is skipped on later starts. This means: - You do not need to import `legacy.sql` again when updating. Never do that on a live database. - If you restore a database backup from **before** a migration, the flags still say "done" and the migration will not rerun. Run `resetmigrations` in the **server console** (it refuses to run from in game), then restart. All migrations run again on the next start. Some addons (`esx_billing`, `esx_license`, `esx_addonaccount`, `esx_banking`) also apply their own schema migrations automatically when they start. ## Optional: security hardening SQL (October 2026) [#optional-security-hardening-sql-october-2026] The ESX-Legacy-Addons repository contains [`sql/security_hardening_2026_10_02.sql`](https://github.com/esx-framework/ESX-Legacy-Addons/blob/main/sql/security_hardening_2026_10_02.sql). It is a manual alternative to the automatic addon migrations mentioned above. It: - adds a `billing_payments` table that makes bill payments idempotent - adds a unique key on `user_licenses (owner, type)` - adds an `owner_key` column and a unique key to `addon_account_data` - adds indexes on `billing.identifier` and `banking.time` Before it changes anything, it checks for duplicate licenses, duplicate or invalid addon accounts and economy tables that are not InnoDB. If it finds any, it stops with a message such as `Duplicate addon accounts: reconcile balances before migration` and changes nothing. It never merges or deletes financial rows for you. Notes before running it: - It requires **MySQL 8 or MariaDB 10.6+**. - DDL statements commit immediately, so it is not a rollback-safe transaction. Take a backup first and run it in a maintenance window. - It is safe to run more than once. - The older `security_indexes.sql` in the same repository is superseded by this file. ## Updating old ESX (1.1, 1.2, v1 final) [#updating-old-esx-11-12-v1-final] There is no in-place upgrade from the original ESX releases to ESX Legacy. The database layout, the inventory format and the API changed too much. The practical path is a fresh ESX Legacy install, then migrating your data and converting scripts one by one. See [Convert old ESX scripts](/docs/guides/convert-old-esx-scripts). ## Rolling back [#rolling-back] If the update breaks something you cannot fix quickly: 1. Stop the server. 2. Restore the `resources` folder from your backup. 3. Restore the database from the dump: `mysql -u esx -p es_extended < esx_backup_2026-10-20.sql` into an empty database. 4. Start the server and confirm it works, then investigate the problem on a test server. ## Related [#related] - [Troubleshooting](/docs/getting-started/troubleshooting) - [Database schema](/docs/guides/database-schema) - [Convert old ESX scripts](/docs/guides/convert-old-esx-scripts) - [Manual installation](/docs/getting-started/manual-installation) # Add Items (/docs/guides/add-items) To add an item in ESX Legacy, insert a row into the `items` table and reload items with `/refreshitems` (or restart the server). To make the item do something when a player uses it, register a server-side handler with `ESX.RegisterUsableItem`. If your server runs ox_inventory, items are defined in ox_inventory's own `data/items.lua` instead and the ESX `items` table is not used. ## Overview [#overview] | What | Where | | --- | --- | | Item definitions (default inventory) | `items` table | | In-memory cache | `ESX.Items`, filled by `ESX.RefreshItems()` at startup | | Reload without restart | `/refreshitems` command | | Add items from a script | `ESX.AddItems(items)` | | Use action | `ESX.RegisterUsableItem(name, cb)` (server) | | Give an item | `/giveitem` command or `xPlayer.addInventoryItem()` | <Callout type="warn" title="Using ox_inventory?"> When the `ox_inventory` resource exists on the server, es_extended switches to ox mode automatically (`Config.CustomInventory = 'ox'`). In that mode ESX does not read the `items` table, `ESX.RefreshItems` and `ESX.AddItems` are not defined, and the ESX `/giveitem` and `/refreshitems` commands are not registered. Follow the [ox_inventory guide](/docs/guides/ox-inventory) instead. </Callout> ## The items table [#the-items-table] | Column | Type | Default | Description | | --- | --- | --- | --- | | `name` | `varchar(50)` | | Internal item name, primary key. Lowercase, no spaces, for example `water`. | | `label` | `varchar(50)` | | Display name shown in the inventory. | | `weight` | `int` | `1` | Weight per piece. The player's limit is `Config.MaxWeight` (24 by default). | | `rare` | `tinyint` | `0` | Stored and passed to the inventory as `rare`. es_extended itself does not change behavior based on it; UI or third-party scripts may. | | `can_remove` | `tinyint` | `1` | `0` blocks dropping and giving the item through the default ESX inventory. | ## Add an item with SQL [#add-an-item-with-sql] <Steps> <Step> ### Insert the item [#insert-the-item] ```sql title="items.sql" INSERT INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES ('energy_drink', 'Energy Drink', 1, 0, 1), ('lockpick', 'Lockpick', 1, 0, 1), ('id_card', 'ID Card', 0, 0, 0); ``` Use `INSERT IGNORE` if the script might be installed twice; `name` is the primary key, so duplicates fail otherwise. </Step> <Step> ### Reload items [#reload-items] In the server console or in game as an admin: ```bash refreshitems ``` `/refreshitems` calls `ESX.RefreshItems()`, which reloads the table and also cleans every online player's inventory: items that no longer exist are removed and the inventory is resent to the client. </Step> <Step> ### Give the item to test it [#give-the-item-to-test-it] ```bash giveitem 1 energy_drink 5 ``` The arguments are `[playerId] [item] [count]`; `me` works as player ID in game. The command validates that the item exists in `ESX.Items`. </Step> </Steps> ## Add items from a script with ESX.AddItems [#add-items-from-a-script-with-esxadditems] If your resource needs its own items, register them at runtime so server owners do not have to run SQL. ```lua ESX.AddItems(items) ``` | Name | Type | Description | | --- | --- | --- | | `items` | `table` | List of `{ name, label, weight?, rare?, canRemove? }`. `weight` defaults to `1`, `rare` to `false`, `canRemove` to `true`. | It skips items that already exist in `ESX.Items`, validates every field (invalid entries are skipped with a console message), inserts the rest with `INSERT IGNORE` and refreshes online inventories. There is no return value. ```lua title="server/items.lua" ESX = exports['es_extended']:getSharedObject() if ESX.AddItems then -- not defined when ox_inventory is used ESX.AddItems({ { name = 'energy_drink', label = 'Energy Drink', weight = 1 }, { name = 'id_card', label = 'ID Card', weight = 0, canRemove = false }, }) end ``` ## Make an item usable [#make-an-item-usable] Usable items are server-side. When a player uses the item in the default inventory, the client sends `esx:useItem`; ESX checks that the player actually has at least one, then calls your handler. ESX does **not** remove the item for you. ```lua ESX.RegisterUsableItem(item, cb) ``` | Name | Type | Description | | --- | --- | --- | | `item` | `string` | Item name from the `items` table. | | `cb` | `function(source, item, ...)` | Called with the player's server ID and the item name. | ```lua title="server/items.lua" ESX = exports['es_extended']:getSharedObject() ESX.RegisterUsableItem('energy_drink', function(source, item) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return end -- Remove first so a fast double-use cannot consume one item twice xPlayer.removeInventoryItem(item, 1) xPlayer.showNotification('You feel energized!') TriggerClientEvent('energy:boost', source, 30000) end) ``` ```lua title="client/main.lua" RegisterNetEvent('energy:boost', function(durationMs) local playerId = PlayerId() SetRunSprintMultiplierForPlayer(playerId, 1.2) Wait(durationMs) SetRunSprintMultiplierForPlayer(playerId, 1.0) end) ``` The handler is wrapped in `pcall`, so an error in your code prints a warning instead of breaking ESX. Registering an item marks it as `usable = true` in online players' inventories immediately. `ESX.GetUsableItems()` returns a table of all registered usable item names, and `ESX.UseItem(source, item)` lets you trigger the handler yourself. Official addons follow the same pattern, for example `esx_basicneeds` (bread, water) and `esx_ambulancejob` (`bandage`, `medikit`). ## Work with items in scripts [#work-with-items-in-scripts] The most used [xPlayer](/docs/es_extended/server/xplayer) inventory methods: ```lua title="server/shop.lua" local xPlayer = ESX.GetPlayerFromId(source) local item = xPlayer.getInventoryItem('lockpick') -- { name, label, count, weight, usable, rare, canRemove } if item and item.count >= 1 then xPlayer.removeInventoryItem('lockpick', 1) end if xPlayer.canCarryItem('energy_drink', 3) then xPlayer.addInventoryItem('energy_drink', 3) else xPlayer.showNotification('Your pockets are full') end ``` Use `ESX.GetItemLabel(name)` to show the label of an item, and `ESX.GetItems()` to get the full item registry. ### Starting items [#starting-items] `Config.StartingInventoryItems` in `es_extended/shared/config/main.lua` is `false` by default. Set it to a table of `item = count` to give new characters items on creation: ```lua title="es_extended/shared/config/main.lua" Config.StartingInventoryItems = { bread = 2, water = 2 } ``` It only applies to newly created players, not to existing ones. ## ox_inventory note [#ox_inventory-note] ox_inventory has its own item list and its own way of handling item use: - Define items in `ox_inventory/data/items.lua`. The ESX `items` table has no effect while ox mode is active. - `ESX.Items` is filled from ox_inventory's item list, and `ESX.GetItemLabel` reads from `exports.ox_inventory:Items(name)`. - `xPlayer.addInventoryItem`, `removeInventoryItem`, `getInventoryItem`, `canCarryItem` and `hasItem` still work; ESX routes them to ox_inventory. - ox_inventory can run code on use through its item options (for example a `server.export`). Check the [ox_inventory docs](https://overextended.dev/ox_inventory) for how it handles items registered with `ESX.RegisterUsableItem`. Details and setup order: [Using ox_inventory with ESX](/docs/guides/ox-inventory). ## Common issues [#common-issues] <Accordions> <Accordion title="'Item ... was used but does not exist!'"> The item is registered as usable but is not in `ESX.Items`. Add it to the `items` table and run `refreshitems`. </Accordion> <Accordion title="Items disappear from inventories after /refreshitems"> `ESX.RefreshItems` removes items from online players that are no longer in the table. That is expected when you rename or delete an item; update scripts that still give the old name. </Accordion> <Accordion title="/giveitem or /refreshitems does not exist"> ox_inventory is present on the server, so ESX runs in ox mode and does not register these commands. Use ox_inventory's own commands. </Accordion> <Accordion title="Players can carry almost nothing"> Item weights add up against `Config.MaxWeight` (default `24`). Lower item weights or raise the limit, or use `xPlayer.setMaxWeight(newWeight)` for individual players (for example when they wear a backpack). </Accordion> </Accordions> ## Related [#related] - [xPlayer reference](/docs/es_extended/server/xplayer) - [Using ox_inventory with ESX](/docs/guides/ox-inventory) - [Database schema](/docs/guides/database-schema) - [esx_inventory](/docs/core/esx_inventory) - [esx_basicneeds](/docs/addons/esx_basicneeds) # Admin Permissions (/docs/guides/admin-permissions) To become admin in ESX Legacy, set your ESX group to `admin`: run `setgroup <yourServerId> admin` in the server console, or set `users.group` to `admin` in the database while you are offline. ESX then links that group to FiveM's ACE system (`group.admin`), which is what actually unlocks the admin commands, so `server.cfg` also needs the ACE lines shown below. ## Overview [#overview] ESX permissions have two layers that work together: | Layer | Stored in | Used for | | --- | --- | --- | | ESX group | `users.group` (default `user`) | `xPlayer.getGroup()`, scripts that check groups, the `Player(source).state.group` state bag | | ACE principal | Added at runtime by es_extended | FiveM command permissions (`command.setjob` and so on) | When a player loads, es_extended runs `add_principal identifier.<license> group.<esxGroup>`. When you change the group with `/setgroup` or `xPlayer.setGroup()`, it removes the old principal and adds the new one. Every command registered with `ESX.RegisterCommand` gets an `add_ace group.<group> command.<name> allow` entry for each allowed group. The default groups are `user`, `admin` and `owner`. The column is free text, so you can invent more, but each group needs entries in `Config.CommandPermissions` to be useful. A group named `superadmin` is converted to `admin` when the player loads. ## Required server.cfg lines [#required-servercfg-lines] These lines come with the official txAdmin recipe and `server.cfg`. If you set up the server by hand, check that they exist: ```bash title="server.cfg" add_principal group.admin group.user add_ace group.admin command allow # allow all commands add_ace group.admin command.quit deny # but don't allow quit add_ace resource.es_extended command.add_ace allow add_ace resource.es_extended command.add_principal allow add_ace resource.es_extended command.remove_principal allow add_ace resource.es_extended command.stop allow ``` The `resource.es_extended` lines let es_extended run `add_ace`, `add_principal` and `remove_principal` itself. Without them, ESX groups never turn into ACE permissions and admin commands are denied even when the database says `admin`. ## Make yourself admin [#make-yourself-admin] <Steps> <Step> ### Join the server once [#join-the-server-once] ESX creates your row in `users` on first join. Note your server ID (txAdmin player list, or the scoreboard). </Step> <Step> ### Set the group from the console [#set-the-group-from-the-console] In the server console or the txAdmin live console, without a slash: ```bash setgroup 1 admin ``` The arguments are `[playerId] [group]`. In game, an existing admin can run `/setgroup me admin` or `/setgroup 5 owner`. The group is applied immediately; it is written to the database on the next player save (every 10 minutes, on disconnect, or with `/save [playerId]`). </Step> <Step> ### Verify [#verify] Run `/group` in game. The command prints your current group to the **server console**. In scripts, read `xPlayer.getGroup()`. </Step> </Steps> ### Alternative: edit the database [#alternative-edit-the-database] With the player offline, update the row directly. The identifier is the Rockstar license hash without the `license:` prefix; with esx_multicharacter it is prefixed with the character slot, for example `char1:`. ```sql UPDATE `users` SET `group` = 'admin' WHERE `identifier` = 'char1:1a2b3c4d5e6f...'; ``` Do this only while the player is offline. An online player's group lives in memory and is saved back on the next save, which would overwrite your change. ### Alternative: admin on first join through ACE [#alternative-admin-on-first-join-through-ace] When a **new** player row is created, es_extended checks `IsPlayerAceAllowed(source, 'command')`. If the player already has ACE access to `command` (for example through an `add_principal identifier.license:... group.admin` line in `server.cfg`), they are created with group `admin`. The txAdmin recipe adds such lines for the txAdmin master account (`{{addPrincipalsMaster}}`), which is why the server owner is often admin automatically. This only affects new characters; existing ones keep their stored group. ```bash title="server.cfg" add_principal identifier.license:1a2b3c4d5e6f... group.admin ``` On a local test server with `sv_lan` set to `true`, every player counts as admin. ## Config.CommandPermissions [#configcommandpermissions] In `es_extended/shared/config/main.lua`, `Config.CommandPermissions` maps each built-in command to the groups that may use it: ```lua title="es_extended/shared/config/main.lua" Config.CommandPermissions = { ["setjob"] = { "owner", "admin" }, ["car"] = { "owner", "admin" }, ["noclip"] = { "owner", "admin" }, ["job"] = { "user", "admin", "owner" }, -- ... } ``` To let a `mod` group use `/goto` and `/bring` but nothing else: ```lua title="es_extended/shared/config/main.lua" ["goto"] = { "owner", "admin", "mod" }, ["bring"] = { "owner", "admin", "mod" }, ``` Then `setgroup <id> mod`. A command without an entry is restricted to `admin` with a console warning. `Config.AdminGroups` (`owner` and `admin`) is marked deprecated in 1.16.4 but is still read by `xPlayer.isAdmin()`. Keep it in sync if third-party scripts rely on it. ## Built-in admin commands [#built-in-admin-commands] All of these accept `me` as the player ID in game. Most also work from the server console. | Command | Arguments | What it does | | --- | --- | --- | | `/setgroup` | `playerId group` | Change ESX group | | `/setjob` | `playerId job grade` | Change job ([Create a job](/docs/guides/create-a-job)) | | `/giveaccountmoney` | `playerId account amount` | Add money to `money`, `bank` or `black_money` | | `/setaccountmoney` / `/removeaccountmoney` | `playerId account amount` | Set or remove account money | | `/giveitem` | `playerId item count` | Give an item (default inventory only) | | `/giveweapon`, `/giveammo`, `/giveweaponcomponent` | | Weapons (default inventory only) | | `/car`, `/cardel` (`/dv`), `/fix` (`/repair`) | | Spawn, delete, repair vehicles | | `/setcoords` (`/tp`), `/tpm`, `/goto`, `/bring` | | Teleporting | | `/noclip`, `/kill`, `/freeze`, `/unfreeze` | | Player control | | `/setdim` (`/setbucket`) | | Routing bucket | | `/players`, `/save`, `/saveall`, `/refreshjobs`, `/refreshitems` | | Server management | With `Config.AdminLogging = true` and a webhook configured in the logs config, many of these commands are logged to Discord. For a full menu, see [esx_adminmenu](/docs/addons/esx_adminmenu). A full list of commands is on the [Commands](/docs/es_extended/commands) page. ## Add your own admin-only command [#add-your-own-admin-only-command] `ESX.RegisterCommand` handles the ACE setup, argument parsing and the chat suggestion for you: ```lua title="server/main.lua" ESX = exports['es_extended']:getSharedObject() ESX.RegisterCommand('announce', { 'owner', 'admin' }, function(xPlayer, args, showError) if #args.message < 3 then return showError('Message too short') end TriggerClientEvent('chat:addMessage', -1, { args = { 'Announcement', args.message }, }) end, true, { help = 'Send a server-wide announcement', arguments = { { name = 'message', help = 'Text to send', type = 'merge' }, }, }) ``` | Argument | Description | | --- | --- | | `name` | Command name, or a table of aliases like `{ 'announce', 'ann' }` | | `group` | Group name or list of groups allowed to run it | | `cb` | `function(xPlayer, args, showError)`; `xPlayer` is `false` when run from the console | | `allowConsole` | `true` to allow the server console | | `suggestion` | `help`, `arguments` (types: `number`, `player`, `playerId`, `string`, `item`, `weapon`, `coordinate`, `merge`, `any`) and optional `validate = true` to require the exact argument count | To check permissions in your own events, read the group on the server, never trust the client: ```lua title="server/main.lua" RegisterNetEvent('myresource:adminAction', function() local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer or xPlayer.getGroup() ~= 'admin' then return end -- privileged code end) ``` ## Common issues [#common-issues] <Accordions> <Accordion title="Group is admin but commands are denied"> The `add_ace resource.es_extended ...` lines are missing from `server.cfg`, or another resource removed the principal. Restart after adding the lines. </Accordion> <Accordion title="My group resets to user after a restart"> You edited the database while the player was online, and the in-memory group was saved back over it. Use `/setgroup` instead, or edit while offline. </Accordion> <Accordion title="/setgroup accepted a typo"> `/setgroup` does not validate group names. A group like `amdin` has no permissions. Set it again with the correct name. </Accordion> <Accordion title="The owner group cannot use non-ESX commands"> `server.cfg` only grants `command allow` to `group.admin`. `owner` only gets the ESX commands listed for it in `Config.CommandPermissions`. Add `add_principal group.owner group.admin` if owners should inherit everything admins have. </Accordion> </Accordions> ## Related [#related] - [Commands](/docs/es_extended/commands) - [server.cfg](/docs/getting-started/server-cfg) - [xPlayer reference](/docs/es_extended/server/xplayer) - [Main config](/docs/es_extended/config) - [esx_adminmenu](/docs/addons/esx_adminmenu) # Callbacks (/docs/guides/callbacks) An ESX callback is a request/response call between client and server: one side sends a request with arguments, the other side runs a handler and sends values back. Use `ESX.RegisterServerCallback` on the server and `ESX.TriggerServerCallback` or `ESX.AwaitServerCallback` on the client whenever the client needs data that only the server has (money, database rows, ownership checks). ## Overview [#overview] | Function | Side | Purpose | | --- | --- | --- | | `ESX.RegisterServerCallback(name, handler)` | Server | Answer requests from clients | | `ESX.TriggerServerCallback(name, cb, ...)` | Client | Ask the server, get the answer in `cb` | | `ESX.AwaitServerCallback(name, ...)` | Client | Ask the server and wait for the return values | | `ESX.DoesServerCallbackExist(name)` | Server | Check whether a name is registered | | `ESX.RegisterClientCallback(name, handler)` | Client | Answer requests from the server | | `ESX.TriggerClientCallback(playerId, name, cb, ...)` | Server | Ask one client, get the answer in `cb` | | `ESX.AwaitClientCallback(playerId, name, ...)` | Server | Ask one client and wait for the return values | | `ESX.DoesClientCallbackExist(name)` | Client | Check whether a name is registered | In ESX Legacy 1.16 all of these are thin wrappers around the callback system in `esx_lib` (`xLib.callback`). ESX keeps the classic handler signatures so old scripts still work: - Server handler: `function(source, cb, ...)` - Client handler: `function(cb, ...)` ## Create a server callback [#create-a-server-callback] <Steps> <Step> ### Load the ESX object [#load-the-esx-object] Add the ESX import to your `fxmanifest.lua` so `ESX` is available on both sides. ```lua title="fxmanifest.lua" fx_version 'cerulean' game 'gta5' lua54 'yes' shared_script '@es_extended/imports.lua' server_script 'server/main.lua' client_script 'client/main.lua' ``` </Step> <Step> ### Register the handler on the server [#register-the-handler-on-the-server] The first argument is the player's server id. Always call `cb` exactly once, also when you reject the request. ```lua title="server/main.lua" ESX.RegisterServerCallback('myshop:getBalance', function(source, cb, accountName) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer or (accountName ~= 'money' and accountName ~= 'bank') then return cb(nil) end local account = xPlayer.getAccount(accountName) cb(account and account.money or 0) end) ``` </Step> <Step> ### Call it from the client [#call-it-from-the-client] Callback style (non-blocking): ```lua title="client/main.lua" ESX.TriggerServerCallback('myshop:getBalance', function(balance) if not balance then return end ESX.ShowNotification(('Bank balance: $%s'):format(ESX.Math.GroupDigits(balance))) end, 'bank') ``` Await style (blocks the current thread until the answer arrives): ```lua title="client/main.lua" CreateThread(function() local balance = ESX.AwaitServerCallback('myshop:getBalance', 'bank') if balance then print('Bank balance', balance) end end) ``` </Step> </Steps> `AwaitServerCallback` must run inside a thread (for example `CreateThread`, an event handler, or a command handler), because it suspends the coroutine while it waits. ## Return several values [#return-several-values] `cb` accepts any number of arguments, and they arrive in the same order on the other side. ```lua title="server/main.lua" ESX.RegisterServerCallback('myjob:getStatus', function(source, cb) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return cb(false) end local job = xPlayer.getJob() cb(true, job.name, job.grade, job.onDuty) end) ``` ```lua title="client/main.lua" CreateThread(function() local ok, jobName, grade, onDuty = ESX.AwaitServerCallback('myjob:getStatus') if ok then print(jobName, grade, onDuty) end end) ``` ## Database work inside a callback [#database-work-inside-a-callback] oxmysql's `.await` functions fit naturally into callback handlers, because the handler already runs in its own thread. ```lua title="server/main.lua" ESX.RegisterServerCallback('mygarage:getVehicles', function(source, cb) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return cb({}) end local rows = MySQL.query.await( 'SELECT `plate`, `vehicle`, `stored` FROM `owned_vehicles` WHERE `owner` = ?', { xPlayer.getIdentifier() } ) cb(rows or {}) end) ``` See [Using oxmysql with ESX](/docs/guides/oxmysql) for the full query API. ## Client callbacks (server asks the client) [#client-callbacks-server-asks-the-client] Sometimes the server needs something only the client knows, such as the vehicle the player is looking at or a UI choice. Register the handler on the client and call it from the server. ```lua title="client/main.lua" ESX.RegisterClientCallback('myscript:getStreetName', function(cb) local coords = GetEntityCoords(PlayerPedId()) local streetHash = GetStreetNameAtCoord(coords.x, coords.y, coords.z) cb(GetStreetNameFromHashKey(streetHash)) end) ``` ```lua title="server/main.lua" RegisterCommand('where', function(source) if source == 0 then return end local street = ESX.AwaitClientCallback(source, 'myscript:getStreetName') print(('Player %s is on %s'):format(source, street)) end, false) ``` Treat the answer from a client callback as untrusted input, exactly like a net event. A modified client can return anything. ## Security: validate on the server [#security-validate-on-the-server] A server callback is a public entry point. Any connected client can call it with any arguments, as often as the rate limit allows. The rules are the same as for `RegisterNetEvent` handlers: 1. Use `source` to identify the player. Never accept a player id, identifier or job from the client arguments. 2. Check types and ranges (`type(amount) == 'number'`, `amount > 0`, `amount <= limit`). 3. Re-check permissions and state on the server (job, grade, distance to a shop, item count). 4. Do the money or item change on the server, not in the client after the callback returns. ```lua title="server/main.lua" local SHOP_COORDS <const> = vector3(25.7, -1347.3, 29.5) local PRICE <const> = 5 ESX.RegisterServerCallback('myshop:buyWater', function(source, cb, amount) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer or type(amount) ~= 'number' or amount ~= math.floor(amount) or amount < 1 or amount > 10 then return cb(false, 'invalid_request') end if #(GetEntityCoords(GetPlayerPed(source)) - SHOP_COORDS) > 5.0 then return cb(false, 'too_far') end if not xPlayer.canCarryItem('water', amount) then return cb(false, 'no_space') end if not xPlayer.removeMoney(PRICE * amount, 'Bought water') then return cb(false, 'no_money') end xPlayer.addInventoryItem('water', amount) cb(true) end) ``` `xPlayer.removeMoney` returns `false` when the player does not have enough money, so the check and the deduction happen in one step. ## Timeouts, payload size and rate limits [#timeouts-payload-size-and-rate-limits] The callback system in `esx_lib` adds protection that older ESX versions did not have. You can tune it with convars in `server.cfg`. | Convar | Default | Effect | | --- | --- | --- | | `xLib:callbackTimeout` (or `esx:callbackTimeout`) | `300000` ms await timeout | How long a pending request waits before it is rejected. The server also cancels handlers that run too long (30 seconds unless the convar is set). | | `xLib:callbackMaxPayloadBytes` | `65536` | Maximum size of the arguments a client may send to a server callback | | `xLib:callbackBurst` / `xLib:callbackRate` | `60` / `30` per second | Per-player request budget | | `xLib:callbackMaxInFlight` | `8` | Parallel callbacks one player may have open | ```bash title="server.cfg" set xLib:callbackTimeout 15000 set xLib:callbackMaxPayloadBytes 32768 ``` When a request is over budget or too large, the server answers with `false` instead of running your handler. Your client code should handle a `false` or `nil` result. ## Common issues [#common-issues] - **Callback never returns**: an early `return` without `cb(...)`. Every code path in the handler must call `cb`. - **"callback is already owned by resource"**: callback names are owned by the resource that registered them first. Two resources cannot register the same name. Prefix names with your resource name (`myshop:getBalance`). - **Callback stops working after a restart**: callbacks are removed when the owning resource stops. Restart the resource that registers the callback, not only the one that calls it. - **`AwaitServerCallback` errors outside a thread**: wrap the call in `CreateThread`. - **Large tables fail silently**: arguments above `xLib:callbackMaxPayloadBytes` are rejected. Send ids and load the data on the server instead. - **Trusting the client**: a callback that takes a price or an amount of money from the client is an exploit. Calculate those values on the server. ## Related [#related] - [Client callback module reference](/docs/es_extended/client/modules/callback) - [Server functions](/docs/es_extended/server/functions) - [xPlayer reference](/docs/es_extended/server/xplayer) - [Using oxmysql with ESX](/docs/guides/oxmysql) - [Coding practices](/docs/getting-started/coding-practices) # 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) # Create a Job (/docs/guides/create-a-job) To create a job in ESX Legacy, add one row to the `jobs` table and one row per rank to the `job_grades` table, then reload the jobs with `/refreshjobs` (or restart the server). From a script you can do the same at runtime with `ESX.CreateJob`. A job only becomes a "company" with a bank account and boss menu when you also register it with `esx_society`. ## Overview [#overview] | What | Where | | --- | --- | | Job definition | `jobs` table (`name`, `label`, `type`, `whitelisted`) | | Ranks and salaries | `job_grades` table, one row per grade | | In-memory cache | `ESX.Jobs`, filled by `ESX.RefreshJobs()` at startup | | Assign a job | `/setjob` command or `xPlayer.setJob()` | | Company money and boss menu | `esx_society` + `esx_addonaccount` (optional) | ESX loads all jobs once when the database is ready. A job that has no rows in `job_grades` is skipped with a warning, and a player whose saved job or grade does not exist is moved to `unemployed` grade `0` when they load. ## How ESX stores jobs [#how-esx-stores-jobs] The `jobs` table holds one row per job: | Column | Type | Description | | --- | --- | --- | | `name` | `varchar(50)` | Internal job name, primary key. Used in code, for example `police`. | | `label` | `varchar(50)` | Display name, for example `LSPD`. | | `type` | `varchar(50)` | Free-form category, default `civ`. The default data uses `civ`, `leo`, `ems` and `mechanic`. `ESX.GetJobs(jobType)` can filter by it. | | `whitelisted` | `tinyint(1)` | `1` hides the job from `esx_joblisting` (the job center). Default `0`. | The `job_grades` table holds the ranks: | Column | Type | Description | | --- | --- | --- | | `id` | `int` | Auto-increment primary key. Leave it out of your inserts. | | `job_name` | `varchar(50)` | Must match `jobs.name`. | | `grade` | `int` | Rank number, starting at `0`. | | `name` | `varchar(50)` | Internal grade name, for example `boss`. Available as `xPlayer.job.grade_name`. | | `label` | `varchar(50)` | Display name of the rank. | | `salary` | `int` | Paycheck amount per interval. | | `skin_male` / `skin_female` | `longtext` | JSON uniform data. `NOT NULL`, so insert `'{}'` if you have no uniform. | See [Database schema](/docs/guides/database-schema) for every table ESX uses. ## Create the job with SQL [#create-the-job-with-sql] This is the most common way and works on any ESX Legacy server. <Steps> <Step> ### Insert the job and its grades [#insert-the-job-and-its-grades] Run this in HeidiSQL, phpMyAdmin or your MySQL client against the ESX database. The example creates a `bahamas` nightclub job with four ranks. ```sql title="bahamas.sql" INSERT INTO `jobs` (`name`, `label`, `type`, `whitelisted`) VALUES ('bahamas', 'Bahama Mamas', 'civ', 1); INSERT INTO `job_grades` (`job_name`, `grade`, `name`, `label`, `salary`, `skin_male`, `skin_female`) VALUES ('bahamas', 0, 'recruit', 'Recruit', 20, '{}', '{}'), ('bahamas', 1, 'barman', 'Barman', 40, '{}', '{}'), ('bahamas', 2, 'manager', 'Manager', 60, '{}', '{}'), ('bahamas', 3, 'boss', 'Owner', 100, '{}', '{}'); ``` Keep `name` lowercase without spaces. `jobs.name` is `varchar(50)`, but `users.job` is only `varchar(20)`, so job names longer than 20 characters cannot be saved on the player. </Step> <Step> ### Reload the jobs [#reload-the-jobs] ESX keeps jobs in memory, so new rows are not visible until you reload them. In the server console (or in game as an admin) run: ```bash refreshjobs ``` `/refreshjobs` calls `ESX.RefreshJobs()`, which reloads both tables and fires the `esx:jobsRefreshed` server event. A server restart does the same. </Step> <Step> ### Assign the job to a player [#assign-the-job-to-a-player] ```bash setjob 1 bahamas 0 ``` The arguments are `[playerId] [job] [grade]`. In game you can use `me` as the player ID: `/setjob me bahamas 3`. ESX checks the job and grade with `ESX.DoesJobExist` and rejects unknown combinations. Who can run `/setjob` is controlled by `Config.CommandPermissions` (owner and admin by default), see [Admin permissions](/docs/guides/admin-permissions). </Step> <Step> ### Check the result [#check-the-result] The `/job` command prints the player's job, grade label and duty status. It runs on the server, so the output appears in the **server console**, not in the player's F8 console. To check from a script, read `xPlayer.getJob()` on the server or `ESX.PlayerData.job` on the client. </Step> </Steps> ## Create a job from a script with ESX.CreateJob [#create-a-job-from-a-script-with-esxcreatejob] If your resource ships its own job, create it at runtime instead of asking server owners to run SQL. `ESX.CreateJob` inserts the job and its grades in one transaction and adds them to `ESX.Jobs` immediately, so no `/refreshjobs` is needed. ```lua ESX.CreateJob(name, label, grades, jobType) ``` | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Internal job name. Required. | | `label` | `string` | Display name. Required. | | `grades` | `table` | List of grades. Each entry has `grade`, `name`, `label`, `salary` and optional `skin_male` / `skin_female` tables. Required. | | `jobType` | `string?` | Job type. Defaults to `civ` when it is not a string. | **Returns:** `boolean`, `true` when the rows were inserted. ```lua title="server/main.lua" ESX = exports['es_extended']:getSharedObject() CreateThread(function() if ESX.DoesJobExist('bahamas', 0) then return end local ok = ESX.CreateJob('bahamas', 'Bahama Mamas', { { grade = 0, name = 'recruit', label = 'Recruit', salary = 20 }, { grade = 1, name = 'barman', label = 'Barman', salary = 40 }, { grade = 2, name = 'manager', label = 'Manager', salary = 60 }, { grade = 3, name = 'boss', label = 'Owner', salary = 100 }, }, 'civ') if not ok then print('[bahamas] could not create the job, see the es_extended log above') end end) ``` Behavior worth knowing: - If the job already exists, only grades that do not exist yet are inserted. The existing label and type are kept. - If the job and all grades already exist, it prints an error and returns `false`. Guard the call with `ESX.DoesJobExist` as above to keep the console clean. - On success it fires the server event `esx:jobCreated` with `(name, job)`. - `ESX.DoesJobExist` and `ESX.GetJobs` wait until jobs are loaded, so call them from a thread, not at the top level of a file. ### Reload a single job [#reload-a-single-job] After changing a salary or label directly in the database, `ESX.RefreshJob(jobName)` reloads just that job and updates every online player who has it (`xPlayer.refreshJob()`). If the job no longer exists or has no grades, online players with that job are set to `unemployed`. ```lua title="server/main.lua" ESX.RefreshJob('bahamas') ``` ## Change a player's job in code [#change-a-players-job-in-code] ```lua title="server/main.lua" local xPlayer = ESX.GetPlayerFromId(source) xPlayer.setJob('bahamas', 1) -- on duty by default (Config.DefaultJobDuty) xPlayer.setJob('bahamas', 1, false) -- explicitly off duty ``` `setJob` triggers the server event `esx:setJob` with `(source, job, lastJob)` and the client event `esx:setJob` with `(job, lastJob)`. It also writes the job to the player's state bag (`Player(source).state.job`). React to job changes on the client like this: ```lua title="client/main.lua" RegisterNetEvent('esx:setJob', function(job, lastJob) if job.name == 'bahamas' then print(('Now working as %s (%s)'):format(job.grade_label, job.label)) end end) ``` The job table contains `name`, `label`, `type`, `onDuty`, `grade`, `grade_name`, `grade_label`, `grade_salary`, `skin_male` and `skin_female`. Full method list: [xPlayer](/docs/es_extended/server/xplayer). ## Add a society (company account and boss menu) [#add-a-society-company-account-and-boss-menu] A society gives the job a shared bank account, employee management (hire, fire, promote) and optionally money washing. It needs [esx_society](/docs/addons/esx_society) and [esx_addonaccount](/docs/addons/esx_addonaccount), started **before** your job resource. <Steps> <Step> ### Create the shared account [#create-the-shared-account] ```sql title="bahamas_society.sql" INSERT INTO `addon_account` (`name`, `label`, `shared`) VALUES ('society_bahamas', 'Bahama Mamas', 1); ``` You do not need to insert into `addon_account_data`: esx_addonaccount creates the missing row for shared accounts (with `0` money) when it starts. Official jobs also add rows to `datastore` and `addon_inventory` with the same name; you only need those if your script uses a society stash or datastore. </Step> <Step> ### Register the society on the server [#register-the-society-on-the-server] ```lua title="server/main.lua" TriggerEvent('esx_society:registerSociety', 'bahamas', 'Bahama Mamas', 'society_bahamas', 'society_bahamas', 'society_bahamas', { type = 'private' }) ``` The arguments are `name, label, account, datastore, inventory, data`. `name` must equal the job name. The same function is exported: `exports.esx_society:registerSociety(...)`. </Step> <Step> ### Open the boss menu from the client [#open-the-boss-menu-from-the-client] ```lua title="client/main.lua" TriggerEvent('esx_society:openBossMenu', 'bahamas', function() ESX.CloseContext() end, { wash = false }) ``` Only grades whose `name` is listed in esx_society's `Config.BossGrades` can use the menu. The default is `['boss'] = true`, which is why the top grade above is named `boss`. </Step> </Steps> With `Config.EnableSocietyPayouts = true` in es_extended, salaries are paid from the society account instead of being created out of thin air. See [Money and accounts](/docs/guides/money-and-accounts). ## Common issues [#common-issues] <Accordions> <Accordion title="'Ignoring job ... due to no job grades found'"> The job row exists but `job_grades` has no rows with that `job_name`. Check spelling and case; `job_name` must match `jobs.name` exactly. </Accordion> <Accordion title="/setjob says the job is invalid"> Either the grade number does not exist for that job or ESX has not reloaded jobs yet. Run `refreshjobs` and try again. </Accordion> <Accordion title="Players lose their job after a restart"> On load, ESX validates the saved `users.job` and `users.job_grade`. If you renamed or deleted a job or grade, those players are reset to `unemployed` grade `0`. A missing `unemployed` grade 0 is replaced by a built-in fallback, but keep it in the database. </Accordion> <Accordion title="The boss menu does not open or shows no money"> esx_society must be started before your job resource, the society must be registered with the same name as the job, the `society_<job>` addon account must exist with `shared = 1`, and the player's grade name must be in `Config.BossGrades`. </Accordion> </Accordions> ## Related [#related] - [xPlayer reference](/docs/es_extended/server/xplayer) - [esx_society](/docs/addons/esx_society) - [Money and accounts](/docs/guides/money-and-accounts) - [Database schema](/docs/guides/database-schema) - [Admin permissions](/docs/guides/admin-permissions) # Database Schema (/docs/guides/database-schema) ESX Legacy stores all persistent data in a MySQL or MariaDB database accessed through `oxmysql`. The complete schema lives in [`[SQL]/legacy.sql`](https://github.com/esx-framework/esx_core/blob/main/%5BSQL%5D/legacy.sql) in esx_core, and each core resource and addon ships its own `.sql` file with the tables it needs. This page lists every table, its columns and which resource reads or writes it. ## How the schema is created [#how-the-schema-is-created] There are three ways tables end up in your database: 1. **`legacy.sql`**: the full schema with seed data (jobs, items, vehicles, fines). txAdmin's ESX recipe imports it for you. 2. **Per-resource `.sql` files**: for example `esx_billing.sql` or `esx_identity.sql`. Useful if you install resources one by one. 3. **Automatic migrations**: since 1.16.0, `es_extended` loads the **SQL catalog** from `esx_lib` on startup. It creates missing tables, adds missing columns and inserts missing seed rows for `es_extended` and the official resources (identity, multicharacter, skin, addonaccount, billing, banking, garage, jobs and more). It records what it did in `esx_catalog_imports`. Several addons (`esx_billing`, `esx_banking`, `esx_garage`, `esx_addonaccount`, `esx_license`, `esx_whitelist`, `esx_property`) also run their own migrations. <Callout type="info" title="Seed rows are only inserted when missing"> The catalog inserts a seed row only if no row with the same key exists (`jobs.name`, `job_grades (job_name, grade)`, `items.name` and so on). It does not overwrite your own changes to labels or salaries. </Callout> ## Core tables (es_extended) [#core-tables-es_extended] ### users [#users] One row per character. Created by `es_extended`; several resources add columns. | Column | Type | Default | Added by | Notes | | --- | --- | --- | --- | --- | | `identifier` | `varchar(60)` | none | es_extended | **Primary key.** License without prefix, or `char<N>:<license>` with multicharacter | | `ssn` | `varchar(11)` | none | es_extended | Unique (`unique_ssn`). Generated per character, read with `xPlayer.getSSN()` | | `accounts` | `longtext` | `NULL` | es_extended | JSON object of account name to balance | | `group` | `varchar(50)` | `'user'` | es_extended | Permission group, see [Admin permissions](/docs/guides/admin-permissions) | | `inventory` | `longtext` | `NULL` | es_extended | JSON of item name to count (default inventory) | | `job` | `varchar(20)` | `'unemployed'` | es_extended | Must match `jobs.name` | | `job_grade` | `int(11)` | `0` | es_extended | Must match `job_grades.grade` | | `loadout` | `longtext` | `NULL` | es_extended | JSON weapons list (default inventory) | | `metadata` | `longtext` | `NULL` | es_extended | JSON, read with `xPlayer.getMeta()` | | `position` | `longtext` | `NULL` | es_extended | JSON coords and heading | | `firstname`, `lastname` | `varchar(16)` | `NULL` | esx_identity | | | `dateofbirth` | `varchar(10)` | `NULL` | esx_identity | | | `sex` | `varchar(1)` | `NULL` | esx_identity | `m` or `f` | | `height` | `int(11)` | `NULL` | esx_identity | | | `skin` | `longtext` | `NULL` | esx_skin | JSON skinchanger data | | `status` | `longtext` | `NULL` | esx_status | Hunger, thirst and other statuses | | `is_dead` | `tinyint(1)` | `0` | esx_ambulancejob / esx_death | Used when `Config.SaveDeathStatus` is on | | `death_time` | `bigint` | `NULL` | esx_death | Added by `esx_death.sql` | | `id` | `int(11)` | auto increment | legacy.sql | Unique key `id` | | `disabled` | `tinyint(1)` | `0` | esx_multicharacter | Disabled character slots | | `last_property` | `longtext` | `NULL` | esx_property | Last property the player was in | | `created_at` | `timestamp` | `CURRENT_TIMESTAMP` | legacy.sql | | | `last_seen` | `timestamp` | `NULL`, `ON UPDATE CURRENT_TIMESTAMP` | legacy.sql | Updated whenever the row changes | | `phone_number` | `varchar(20)` | `NULL` | legacy.sql | Not read by es_extended; available for phone resources | | `pincode` | `int` | `NULL` | esx_banking | ATM PIN | Example `accounts` value with the default `Config.Accounts`: ```json { "bank": 50000, "money": 0, "black_money": 0 } ``` ### jobs [#jobs] | Column | Type | Default | Notes | | --- | --- | --- | --- | | `name` | `varchar(50)` | none | **Primary key**, used in `users.job` and `xPlayer.setJob()` | | `label` | `varchar(50)` | `NULL` | Display name | | `type` | `varchar(50)` | `'civ'` | Job type, filter with `ESX.GetJobs(type)`. Seeds use `civ`, `leo`, `ems`, `mechanic` | | `whitelisted` | `tinyint(1)` | `0` | Used by `esx_joblisting` to hide jobs from the job center | ### job_grades [#job_grades] | Column | Type | Default | Notes | | --- | --- | --- | --- | | `id` | `int(11)` | auto increment | **Primary key** | | `job_name` | `varchar(50)` | `NULL` | References `jobs.name` (no foreign key) | | `grade` | `int(11)` | none | Grade number, starting at 0 | | `name` | `varchar(50)` | none | Internal grade name. `boss` grants the society boss menu by default | | `label` | `varchar(50)` | none | Display name | | `salary` | `int(11)` | none | Paycheck amount per `Config.PaycheckInterval` | | `skin_male`, `skin_female` | `longtext` | none | JSON uniform, `'{}'` if unused. Written by the esx_society uniform manager | A job without any grade is ignored by `ESX.RefreshJobs()` with a warning. See [Create a job](/docs/guides/create-a-job). ### items [#items] | Column | Type | Default | Notes | | --- | --- | --- | --- | | `name` | `varchar(50)` | none | **Primary key** | | `label` | `varchar(50)` | none | Display name | | `weight` | `int(11)` | `1` | Counted against `Config.MaxWeight` | | `rare` | `tinyint(4)` | `0` | Stored and sent to the client; no core gameplay effect | | `can_remove` | `tinyint(4)` | `1` | `0` blocks dropping and giving the item | Not used when `ox_inventory` runs; items then come from `ox_inventory/data/items.lua`. See [Add items](/docs/guides/add-items). ### owned_vehicles [#owned_vehicles] Created by `esx_vehicleshop.sql` and `legacy.sql`; extended by `esx_garage`. Used by es_extended (extended vehicles), esx_garage, esx_vehicleshop, esx_policejob, esx_lscustom, esx_boat, esx_property and others. | Column | Type | Default | Added by | Notes | | --- | --- | --- | --- | --- | | `owner` | `varchar(60)` | `NULL` | core | `users.identifier`. Indexed | | `plate` | `varchar(12)` | none | core | **Primary key** | | `vehicle` | `longtext` | `NULL` | core | JSON vehicle properties | | `type` | `varchar(20)` | `'car'` | core | `car`, `boat`, `aircraft`... | | `job` | `varchar(20)` | `NULL` | core | Set for job vehicles | | `stored` | `tinyint(4)` | `0` | core | 1 = in a garage | | `parking` | `varchar(60)` | `NULL` | esx_garage | Garage the vehicle is parked in | | `pound` | `varchar(60)` | `NULL` | esx_garage | Impound location | | `custom_name` | `varchar(50)` | `NULL` | esx_garage | Player-chosen name | | `is_favorite` | `tinyint(1)` | `0` | esx_garage | | | `last_used` | `int` | `NULL` | esx_garage | | | `mileage` | `decimal(10,2)` | `0.00` | esx_garage | | `esx_garage` also creates composite indexes such as `idx_owned_vehicles_owner_plate` and `idx_owned_vehicles_owner_stored_pound_plate`, and tracks its schema version in `esx_garage_migrations`. ### esx_catalog_imports [#esx_catalog_imports] Created by the `esx_lib` SQL catalog. | Column | Type | Notes | | --- | --- | --- | | `resource` | `varchar(64)` | **Primary key**, resource whose seed data was prepared | | `version` | `varchar(32)` | Catalog version, for example `1.16.0` | | `locale` | `varchar(32)` | Locale used for seeded labels | | `updated_at` | `timestamp` | | ## Character and identity tables [#character-and-identity-tables] ### multicharacter_slots [#multicharacter_slots] Used by `esx_multicharacter` to give individual players more character slots. | Column | Type | Notes | | --- | --- | --- | | `identifier` | `varchar(60)` | **Primary key**, license without the `char` prefix | | `slots` | `int(11)` | Number of slots. Indexed | ### whitelist [#whitelist] | Column | Type | Notes | | --- | --- | --- | | `identifier` | `varchar(60)` | **Primary key** | This table is still in `legacy.sql`, but the current `esx_whitelist` resource uses the two tables below instead. ### esx_whitelist_config and esx_whitelist_identifiers [#esx_whitelist_config-and-esx_whitelist_identifiers] Created by `esx_whitelist` (`whitelist.sql` or automatically on start). | Table | Column | Type | Notes | | --- | --- | --- | --- | | `esx_whitelist_config` | `setting_key` | `varchar(64)` | **Primary key** | | | `setting_value` | `longtext` | | | | `updated_at` | `timestamp` | Auto-updated | | `esx_whitelist_identifiers` | `identifier` | `varchar(128)` | Primary key with `type` | | | `type` | `varchar(32)` | Default `'whitelist'`, indexed | | | `created_at` | `timestamp` | | ## Society and shared data tables [#society-and-shared-data-tables] ### addon_account and addon_account_data [#addon_account-and-addon_account_data] Used by `esx_addonaccount`, `esx_society` (society money) and `esx_vehicleshop`. Society accounts are named `society_<job>`. | Table | Column | Type | Notes | | --- | --- | --- | --- | | `addon_account` | `name` | `varchar(60)` | **Primary key**, e.g. `society_police` | | | `label` | `varchar(100)` | | | | `shared` | `int(11)` | `1` = one account for everyone (society), `0` = one per player | | `addon_account_data` | `id` | `int(11)` | **Primary key**, auto increment | | | `account_name` | `varchar(100)` | Indexed | | | `money` | `int(11)` | Balance | | | `owner` | `varchar(60)` | `NULL` for shared accounts, identifier otherwise. Indexed | Unique key `(account_name, owner)`. The 2026-10-02 hardening adds a generated `owner_key` column (`COALESCE(owner, '')`) with a unique key, so shared accounts cannot be duplicated either. ### addon_inventory and addon_inventory_items [#addon_inventory-and-addon_inventory_items] Used by `esx_addoninventory` for society and property stashes (default inventory). | Table | Column | Type | Notes | | --- | --- | --- | --- | | `addon_inventory` | `name` | `varchar(60)` | **Primary key** | | | `label` | `varchar(100)` | | | | `shared` | `int(11)` | Shared or per player | | `addon_inventory_items` | `id` | `int(11)` | **Primary key**, auto increment | | | `inventory_name` | `varchar(100)` | Indexed | | | `name` | `varchar(100)` | Item name | | | `count` | `int(11)` | | | | `owner` | `varchar(60)` | `NULL` for shared. Indexed | ### datastore and datastore_data [#datastore-and-datastore_data] Generic JSON storage, used by `esx_datastore`, `esx_accessories` (`user_mask`, `user_helmet`...), `esx_property` and society resources. | Table | Column | Type | Notes | | --- | --- | --- | --- | | `datastore` | `name` | `varchar(60)` | **Primary key** | | | `label` | `varchar(100)` | | | | `shared` | `int(11)` | | | `datastore_data` | `id` | `int(11)` | **Primary key**, auto increment | | | `name` | `varchar(60)` | Datastore name | | | `owner` | `varchar(60)` | Indexed | | | `data` | `longtext` | JSON | Unique key `(name, owner)`. ### society_moneywash [#society_moneywash] Used by `esx_society` for money washing. | Column | Type | Notes | | --- | --- | --- | | `id` | `int(11)` | **Primary key**, auto increment | | `identifier` | `varchar(60)` | Indexed | | `society` | `varchar(60)` | | | `amount` | `int(11)` | | Current `esx_society` also creates `society_moneywash_operations` (`operation_id` primary key, `identifier`, `society`, `amount`, `state`, `created_at`) on start and moves pending rows from `society_moneywash` into it. ## Economy tables [#economy-tables] ### billing [#billing] Used by `esx_billing` (and jobs that send bills). | Column | Type | Notes | | --- | --- | --- | | `id` | `int(11)` | **Primary key**, auto increment | | `identifier` | `varchar(60)` | Player who must pay. Indexed | | `sender` | `varchar(60)` | Identifier of the sender | | `target_type` | `varchar(50)` | `player` or `society` | | `target` | `varchar(60)` | Receiving identifier or society account | | `label` | `varchar(255)` | | | `amount` | `int(11)` | | ### billing_payments [#billing_payments] Created by `esx_billing` on start (and by the hardening script). It is a payment journal that makes paying a bill idempotent. | Column | Type | Notes | | --- | --- | --- | | `bill_id` | `int` | **Primary key** | | `operation_id` | `varchar(64)` | Unique | | `identifier`, `sender` | `varchar(60)` | | | `target_type` | `varchar(10)` | | | `target` | `varchar(100)` | | | `label` | `varchar(255)` | | | `amount` | `int` | | | `payment_account` | `varchar(20)` | Account used to pay | | `state` | `enum('claimed','applied')` | Default `claimed` | | `created_at`, `applied_at` | `timestamp` | | Rows stuck in `claimed` should be inspected manually. The hardening script explicitly says not to replay or refund them automatically. ### banking [#banking] Transaction history for `esx_banking`. | Column | Type | Notes | | --- | --- | --- | | `ID` | `int(11)` | **Primary key**, auto increment | | `identifier` | `varchar(46)` in legacy.sql | `esx_banking` widens it to `varchar(60)` on start | | `type` | `varchar(50)` | Deposit, withdraw, transfer... | | `amount` | `int(64)` | | | `time` | `bigint(20)` | Unix time in ms | | `balance` | `int(11)` | Balance after the transaction | | `label` | `varchar(255)` | | Keys: `idx_banking_identifier_time (identifier, time)` and `idx_banking_time (time)`. ### fine_types [#fine_types] Fine catalog for `esx_policejob`. | Column | Type | Notes | | --- | --- | --- | | `id` | `int` | **Primary key**, auto increment | | `label` | `varchar(255)` | | | `amount` | `int` | | | `category` | `int` | Groups fines in the police fine menu. Seed data uses 0 (traffic) to 3 (most severe) | ## License tables [#license-tables] Used by `esx_license`, and through it by `esx_dmvschool`, `esx_weaponshop`, `esx_boat` and `esx_drugs`. | Table | Column | Type | Notes | | --- | --- | --- | --- | | `licenses` | `type` | `varchar(60)` | **Primary key**, e.g. `dmv`, `drive`, `weapon` | | | `label` | `varchar(60)` | | | `user_licenses` | `id` | `int(11)` | **Primary key**, auto increment | | | `type` | `varchar(60)` | License type | | | `owner` | `varchar(60)` | Identifier | Unique key `uq_user_licenses_owner_type (owner, type)` so a player cannot hold the same license twice. ## Vehicle shop tables [#vehicle-shop-tables] All used by `esx_vehicleshop` (`vehicles` is also read by `esx_lscustom`). | Table | Columns | Notes | | --- | --- | --- | | `vehicles` | `name varchar(60)`, `model varchar(60)` (**PK**), `price int(11)`, `category varchar(60)` | Vehicles for sale | | `vehicle_categories` | `name varchar(60)` (**PK**), `label varchar(60)` | Shop categories | | `cardealer_vehicles` | `id int(11)` (**PK**), `vehicle varchar(255)`, `price int(11)` | Car dealer job stock | | `vehicle_sold` | `id int(11)` (**PK**), `client`, `model`, `plate`, `soldby`, `date` (all `varchar(50)`) | Sales log of the car dealer job | | `rented_vehicles` | `vehicle varchar(60)`, `plate varchar(12)` (**PK**), `player_name varchar(255)`, `base_price int`, `rent_price int`, `owner varchar(60)` | Rentals | `esx_vehicleshop` also creates `vehicle_rental_payments` (`plate`, `billing_date` as primary key, `owner`, `amount`, `operation_id` unique, `state`) on start. ## Other tables created by addons [#other-tables-created-by-addons] | Table | Created by | Purpose | | --- | --- | --- | | `esx_garage_migrations` | esx_garage | Applied garage schema migrations (`name`, `version`, `applied_at`) | | `esx_properties` | esx_property | Property definitions as JSON (`id`, `data`) | | `esx_property_migrations` | esx_property | Applied property migrations | | `bans` | esx_adminmenu | Bans with `identifier`, `reason`, `banned_by`, `expires_at` | | `admin_logs` | esx_adminmenu | Admin action log (actor, target, action, payload) | ## Inspecting your own database [#inspecting-your-own-database] ```sql -- Which catalog imports ran SELECT * FROM esx_catalog_imports; -- Jobs that have no grades (ignored by ESX) SELECT j.name FROM jobs j LEFT JOIN job_grades g ON g.job_name = j.name WHERE g.id IS NULL; -- Players whose job no longer exists (reset to unemployed on load) SELECT identifier, job FROM users WHERE job NOT IN (SELECT name FROM jobs); ``` ## Common issues [#common-issues] - **`Unknown column 'ssn'`** on old databases: start `es_extended` 1.16 once, its migration adds and fills `users.ssn`. It stops if duplicate SSNs exist. - **"Economy tables must use InnoDB"**: convert `billing`, `users` and `addon_account_data` with `ALTER TABLE ... ENGINE=InnoDB` after a backup. - **Duplicate key errors in the hardening script**: you have duplicate `user_licenses` or `addon_account_data` rows. Reconcile them by hand; the script never deletes financial rows. - **Editing JSON columns while the server runs**: online players are kept in memory and overwrite your change on the next save. Edit offline players only, or use in-game commands. ## Related [#related] - [Create a job](/docs/guides/create-a-job) - [Add items](/docs/guides/add-items) - [Money and accounts](/docs/guides/money-and-accounts) - [oxmysql with ESX](/docs/guides/oxmysql) - [Performance](/docs/guides/performance) # ESX vs QBCore (/docs/guides/esx-vs-qbcore) ESX Legacy and QBCore are the two most widely used roleplay frameworks for FiveM. Both give you a persistent player object with money, a job and an inventory, both run on Lua and both use `oxmysql` for the database. The real differences are in **how data is stored, how the API looks and which scripts already exist for each**. This page compares them as neutrally as possible so you can choose, or understand what a migration involves. <Callout type="info" title="Scope"> This site documents ESX Legacy 1.16.4 and every ESX statement below is checked against its source code. For QBCore, read the official documentation at [qbcore.net](https://qbcore.net). QBCore changes over time, so verify QBCore specifics there before you make a decision. </Callout> ## Quick comparison [#quick-comparison] | Topic | ESX Legacy | QBCore | | --- | --- | --- | | Core resource | `es_extended` plus `esx_lib` | `qb-core` | | Getting the core object | `exports['es_extended']:getSharedObject()` or `@es_extended/imports.lua` | `exports['qb-core']:GetCoreObject()` | | Player object | `xPlayer` with methods like `xPlayer.addAccountMoney()` | Player object with methods under `Player.Functions` | | Jobs and grades | Database tables `jobs` and `job_grades` | Defined in shared Lua files by default | | Items | Database table `items` (or `ox_inventory` item files) | Defined in shared Lua files by default | | Money | Accounts from `Config.Accounts`: `money`, `bank`, `black_money` | Money types configured in QBCore (cash, bank and others) | | Database driver | `oxmysql` (hard dependency in `fxmanifest.lua`) | `oxmysql` | | OneSync | Required (OneSync Infinity) | See QBCore docs | | License | GPL-3.0 (SPDX header in every ESX file) | See the QBCore repository | ## Architecture [#architecture] ### ESX Legacy [#esx-legacy] ESX Legacy is split into a core and optional resources: - **`es_extended`** is the framework core. It loads players, manages accounts, jobs, groups, the default inventory, commands, callbacks and paychecks. - **`esx_lib`** is a shared library that `es_extended` depends on. It provides callbacks, table and math helpers, points, streaming helpers and the SQL catalog that creates and seeds tables on startup. - **Core resources** like `esx_multicharacter`, `esx_identity`, `esx_skin`, `esx_context` and `esx_notify` handle character creation and UI. - **Addons** like `esx_policejob`, `esx_society`, `esx_billing` and `esx_garage` add gameplay. The design is database-first: jobs, grades and items live in SQL tables. You can add a job with a single `INSERT` and the `/refreshjobs` command, or at runtime with `ESX.CreateJob()`. See [Create a job](/docs/guides/create-a-job) and [Add items](/docs/guides/add-items). ESX refuses to start without OneSync Infinity: the join handler rejects players with "ESX Requires Onesync Infinity to work" when the `onesync` convar is `off` or `legacy`. ### QBCore [#qbcore] QBCore is built around the `qb-core` resource and a family of `qb-*` resources. By default, static data like jobs, items, gangs and vehicles is defined in Lua files inside `qb-core/shared`, and player data is stored in a `players` table. Check [qbcore.net](https://qbcore.net) for the current file layout, because it has changed between versions. ### What this means in practice [#what-this-means-in-practice] | You want to... | ESX Legacy | QBCore | | --- | --- | --- | | Add a job | `INSERT` into `jobs` and `job_grades`, then `/refreshjobs` | Edit the shared jobs file and restart | | Add an item | `INSERT` into `items`, then `/refreshitems` (or edit `ox_inventory` items) | Edit the shared items file (or the inventory resource) | | Change data without a restart | Supported for jobs and items through the refresh commands | Depends on the version and resource | | Edit data from a web panel | Easy, it is all SQL | Requires editing Lua files | Database-first is convenient for admin panels and live changes. File-first is convenient for version control and code review. Neither is wrong. ## API style [#api-style] Both frameworks expose the same concepts with different names. Here is the same task, giving a player money and reading their job, in ESX: ```lua title="server/main.lua" local ESX = exports['es_extended']:getSharedObject() RegisterCommand('payday', function(source) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return end xPlayer.addAccountMoney('bank', 500, 'Payday command') local job = xPlayer.getJob() xPlayer.showNotification(('Paid as %s (%s)'):format(job.label, job.grade_label)) end, false) ``` In QBCore the equivalent uses the QBCore player object and its `Functions` table. If you are porting a script, map each call one by one: | Concept | ESX Legacy (verified) | Notes | | --- | --- | --- | | Get player | `ESX.GetPlayerFromId(source)` | Returns `nil` when the player is not loaded | | Add money | `xPlayer.addAccountMoney(account, amount, reason)` | `addMoney()` is a shortcut for the `money` account | | Remove money | `xPlayer.removeAccountMoney(account, amount, reason)` | Returns `false` if the balance would go negative | | Set job | `xPlayer.setJob(name, grade, onDuty)` | Grade must exist in `job_grades` | | Check item | `xPlayer.getInventoryItem(name)` / `xPlayer.hasItem(name)` | Proxied to `ox_inventory` when it is running | | Server callback | `ESX.RegisterServerCallback(name, function(source, cb, ...) end)` | See [Callbacks](/docs/guides/callbacks) | | Player loaded event | `esx:playerLoaded` | Server: `(playerId, xPlayer, isNew)` | ESX also replicates some player data through state bags: `Player(source).state.job`, `.group` and `.name` are set by `es_extended`, which helps when you write framework-agnostic code. See [Performance](/docs/guides/performance). ## Ecosystem [#ecosystem] ESX has been around since 2017, so a very large number of free and paid scripts target it, including many written for old ESX versions. Most of those still run on ESX Legacy because the classic API (`ESX.GetPlayerFromId`, `xPlayer.addMoney`, `esx:playerLoaded`) is kept. Old scripts usually need small fixes, covered in [Convert old ESX scripts](/docs/guides/convert-old-esx-scripts). QBCore has its own large ecosystem of `qb-*` and third-party resources. Many commercial script authors ship both an ESX and a QBCore version, or a single script with a framework bridge. The overextended resources (`oxmysql`, `ox_lib`, `ox_inventory`, `ox_target`) work with both frameworks. ESX Legacy detects `ox_inventory` automatically. See [ox_inventory with ESX](/docs/guides/ox-inventory). <Callout type="warn" title="Do not mix frameworks"> `es_extended` contains a resource guard that stops `qb-core` (and old resources like `essentialmode` and `es_admin2`) when it detects them and prints "WE STOPPED A RESOURCE THAT WILL BREAK ESX". Pick one framework per server. </Callout> ## Performance [#performance] You will find many claims online that one framework is "faster". We do not repeat any numbers here because we know of no neutral, reproducible benchmark. In practice: - An idle framework core costs very little. What hurts servers is scripts with `Wait(0)` loops, unindexed queries and chatty events. - Both frameworks can run large servers when the installed scripts are well written. - Measure your own server with `resmon` and the FiveM profiler instead of trusting comparisons. ESX Legacy has some built-in measures, for example paychecks are paid in batches (`Config.PaycheckBatchSize`, `Config.PaycheckBatchDelay`, `Config.PaycheckJitter`) and players are saved every 10 minutes in one background thread. Details are in [Performance](/docs/guides/performance). ## Learning curve [#learning-curve] - **ESX** has the most tutorials and forum answers of any FiveM framework, but many of them are outdated (`esx:getSharedObject` events, `mysql-async`). Learning to recognize old patterns is part of learning ESX. The core API is small and consistent: one `ESX` object, one `xPlayer` object. - **QBCore** documentation and tutorials are mostly newer. If you are used to its `Functions` naming, ESX will look different but conceptually similar. If your team already knows one framework, that is usually the strongest argument for it. ## Migrating between frameworks [#migrating-between-frameworks] There is no official converter in ESX Legacy for QBCore data. A migration is a project, not a setting. Plan it like this: <Steps> <Step> ### Inventory your scripts [#inventory-your-scripts] List every resource and check whether an ESX version exists. Scripts that only exist for the other framework must be rewritten or replaced. </Step> <Step> ### Map your data [#map-your-data] Player money, jobs, inventories and vehicles are stored in different tables and formats. In ESX, player data is in `users` (accounts and inventory are JSON in `accounts` and `inventory`) and vehicles in `owned_vehicles`. See the [Database schema](/docs/guides/database-schema). </Step> <Step> ### Write and test a one-off migration [#write-and-test-a-one-off-migration] Write a script or SQL that converts the data, and run it on a copy of the database. Check identifiers: ESX stores the license without the `license:` prefix, and with `esx_multicharacter` as `char1:<license>`. </Step> <Step> ### Run both servers side by side [#run-both-servers-side-by-side] Keep the old server running until the new one is tested with real players. Never run both frameworks in one server. </Step> </Steps> ## How to decide [#how-to-decide] - Choose **ESX Legacy** if you want database-driven jobs and items, need a specific ESX script, or your developers already know ESX. - Choose **QBCore** if the scripts you need exist only for QBCore or your team already knows it. Start at [qbcore.net](https://qbcore.net). - If you are undecided, list the 10 scripts your server cannot live without and check which framework they support. ## Common issues [#common-issues] - **"WE STOPPED A RESOURCE THAT WILL BREAK ESX"**: you have `qb-core` or another conflicting resource in your `server.cfg`. Remove it. - **A QBCore script throws `attempt to index a nil value (global 'QBCore')`** on an ESX server: the script needs a QBCore core. Use its ESX version or a bridge. - **Players kicked with "ESX Requires Onesync Infinity"**: set `onesync on` (txAdmin does this for you) before starting ESX. ## Related [#related] - [Introduction to ESX Legacy](/docs) - [Installation](/docs/getting-started/installation) - [Convert old ESX scripts](/docs/guides/convert-old-esx-scripts) - [xPlayer reference](/docs/es_extended/server/xplayer) - [Database schema](/docs/guides/database-schema) # Money and Accounts (/docs/guides/money-and-accounts) ESX Legacy stores a player's money in **accounts**: `money` (cash), `bank` and `black_money` by default. On the server you read and change them through the [xPlayer](/docs/es_extended/server/xplayer) object, for example `xPlayer.addAccountMoney('bank', 500, 'Job reward')`, and you configure which accounts exist and how much new players start with in `es_extended/shared/config/main.lua`. ## Overview [#overview] | Account | Meaning | Notes | | --- | --- | --- | | `money` | Cash on hand | Shortcut methods `getMoney`, `addMoney`, `removeMoney`, `setMoney` | | `bank` | Bank balance | `transferable = false`, receives paychecks | | `black_money` | Dirty money | Used by drug and money-wash scripts | Accounts are saved as JSON in the `users.accounts` column, for example `{"bank":50000,"money":0,"black_money":0}`. ESX saves players every 10 minutes and when they disconnect. Only the server can change money; clients receive updates through the `esx:setAccountMoney` event. ## Configure accounts [#configure-accounts] ```lua title="es_extended/shared/config/main.lua" Config.Accounts = { bank = { label = TranslateCap("account_bank"), round = true, transferable = false, }, black_money = { label = TranslateCap("account_black_money"), round = true, }, money = { label = TranslateCap("account_money"), round = true, }, } Config.StartingAccountMoney = { bank = 50000 } ``` | Key | Type | Description | | --- | --- | --- | | `label` | `string` | Display name. The defaults are translated through the locale files. | | `round` | `boolean` | Round amounts to whole numbers. Defaults to `true` when omitted. | | `transferable` | `boolean` | `false` stops players from giving or dropping this account through the default inventory. Defaults to transferable. | ### Change the starting money [#change-the-starting-money] `Config.StartingAccountMoney` is used when a character is created, and as the fallback value for any account a loading player does not have yet. New players get no cash by default. To give them some: ```lua title="es_extended/shared/config/main.lua" Config.StartingAccountMoney = { money = 500, bank = 5000 } ``` Existing players are not affected unless the account is new to them. ## Read and change money (server) [#read-and-change-money-server] All account methods take the account name as a string. Amounts must be numbers. ```lua xPlayer.getAccount(accountName) -- { name, money, label, round, index } or nil xPlayer.getAccounts(minimal) -- list, or { name = money } when minimal is true xPlayer.addAccountMoney(accountName, money, reason) -- true on success xPlayer.removeAccountMoney(accountName, money, reason) xPlayer.setAccountMoney(accountName, money, reason) xPlayer.getMoney() / addMoney(money, reason) / removeMoney(money, reason) / setMoney(money) ``` | Name | Type | Description | | --- | --- | --- | | `accountName` | `string` | `money`, `bank`, `black_money` or a custom account. Case-insensitive lookup. | | `money` | `number` | Amount. Must be greater than 0 for add/remove, at least 0 for set. | | `reason` | `string?` | Free text passed to the money events, useful for logging. | ### Return values and errors [#return-values-and-errors] - `removeAccountMoney` returns `false` when the balance would go negative or when rounding turns the amount into `0`. It never allows negative balances. - `addAccountMoney` returns `false` if the new balance would overflow. - An **unknown account name**, a non-number amount, or an amount of `0` or less raises a Lua **error** with the default inventory. Validate input first, especially values that came from a client. ### Example: a safe purchase [#example-a-safe-purchase] ```lua title="server/main.lua" ESX = exports['es_extended']:getSharedObject() local PRICE <const> = 250 RegisterNetEvent('coffee:buy', function() local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return end -- Price is decided on the server, never sent by the client if not xPlayer.removeAccountMoney('money', PRICE, 'Coffee shop') then return xPlayer.showNotification('Not enough cash') end xPlayer.addInventoryItem('coffee', 1) end) ``` Removing the money first and checking the boolean is enough to prevent the classic "buy with no money" exploit. Do not trust a price, account or amount sent by the client; see [Callbacks](/docs/guides/callbacks) for validating client input. ### Example: pay from bank, fall back to cash [#example-pay-from-bank-fall-back-to-cash] ```lua title="server/main.lua" local function charge(xPlayer, amount, reason) if xPlayer.removeAccountMoney('bank', amount, reason) then return 'bank' end if xPlayer.removeAccountMoney('money', amount, reason) then return 'money' end return nil end ``` ## Read money on the client [#read-money-on-the-client] The client gets a copy of the accounts in `ESX.PlayerData.accounts`. Use it for display only. ```lua title="client/main.lua" local bank = ESX.GetAccount('bank') if bank then print(('Bank balance: $%s'):format(ESX.Math.GroupDigits(bank.money))) end RegisterNetEvent('esx:setAccountMoney', function(account) print(('%s is now %s'):format(account.name, account.money)) end) ``` ## Money events [#money-events] | Event | Side | Arguments | | --- | --- | --- | | `esx:addAccountMoney` | Server | `source, accountName, money, reason` | | `esx:removeAccountMoney` | Server | `source, accountName, money, reason` | | `esx:setAccountMoney` | Server | `source, accountName, money, reason` | | `esx:setAccountMoney` | Client | `account` (the full account table) | A simple server-side money log: ```lua title="server/logs.lua" AddEventHandler('esx:removeAccountMoney', function(playerId, accountName, amount, reason) if amount >= 100000 then print(('[money] %s removed %s from %s (%s)'):format(playerId, amount, accountName, reason)) end end) ``` ## Add a custom account [#add-a-custom-account] <Steps> <Step> ### Add the account to Config.Accounts [#add-the-account-to-configaccounts] ```lua title="es_extended/shared/config/main.lua" Config.Accounts = { bank = { label = TranslateCap("account_bank"), round = true, transferable = false }, black_money = { label = TranslateCap("account_black_money"), round = true }, money = { label = TranslateCap("account_money"), round = true }, crypto = { label = "Crypto", round = false, transferable = false }, } ``` Use a plain string label unless you also add a locale key. </Step> <Step> ### Optionally give a starting balance [#optionally-give-a-starting-balance] ```lua title="es_extended/shared/config/main.lua" Config.StartingAccountMoney = { bank = 50000, crypto = 0 } ``` </Step> <Step> ### Restart the server [#restart-the-server] When players load, ESX builds their account list from `Config.Accounts`. Existing players get the new account with the starting value (or `0`), and it is saved into `users.accounts` on the next save. Now `xPlayer.addAccountMoney('crypto', 0.25, 'Mining')` works, and `/giveaccountmoney` accepts `crypto`. </Step> </Steps> <Callout type="warn" title="ox_inventory"> With ox_inventory, accounts listed in the `inventory:accounts` convar (default `["money", "black_money"]`) are stored as **items** in the ox inventory, not in `users.accounts`. ESX routes the account methods to ox_inventory and returns `false` instead of raising an error on invalid input. Only add a custom account to that convar if it should be a physical item. See [ox_inventory](/docs/guides/ox-inventory). </Callout> ## Paychecks [#paychecks] Salaries come from the player's job grade (`job_grades.salary`) and are paid into `bank`. | Config key | Default | Description | | --- | --- | --- | | `Config.EnablePaycheck` | `true` | Turn paychecks on or off | | `Config.PaycheckInterval` | `7 * 60000` | Interval in milliseconds | | `Config.OffDutyPaycheckMultiplier` | `0.5` | Off-duty players get this share of their salary | | `Config.EnableSocietyPayouts` | `false` | Pay salaries from the job's society account (needs esx_society) | | `Config.LogPaycheck` | `false` | Log paychecks to Discord | | `Config.PaycheckBatchSize`, `PaycheckBatchDelay`, `PaycheckJitter` | `100`, `250`, `30000` | Spread payouts on big servers | With society payouts enabled, a player whose company cannot cover the salary gets a "company has no money" notification and is not paid. Jobs without a registered society are paid normally. `xPlayer.togglePaycheck(false)` pauses paychecks for one player. ## Society (company) money [#society-company-money] Company money lives in `esx_addonaccount` shared accounts named `society_<job>`, not on the player. Use the `GetSharedAccount` export on the server: ```lua title="server/main.lua" local account = exports.esx_addonaccount:GetSharedAccount('society_police') if account then account.addMoney(5000) print(('Police funds: %s'):format(account.money)) end ``` The account object has `addMoney`, `removeMoney` and `setMoney`. Setup is described in [Create a job](/docs/guides/create-a-job#add-a-society-company-account-and-boss-menu) and on the [esx_addonaccount](/docs/addons/esx_addonaccount) page. ## Common issues [#common-issues] <Accordions> <Accordion title="'Tried To Set Add To Invalid Account ...'"> The account name is not in `Config.Accounts`. Check spelling; cash is `money`, not `cash`. </Accordion> <Accordion title="'... To An Invalid Number'"> The amount was `0`, negative, `nil` or a string. Convert with `tonumber` and check `> 0` before calling. </Accordion> <Accordion title="Changes in the database are overwritten"> The player was online. ESX holds accounts in memory and saves them back. Change online players through xPlayer methods or the admin commands. </Accordion> <Accordion title="Decimal amounts become whole numbers"> `round = true` rounds every amount. Set `round = false` for accounts that need decimals. </Accordion> </Accordions> ## Related [#related] - [xPlayer reference](/docs/es_extended/server/xplayer) - [Main config](/docs/es_extended/config) - [Create a job](/docs/guides/create-a-job) - [esx_banking](/docs/addons/esx_banking) - [Callbacks](/docs/guides/callbacks) # Using ox_inventory (/docs/guides/ox-inventory) ESX Legacy works with [ox_inventory](https://github.com/overextended/ox_inventory) out of the box. Start ox_inventory after `es_extended`, and es_extended detects it, switches to its built-in ox bridge and forwards the usual `xPlayer` inventory and cash functions to ox_inventory. You do not edit `Config.CustomInventory`. Items, weapons and stashes are then managed by ox_inventory, not by ESX. ## Overview [#overview] - Bridge files: `es_extended/server/bridge/inventory/oxinventory.lua` and `es_extended/server/classes/overrides/oxinventory.lua` - Detection: `shared/config/main.lua`, below the "DO NOT CHANGE" line - Requires: `oxmysql`, `ox_lib` (required by ox_inventory itself), `esx_lib`, `es_extended` - Official ox_inventory docs: [overextended.dev/ox_inventory](https://overextended.dev/ox_inventory) ## How ESX detects ox_inventory [#how-esx-detects-ox_inventory] The detection is one block at the bottom of the main config: ```lua title="es_extended/shared/config/main.lua" if GetResourceState("ox_inventory") ~= "missing" then Config.CustomInventory = "ox" end Config.EnableDefaultInventory = Config.CustomInventory == false -- Display the default Inventory ( F2 ) ``` So ESX switches to ox mode whenever the `ox_inventory` folder **exists** in your resources, even if you never `ensure` it. In ox mode, `es_extended` also: - sets `Config.OxInventory = true` (a backwards-compatibility flag that older scripts still read) - sets the replicated convars `inventory:framework` to `esx` and `inventory:weight` to `Config.MaxWeight * 1000` - skips `ESX.RefreshItems()` at startup and loads the item list from ox_inventory instead - does not register the ESX inventory commands (`/giveitem`, `/giveweapon`, `/giveammo`, `/giveweaponcomponent`, `/refreshitems`, `/clearinventory`, `/clearloadout`) - disables the default F2 inventory <Callout type="warn" title="Remove the folder if you do not use it"> If you downloaded ox_inventory but decided not to use it, delete or rename the folder. A folder that exists but is not started still puts ESX into ox mode, and players will have no working inventory. </Callout> ## Set up ox_inventory with ESX [#set-up-ox_inventory-with-esx] <Steps> <Step> ### Install the dependencies [#install-the-dependencies] Download the latest releases of `ox_lib` and `ox_inventory` from Overextended (use the release zip, not the source code, because the UI needs to be built). Put them in your resources folder. `oxmysql`, `esx_lib` and `es_extended` are already part of an ESX install. </Step> <Step> ### Set the start order [#set-the-start-order] ox_inventory loads its ESX bridge when it starts, so `es_extended` must already be running. ```bash title="server.cfg" ensure oxmysql ensure ox_lib ensure esx_lib ensure es_extended ensure ox_inventory ensure [core] ensure [esx_addons] ``` If you use `ensure [core]` for all core resources, keep `es_extended` above it as in the txAdmin recipe and place `ox_inventory` right after `es_extended`. </Step> <Step> ### Set the convars [#set-the-convars] ESX already replicates `inventory:framework` and `inventory:weight`. Set the rest yourself, for example which ESX accounts are handled as items: ```bash title="server.cfg" setr inventory:framework "esx" setr inventory:accounts '["money", "black_money"]' ``` Accounts listed in `inventory:accounts` live inside the ox_inventory as items. `bank` normally stays an ESX account. The ESX bridge always treats `black_money` as an inventory account, even if you leave it out of the convar. See the ox_inventory docs for all other convars (slots, drop distance, police jobs and so on). </Step> <Step> ### Define your items [#define-your-items] Add items to `ox_inventory/data/items.lua`. The ESX `items` table is not used in ox mode. ```lua title="ox_inventory/data/items.lua" ['coffee'] = { label = 'Coffee', weight = 200, stack = true, close = true, }, ``` Weights in ox_inventory are in grams. That is why ESX multiplies `Config.MaxWeight` (24 by default) by 1000 when it sets `inventory:weight`. </Step> <Step> ### Restart and check [#restart-and-check] Restart the whole server (not only one resource). In the server console you should see es_extended initialize without errors and ox_inventory report that it loaded the ESX framework. Join and open the ox inventory with its default key. </Step> </Steps> ## What changes for your scripts [#what-changes-for-your-scripts] The xPlayer API stays the same for the common calls. The ox override routes them to ox_inventory exports: | xPlayer method | In ox mode | | --- | --- | | `getInventoryItem(name, metadata)` | `ox_inventory` `GetItem` | | `addInventoryItem(name, count, metadata, slot)` | `AddItem` | | `removeInventoryItem(name, count, metadata, slot)` | `RemoveItem` | | `setInventoryItem(name, count, metadata)` | `SetItem` | | `canCarryItem(name, count, metadata)` | `CanCarryItem` | | `canSwapItem(...)` | `CanSwapItem` | | `setMaxWeight(weight)` | `SetMaxWeight` | | `hasItem(name, metadata)` | returns `item, count` or `false` | | `getInventory(minimal)` | ox slots; minimal returns `{ name, count, slot, metadata }` entries | | `getLoadout()` | always `{}` | | `addWeapon`, `removeWeapon`, `addWeaponAmmo` and other weapon methods | do nothing | | `hasWeapon`, `hasWeaponComponent` | always `false` | | `addAccountMoney` / `removeAccountMoney` / `setAccountMoney` | item change for accounts in `inventory:accounts`, normal ESX account otherwise | Note the extra `metadata` and `slot` parameters, which exist only in ox mode. Code written for the default ESX inventory keeps working because the extra parameters are optional. ```lua title="server/main.lua" local xPlayer = ESX.GetPlayerFromId(source) if xPlayer.canCarryItem('coffee', 1) then xPlayer.addInventoryItem('coffee', 1) end -- Works in both modes: hasItem returns false when the player has none if xPlayer.hasItem('lockpick') then -- ... end ``` ### Money with ox_inventory [#money-with-ox_inventory] With the default `inventory:accounts`, cash (`money`) and `black_money` are inventory items. `xPlayer.addMoney`, `removeMoney` and `getAccount('money')` still work because ESX syncs the item count back into the player's accounts. Unlike the default ESX mode, the ox versions of the account methods return `false` instead of throwing an error for an invalid account name or amount. Check the return value. See [Money and accounts](/docs/guides/money-and-accounts). ### Usable items [#usable-items] `ESX.RegisterUsableItem` still stores the handler in ox mode. How an item is used, and whether it is removed, is decided by ox_inventory's item definition (`consume`, `client`, `server.export`). Check the ox_inventory item docs when you port usable items, and test each one. For new scripts, defining the use behavior in `data/items.lua` with a `server.export` is the most direct option: ```lua title="ox_inventory/data/items.lua" ['coffee'] = { label = 'Coffee', weight = 200, server = { export = 'mycafe.useCoffee', }, }, ``` ### Weapons [#weapons] Weapons are items in ox_inventory. Scripts that give weapons with `xPlayer.addWeapon` silently do nothing in ox mode. Replace those calls with `xPlayer.addInventoryItem('WEAPON_PISTOL', 1)` or the ox_inventory exports. ## Moving from the ESX inventory to ox_inventory [#moving-from-the-esx-inventory-to-ox_inventory] <Steps> <Step> ### Back up the database [#back-up-the-database] Inventory and loadout data are stored as JSON in `users.inventory` and `users.loadout`. Take a full dump before you switch. </Step> <Step> ### Port the item list [#port-the-item-list] Every item in the ESX `items` table that players can own needs an entry in `ox_inventory/data/items.lua`, with the same name. Items that are missing in ox_inventory are dropped from inventories. </Step> <Step> ### Convert player data [#convert-player-data] ox_inventory ships a conversion path for ESX data. Follow the current instructions in the ox_inventory documentation, because the details change between ox releases. Run it once on a test copy of your database first. </Step> <Step> ### Update scripts that use ESX inventory internals [#update-scripts-that-use-esx-inventory-internals] Search your resources for `addWeapon`, `getLoadout`, `esx:useItem`, `esx:addInventoryItem` and `ESX.ShowInventory`. Those rely on the default ESX inventory and need ox equivalents. </Step> </Steps> ## Common issues [#common-issues] - **No inventory at all**: the `ox_inventory` folder exists but is not started, so ESX is in ox mode without an inventory. Start ox_inventory or remove the folder. - **ox_inventory errors about the framework on start**: it started before `es_extended`. Fix the order in `server.cfg`. - **`/giveitem` does nothing or is unknown**: in ox mode ESX does not register its inventory commands. Use the ox_inventory admin commands instead. - **Cash shows 0 after switching**: `money` is now an item. Old balances stay in `users.accounts` until a conversion moves them. Starting money for new players is given as an item from `Config.StartingAccountMoney` for accounts that are in `inventory:accounts`. - **Weapon shop or job armory gives nothing**: the script uses `xPlayer.addWeapon`. Change it to items. - **Scripts checking `Config.OxInventory`**: still works, ESX sets it from `Config.CustomInventory == "ox"`. New code can check `ESX.GetConfig('CustomInventory') == 'ox'` on the client. ## Related [#related] - [esx_inventory (default inventory)](/docs/core/esx_inventory) - [Add items](/docs/guides/add-items) - [Money and accounts](/docs/guides/money-and-accounts) - [Main config](/docs/es_extended/config) - [xPlayer reference](/docs/es_extended/server/xplayer) # Using oxmysql (/docs/guides/oxmysql) oxmysql is the MySQL/MariaDB connector that ESX Legacy uses for all database access. In your own ESX resources you load `@oxmysql/lib/MySQL.lua` and call `MySQL.query`, `MySQL.single`, `MySQL.scalar`, `MySQL.insert`, `MySQL.update`, `MySQL.prepare` or `MySQL.transaction`, each in a callback form and an `.await` form. ## Overview [#overview] - Resource: [oxmysql](https://github.com/overextended/oxmysql) by Overextended, official docs at [overextended.dev/oxmysql](https://overextended.dev/oxmysql) - Side: server only - Required by: `es_extended` (listed in its `dependencies`) and most ESX addons - Connection: the `mysql_connection_string` convar in `server.cfg` ## Set up oxmysql [#set-up-oxmysql] <Steps> <Step> ### Configure the connection string [#configure-the-connection-string] ```bash title="server.cfg" set mysql_connection_string "mysql://user:password@localhost/es_extended?charset=utf8mb4" ensure oxmysql ensure esx_lib ensure es_extended ``` oxmysql must start before `esx_lib` and `es_extended`. The txAdmin ESX recipe uses this order. See [server.cfg](/docs/getting-started/server-cfg) for a full example. </Step> <Step> ### Load the MySQL library in your resource [#load-the-mysql-library-in-your-resource] ```lua title="fxmanifest.lua" fx_version 'cerulean' game 'gta5' lua54 'yes' shared_script '@es_extended/imports.lua' server_scripts { '@oxmysql/lib/MySQL.lua', 'server/main.lua', } dependencies { 'oxmysql', 'es_extended', } ``` `@oxmysql/lib/MySQL.lua` creates the global `MySQL` table. Without it, `MySQL` is `nil` in your resource. </Step> <Step> ### Wait for the connection when you query at startup [#wait-for-the-connection-when-you-query-at-startup] Queries at the top level of a file can run before the database is ready. Wrap startup queries in `MySQL.ready`: ```lua title="server/main.lua" MySQL.ready(function() local count = MySQL.scalar.await('SELECT COUNT(*) FROM `users`') print(('[myresource] %s characters in the database'):format(count)) end) ``` es_extended itself uses `MySQL.ready` to load items and jobs. </Step> </Steps> ## Query functions [#query-functions] | Function | Returns | Typical use | | --- | --- | --- | | `MySQL.query(sql, params, cb)` | Array of rows (or a result object for non-SELECT) | Read many rows | | `MySQL.single(sql, params, cb)` | First row or `nil` | Read one row | | `MySQL.scalar(sql, params, cb)` | First column of the first row | `COUNT(*)`, one value | | `MySQL.insert(sql, params, cb)` | Insert id | `INSERT` into tables with `AUTO_INCREMENT` | | `MySQL.update(sql, params, cb)` | Number of affected rows | `UPDATE`, `DELETE` | | `MySQL.prepare(sql, params, cb)` | Depends on the query (see below) | Hot, repeated queries and batches | | `MySQL.transaction(queries, cb)` | `true` on commit, `false` on rollback | Several writes that must succeed together | Every function has an `.await` variant, for example `MySQL.query.await(sql, params)`, that returns the value instead of calling `cb`. ### Callback vs. await [#callback-vs-await] ```lua title="server/main.lua" -- Callback style: does not block MySQL.query('SELECT `name`, `label` FROM `jobs` WHERE `whitelisted` = ?', { 1 }, function(rows) for i = 1, #rows do print(rows[i].name, rows[i].label) end end) -- Await style: blocks this thread until the result is there CreateThread(function() local rows = MySQL.query.await('SELECT `name`, `label` FROM `jobs` WHERE `whitelisted` = ?', { 1 }) print(#rows, 'whitelisted jobs') end) ``` Use `.await` when the next line depends on the result. It reads top to bottom and avoids nested callbacks. Event handlers, commands registered with `ESX.RegisterCommand` and ESX callback handlers already run in a thread, so you can call `.await` directly there. ### Examples with ESX tables [#examples-with-esx-tables] ```lua title="server/main.lua" -- One row local user = MySQL.single.await( 'SELECT `firstname`, `lastname`, `job` FROM `users` WHERE `identifier` = ?', { xPlayer.getIdentifier() } ) -- One value local vehicleCount = MySQL.scalar.await( 'SELECT COUNT(*) FROM `owned_vehicles` WHERE `owner` = ?', { xPlayer.getIdentifier() } ) -- Insert, returns the AUTO_INCREMENT id local billId = MySQL.insert.await( 'INSERT INTO `billing` (`identifier`, `sender`, `target_type`, `target`, `label`, `amount`) VALUES (?, ?, ?, ?, ?, ?)', { targetIdentifier, xPlayer.getIdentifier(), 'society', 'society_police', 'Speeding', 250 } ) -- Update, returns affected rows local changed = MySQL.update.await( 'UPDATE `owned_vehicles` SET `stored` = 1 WHERE `plate` = ? AND `owner` = ?', { plate, xPlayer.getIdentifier() } ) if changed == 0 then print('Vehicle not found or not owned by this player') end ``` The `users.identifier` column stores the identifier without the `license:` prefix (and with a `charN:` prefix when multicharacter is used). Always use `xPlayer.getIdentifier()` instead of building it yourself. ### Prepared statements and batches [#prepared-statements-and-batches] `MySQL.prepare` uses the binary protocol. It is a good fit for queries that run very often. A single-row, single-column result is unwrapped to the bare value, a single row is returned as a row, otherwise you get an array. You can also pass an array of parameter arrays to run the same statement many times. es_extended does this in `ESX.AddItems`: ```lua title="server/main.lua" MySQL.prepare.await( 'INSERT IGNORE INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES (?, ?, ?, ?, ?)', { { 'coffee', 'Coffee', 1, 0, 1 }, { 'donut', 'Donut', 1, 0, 1 }, } ) ``` ### Transactions [#transactions] A transaction runs several statements and rolls everything back if one fails. Use it for anything that moves money or items between rows. ```lua title="server/main.lua" local ok = MySQL.transaction.await({ { query = 'UPDATE `addon_account_data` SET `money` = `money` - ? WHERE `account_name` = ? AND `owner` IS NULL', values = { 500, 'society_police' } }, { query = 'UPDATE `addon_account_data` SET `money` = `money` + ? WHERE `account_name` = ? AND `owner` IS NULL', values = { 500, 'society_ambulance' } }, }) if not ok then print('Transfer rolled back') end ``` `ESX.CreateJob` uses the same pattern (`exports.oxmysql:transaction_async`) to insert a job and its grades together. <Callout type="warn" title="Society money"> The example above edits the database directly. If `esx_addonaccount` is running, it keeps society balances in memory and writes them back, so a direct `UPDATE` can be overwritten. For live society money, use the account object from `esx_addonaccount` instead. </Callout> ## Migrate from mysql-async [#migrate-from-mysql-async] Old ESX (1.1, 1.2, v1-final) used `mysql-async` with `MySQL.Async.*` and `MySQL.Sync.*`. oxmysql keeps these names as aliases, so old code usually runs, but the modern names are shorter and clearer. | mysql-async | oxmysql | | --- | --- | | `MySQL.Async.fetchAll` | `MySQL.query` | | `MySQL.Async.fetchScalar` | `MySQL.scalar` | | `MySQL.Async.fetchSingle` | `MySQL.single` | | `MySQL.Async.insert` | `MySQL.insert` | | `MySQL.Async.execute` | `MySQL.update` | | `MySQL.Async.transaction` | `MySQL.transaction` | | `MySQL.Sync.fetchAll(...)` | `MySQL.query.await(...)` | | `MySQL.Sync.execute(...)` | `MySQL.update.await(...)` | | `MySQL.Sync.fetchScalar(...)` | `MySQL.scalar.await(...)` | <Steps> <Step> ### Replace the manifest include [#replace-the-manifest-include] Change `'@mysql-async/lib/MySQL.lua'` to `'@oxmysql/lib/MySQL.lua'` in `server_scripts`, and replace `mysql-async` with `oxmysql` in `dependencies`. </Step> <Step> ### Rename the calls [#rename-the-calls] Before: ```lua title="server/main.lua" MySQL.Async.fetchAll('SELECT * FROM users WHERE identifier = @identifier', { ['@identifier'] = identifier }, function(result) print(result[1].job) end) ``` After: ```lua title="server/main.lua" local row = MySQL.single.await('SELECT `job` FROM `users` WHERE `identifier` = ?', { identifier }) print(row and row.job) ``` Named parameters (`@identifier`) still work in oxmysql, but positional `?` placeholders are what es_extended uses and are easier to keep in sync. </Step> <Step> ### Remove mysql-async [#remove-mysql-async] Delete `mysql-async` and `ghmattimysql` from `server.cfg`. Running two connectors wastes connections and hides which one a script uses. </Step> </Steps> For a full conversion checklist (shared object, xPlayer, manifest), see [Convert old ESX scripts](/docs/guides/convert-old-esx-scripts). ## Common issues [#common-issues] - **`attempt to index a nil value (global 'MySQL')`**: the resource does not load `@oxmysql/lib/MySQL.lua`, or loads it in `client_scripts`. - **`.await` errors at file load**: top-level code is not inside a thread. Wrap it in `MySQL.ready` or `CreateThread`. - **`SELECT *` on `users`**: the table has long JSON columns (`accounts`, `inventory`, `loadout`, `metadata`, `skin`). Select only the columns you need. - **String-built SQL**: `'... WHERE plate = "' .. plate .. '"'` is an SQL injection. Use `?` and the parameter table. - **Slow queries on large tables**: add indexes for the columns in your `WHERE` clauses and check with `EXPLAIN`. See [Performance](/docs/guides/performance). ## Related [#related] - [Database schema](/docs/guides/database-schema) - [Callbacks](/docs/guides/callbacks) - [Convert old ESX scripts](/docs/guides/convert-old-esx-scripts) - [Performance](/docs/guides/performance) - [server.cfg](/docs/getting-started/server-cfg) # Performance (/docs/guides/performance) Most ESX performance problems come from **scripts, not from the framework core**: client loops that run every frame, server events that fire too often and database queries without indexes. This guide shows how to measure first, then fix the usual causes in order of impact. ## Measure before you optimize [#measure-before-you-optimize] <Steps> <Step> ### Client: resmon [#client-resmon] Open the F8 console in game and run: ```txt resmon 1 ``` You see CPU time (ms) and memory per resource. Stand still somewhere quiet, then near the places your scripts care about (shops, job markers). A resource that stays high everywhere has an always-running loop. </Step> <Step> ### Server: profiler [#server-profiler] In the server console (or txAdmin live console): ```txt profiler record 500 profiler view ``` `profiler record` captures the given number of frames, `profiler view` opens the result in the browser-based viewer. Look for resources with long tick times or event handlers that run constantly. See the [cfx.re profiler docs](https://docs.fivem.net/docs/scripting-manual/debugging/using-profiler/). </Step> <Step> ### Database: slow queries [#database-slow-queries] `oxmysql` prints warnings for slow queries in the server console. Note the query and the resource. Those are your index candidates (see below). </Step> </Steps> ## Fix thread waits [#fix-thread-waits] The single biggest client cost is `while true do Wait(0) ... end` that runs whether or not the player is near anything. ```lua title="client/main.lua" -- Bad: runs every frame for every player, everywhere CreateThread(function() while true do Wait(0) local coords = GetEntityCoords(PlayerPedId()) if #(coords - vector3(25.7, -1347.3, 29.5)) < 2.0 then ESX.ShowHelpNotification('Press ~INPUT_CONTEXT~ to open the shop') end end end) ``` ```lua title="client/main.lua" -- Better: sleep long when far away, only run per frame when close local SHOP <const> = vector3(25.7, -1347.3, 29.5) CreateThread(function() while true do local sleep = 1000 local distance = #(GetEntityCoords(PlayerPedId()) - SHOP) if distance < 2.0 then sleep = 0 ESX.ShowHelpNotification('Press ~INPUT_CONTEXT~ to open the shop') elseif distance < 20.0 then sleep = 250 end Wait(sleep) end end) ``` Rules of thumb: - Only use `Wait(0)` while you draw something or read a key every frame. - Start job-specific loops when the player gets the job and stop them when the job changes, instead of checking `ESX.PlayerData.job.name` every frame. Listen to `esx:setJob` on the client. - Cache values that rarely change (`PlayerId()`, `GetPlayerServerId(PlayerId())`). Update a cached ped on `esx:playerPedChanged`, which `es_extended` triggers when the ped changes. - Use the built-in point helpers (`ESX.Point`) or `ox_lib` points rather than writing your own distance loops for every marker. ## State bags instead of polling events [#state-bags-instead-of-polling-events] A common anti-pattern is a client that asks the server every few seconds "what is my job?" or "how many police are online?". ESX already replicates this data with state bags, which are synced by the engine only when they change. | State | Set by `es_extended` | Replicated to clients | | --- | --- | --- | | `Player(src).state.job` | On load, `xPlayer.setJob()` and job refresh | Yes | | `Player(src).state.group` | On load and `xPlayer.setGroup()` | Yes | | `Player(src).state.name` | On load and name change | Yes | | `Player(src).state.identifier`, `.license` | On load | No (server only) | | `GlobalState.playerCount` | On player load and drop | Yes | | `GlobalState['<job>:count']` | When job counts change, for example `GlobalState['police:count']` | Yes | Read them instead of building your own sync: ```lua title="client/main.lua" -- Number of police online without any server callback local function getPoliceCount() return GlobalState['police:count'] or 0 end -- React to your own job changing AddStateBagChangeHandler('job', ('player:%s'):format(GetPlayerServerId(PlayerId())), function(_, _, job) if job then print(('Job is now %s grade %s'):format(job.name, job.grade)) end end) ``` On the server, `ESX.GetNumPlayers('job', 'police')` returns the same count from memory without looping over players. <Callout type="warn" title="Do not read state bags every frame"> Reading `GlobalState` deserializes the value. ESX itself caches the population density state in a change handler for this reason. Cache the value in a local and update it in `AddStateBagChangeHandler`. </Callout> ## Use events and callbacks sparingly [#use-events-and-callbacks-sparingly] - Do not trigger server events from a client loop. Trigger them on an action (key press, menu choice). - Prefer one callback that returns everything a menu needs over five callbacks. - ESX callbacks go through `esx_lib`, which rate limits callbacks per player and rejects payloads larger than `xLib:callbackMaxPayloadBytes` (default 65536 bytes). If your callback sends huge tables, send less data. See [Callbacks](/docs/guides/callbacks). - On the server, `ESX.GetExtendedPlayers('job', 'police')` filters in memory. Use it instead of looping over `GetPlayers()` and calling `ESX.GetPlayerFromId` on every entry. ## Database performance [#database-performance] ### Query patterns [#query-patterns] - Never run a query per player per tick. Load data once, keep it in memory, write it back on change or on a timer. - Select only the columns you need instead of `SELECT *` on `users`, which contains several `LONGTEXT` JSON columns. - Use `MySQL.prepare` for statements you run very often, and batch inserts. See [oxmysql](/docs/guides/oxmysql). ### Indexes [#indexes] `legacy.sql` already defines primary keys and the important secondary keys, for example `owned_vehicles.owner`, `billing.identifier`, `user_licenses (owner, type)` and the `addon_account_data` and `datastore_data` owner keys. Several addons add their own indexes at startup, for example `esx_garage` adds composite indexes on `owned_vehicles` and `esx_banking` adds `idx_banking_identifier_time`. The old `security_indexes.sql` in ESX-Legacy-Addons is **superseded** and now only prints a message. Its replacement is `sql/security_hardening_2026_10_02.sql`. It is optional because `esx_billing`, `esx_license`, `esx_addonaccount` and `esx_banking` apply these schema changes automatically on startup. If you run it by hand: <Steps> <Step> ### Back up the database [#back-up-the-database] DDL statements commit implicitly. The script is not a rollback transaction. </Step> <Step> ### Check for duplicates [#check-for-duplicates] The script stops with an error if `user_licenses` or `addon_account_data` contain duplicate rows, and it never merges financial rows for you. Reconcile them first. </Step> <Step> ### Run it in a maintenance window [#run-it-in-a-maintenance-window] It requires MySQL 8 or MariaDB 10.6+ and InnoDB for the economy tables. It is safe to re-run. </Step> </Steps> Do not blindly add indexes from forum posts. The file itself warns that InnoDB secondary indexes already contain the primary key, so indexes like `(owner, plate)` on `owned_vehicles` are redundant. Use `EXPLAIN` on the slow query first, then add the index that matches its `WHERE` clause. ## Framework settings that matter [#framework-settings-that-matter] | Key | Default | Effect | | --- | --- | --- | | `Config.PaycheckInterval` | `7 * 60000` | How often paychecks run (ms) | | `Config.PaycheckBatchSize` | `100` | Players paid before the thread yields | | `Config.PaycheckBatchDelay` | `250` | Wait between batches (ms) | | `Config.PaycheckJitter` | `30 * 1000` | Random delay before each run, avoids synchronized bursts | | `Config.EnableDebug` | `false` | Extra debug output. Keep it off in production | | `Config.AdminLogging` | `false` | Discord logging for admin commands. Each log is an HTTP request | | `Config.LogPaycheck` | `false` | Logs every paycheck to Discord. Expensive on big servers | All are in `es_extended/shared/config/main.lua`. See [Main config](/docs/es_extended/config). Player saving is already efficient: `es_extended` saves all players every 10 minutes in one background thread, saves a player when they drop and saves everyone before a txAdmin scheduled restart and on shutdown. You do not need extra "save all" loops. ## OneSync and the server artifact [#onesync-and-the-server-artifact] - ESX Legacy requires **OneSync Infinity**. With `onesync` set to `off` or `legacy`, players are rejected on join. txAdmin enables OneSync by default; in a manual setup add `set onesync on`. - `es_extended` requires server artifact **10188 or newer** and prints an error every minute if the artifact is older. Updating the artifact is also one of the cheapest performance fixes. - With OneSync, prefer server-side entity creation (`ESX.OneSync.SpawnVehicle`, `ESX.OneSync.SpawnObject`) and server-side lookups (`ESX.OneSync.GetPlayersInArea`) over asking clients. ## Common issues [#common-issues] - **A resource shows 0.5 ms or more in resmon everywhere**: it has an always-running `Wait(0)` loop. Apply the sleep pattern above. - **Server hitches every few minutes**: check what runs on a timer. Paychecks, autosave and Discord logging are the usual suspects. Lower `Config.PaycheckBatchSize` or turn off paycheck logging. - **Hitches when many players join**: look for scripts that run heavy queries in `esx:playerLoaded`. - **"ESX Requires a minimum Artifact version of 10188"**: update your FXServer artifact. ## Related [#related] - [Coding practices](/docs/getting-started/coding-practices) - [OneSync functions](/docs/es_extended/server/onesync) - [oxmysql with ESX](/docs/guides/oxmysql) - [Database schema](/docs/guides/database-schema) - [Troubleshooting](/docs/getting-started/troubleshooting) # Introduction to ESX Legacy (/docs) ESX Legacy is an open-source roleplay framework for [FiveM](https://fivem.net), the GTA V multiplayer platform. It gives your server a persistent player object with money accounts, jobs, an inventory and permission groups, plus a set of ready-made resources (character creation, menus, jobs, shops, banking) that you can use as they are or replace with your own. This site documents ESX Legacy **1.16.4** (current release as of October 2026). Everything here is checked against the Lua source in [esx-framework/esx_core](https://github.com/esx-framework/esx_core). When the official docs and the source disagree, this site follows the source. <Callout type="info" title="Independent documentation"> esx-fivem.com is a community project and is **not affiliated with the ESX team**. The official documentation lives at [docs.esx-framework.org](https://docs.esx-framework.org/en) and the source code at [github.com/esx-framework](https://github.com/esx-framework). Report framework bugs on the official GitHub, not here. </Callout> ## A short history [#a-short-history] ESX started in 2017 as a framework that added jobs, money accounts and an inventory to FiveM servers. It quickly became one of the most common bases for FiveM roleplay servers, and a large ecosystem of free and paid scripts grew around it. The original ESX releases were written for a much older FiveM: no OneSync, Lua 5.3, `mysql-async` and many client-trusted events. **ESX Legacy** is the continued, actively maintained line of the framework. It keeps the familiar API (`ESX.GetPlayerFromId`, `xPlayer.addMoney`, `esx:playerLoaded` and so on) so that most older scripts still work, but it modernizes the internals: - `oxmysql` instead of `mysql-async` / `ghmattimysql` - Lua 5.4 (`lua54 'yes'`) in every core resource - OneSync Infinity is required, which allows server-side entity and player lookups - a shared library resource, `esx_lib`, that provides callbacks, validation, rate limiting and other helpers to the core - automatic database migrations when you update the core - many security fixes, for example events that only accept calls from `es_extended` itself If you find a tutorial that uses `TriggerEvent('esx:getSharedObject', ...)` or `mysql-async`, it was written for old ESX. See [Convert old ESX scripts](/docs/guides/convert-old-esx-scripts) for how to update it. ## Architecture [#architecture] An ESX Legacy server is a normal FiveM server with a few layers of resources. They start in this order: | Layer | Resources | Role | | --- | --- | --- | | Database driver | `oxmysql` | Connects FiveM to MariaDB or MySQL. Every ESX resource queries the database through it. | | Library | `esx_lib` | Shared helpers used by `es_extended` (callbacks, schema helpers, OneSync helpers, validation). | | Core | `es_extended` | The framework itself: player objects (`xPlayer`), accounts, jobs, items, commands, callbacks, paychecks. | | Core resources | `esx_identity`, `esx_multicharacter`, `esx_skin`, `skinchanger`, `esx_notify`, `esx_textui`, `esx_progressbar`, `esx_context`, `esx_menu_*`, `esx_inventory`, `esx_whitelist`, `esx_loadingscreen`, `esx_chat_theme`, `cron` | UI, character creation and other systems that most servers need. They ship in the `[core]` folder. | | Addons | `esx_policejob`, `esx_ambulancejob`, `esx_banking`, `esx_shops`, `esx_vehicleshop`, `esx_property` and more | Optional gameplay resources from the `[esx_addons]` folder in [ESX-Legacy-Addons](https://github.com/esx-framework/ESX-Legacy-Addons). | | Your scripts | anything you write | Get the `ESX` object through `@es_extended/imports.lua` or `exports['es_extended']:getSharedObject()`. | The most important idea is the **player object**. When a player has loaded, the server holds an `xPlayer` for them. You get it with `ESX.GetPlayerFromId(source)` and call methods on it, for example `xPlayer.addAccountMoney('bank', 500, 'Salary')`. ESX keeps that state in memory and saves it to the `users` table. On the client, the same data is mirrored in `ESX.PlayerData`. ```lua title="server/main.lua" local ESX = exports['es_extended']:getSharedObject() RegisterNetEvent('myscript:hello', function() local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return end xPlayer.showNotification(('Hello %s, you work as %s'):format(xPlayer.getName(), xPlayer.job.label)) end) ``` ### Requirements at a glance [#requirements-at-a-glance] - A current FiveM server build (ESX refuses connections on artifacts older than 10188) - OneSync set to `on` (Infinity). `off` and `legacy` are rejected when a player connects. - MariaDB (recommended) or MySQL - `oxmysql` and `esx_lib` started before `es_extended` ## Who this documentation is for [#who-this-documentation-is-for] - **Server owners** who want to install ESX, configure `server.cfg`, give admin rights and keep the server updated. Start with [Installation](/docs/getting-started/installation). - **Script developers** who write resources on top of ESX. Start with [Your first ESX resource](/docs/getting-started/first-resource), then use the [es_extended reference](/docs/es_extended). - **People coming from other frameworks** who want to compare. See [ESX vs QBCore](/docs/guides/esx-vs-qbcore). ## How this site is organized [#how-this-site-is-organized] <Cards> <Card title="Getting Started" href="/docs/getting-started/installation"> Install ESX with txAdmin or by hand, configure server.cfg, build your first resource and fix common errors. </Card> <Card title="es_extended" href="/docs/es_extended"> Reference for the core: xPlayer, server and client functions, events, commands, config and shared helpers. </Card> <Card title="Core resources" href="/docs/core"> Multicharacter, identity, skin, notifications, menus, text UI, progress bar, inventory and the other [core] resources. </Card> <Card title="Addons" href="/docs/addons"> Jobs, shops, banking, housing, vehicles and the other official addons. </Card> <Card title="Guides" href="/docs/guides/create-a-job"> Task-focused guides: create a job, add items, admin permissions, callbacks, oxmysql, ox_inventory and more. </Card> <Card title="FAQ" href="/docs/faq"> Short answers to the questions people ask most often about ESX. </Card> </Cards> ## Conventions used on this site [#conventions-used-on-this-site] - Code examples use Lua 5.4 and the modern way of getting ESX (`@es_extended/imports.lua` or the `getSharedObject` export). - Every function has its side marked: **server**, **client** or **shared**. Calling a server function on the client (or the other way round) is the most common cause of `attempt to call a nil value` errors. - `source` always means the server ID of the player who triggered an event or command. - Each page lists its upstream source file, so you can check the code yourself. ## Related [#related] - [Install ESX Legacy with txAdmin](/docs/getting-started/installation) - [server.cfg for ESX](/docs/getting-started/server-cfg) - [xPlayer reference](/docs/es_extended/server/xplayer) - [FAQ](/docs/faq)