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