# 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)
