# API overview

koja-dispatcher exposes five integration surfaces:

| Surface | Side | Use it to |
|---|---|---|
| [Server exports](/new/dispatcher/api/server-exports) | server | Ring calls, create and manage incidents, move units, read dispatchers, change and register settings, create BOLOs, log to Discord, report plates. |
| [Client exports](/new/dispatcher/api/client-exports) | client | Check whether the local player is on shift, raise an alert or a `/911` call from a client script. |
| [Hooks](/new/dispatcher/api/hooks) | server | Block or rewrite calls, incidents, assignments and shifts before they happen, and react after. |
| [Events](/new/dispatcher/api/events) | both | Listen to dispatcher activity with plain `AddEventHandler`, feed phones and other dispatch scripts in. |
| [Settings](/new/dispatcher/api/settings) | server | Every key `GetSetting` and `SetSetting` accept, with ranges and defaults. |

The [examples](/new/dispatcher/api/examples) page has complete, copy-paste recipes.

## Getting started

::::steps
### Start order

Start your resource **after** `koja-dispatcher` (and its dependencies `oxmysql` and `koja-lib`).

```cfg title="server.cfg"
ensure oxmysql
ensure koja-lib
ensure koja-dispatcher
ensure my-robberies
```

### Call an export

The resource folder must be named `koja-dispatcher`: the folder name is the export namespace.

```lua title="server.lua"
local dispatch = exports['koja-dispatcher']
print(('%d dispatcher(s) on shift'):format(dispatch:GetDispatcherCount()))
```

### Handle the result

Exports that create something return `id, route` on success and `nil, errorCode` on failure. Always check the first value.

```lua title="server.lua"
local id, route = exports['koja-dispatcher']:CreateCall({
    coords = vector3(24.5, -1347.3, 29.5),
    caller = 'Store alarm',
    message = 'Silent alarm at the 24/7 on Innocence Blvd',
})
if not id then
    print('911 call failed: ' .. tostring(route)) -- route holds the error code
end
```
::::

## Concepts

| Thing | What it is |
|---|---|
| **Call** | A line at the dispatcher console. `kind = 'script'` is a generated caller with a scripted (or AI) conversation; `kind = 'player'` is a text line from `/911`, a phone, an export, a dispatch-script alert or koja-mdt. A dispatcher answers the call, questions the caller and dispatches it, which turns it into an incident. |
| **Incident** | A job on the map. It has a category, priority 1 to 3, a `need` (units per department), progress, danger, assigned units and units on scene. It ends as `resolved`, `failed` or `cancelled`. |
| **Unit** | An AI unit from the fleet (`ai:<n>`) or a player on duty in a department (`p:<serverId>`). Departments are `police`, `ems` and `fire`. |
| **Dispatcher** | A player on shift at a station console, or with the remote CAD (`/cad`) when `access.remote` is on. |

### Call lifecycle

| State | Meaning | Next |
|---|---|---|
| `ringing` | Waiting at the console. Generated calls hang up after `calls.patience` seconds (`callMissed`). | `talking` |
| `talking` | A dispatcher answered (`callAnswered`). Questions, instructions and replies happen here. | `done` |
| `done` | Dispatched (the call's `incident` field holds the new incident id), hung up, marked as a prank or expired (`callEnded`). Removed 4 s later. | |

Player calls also end on their own when nobody touched them for 3 times `calls.playerReplyWindow`. Within `calls.playerReplyWindow` a second `/911` from the same player is added to the open call instead of creating a new one.

Call ids start at 1 and count up per server start.

### Incident lifecycle

| Status | Meaning |
|---|---|
| `active` | On the map. Units drive to it; when they are on scene, **progress** fills at a speed set by how much of the `need` is covered. Uncovered time raises **danger** (exposure). Scenarios can escalate (more units, priority 1) when danger passes a threshold. |
| `resolved` | Progress reached 100 %, a field unit sent code 4, `ResolveIncident` was called, the MDT call was closed (`outcome = 'mdt'`) or a prank was uncovered (`outcome = 'prank'`). Multi-stage scenarios start the next stage instead of resolving. |
| `failed` | Danger reached the incident's fail time, or a pursuit suspect got away (`outcome = 'lost'`). |
| `cancelled` | A dispatcher or `CancelIncident` cancelled it (`outcome = 'cancelled'`, or `'prank'` for a prank call). |

Ended incidents stay in `GetIncidents()` for 20 seconds (`Config.Incidents.lifetimeAfterEnd`), then disappear. Incident ids start at 1001.

An incident whose caller did not give the location yet is `located = false`: it sits at an approximate point (up to `Config.Calls.approximateRadius` = 300 m off). Units that arrive there first search (`searching`) before they find the scene.

Incidents created from a player (`/911` fallback, `follow` field) follow that player until the first unit is on scene.

### Unit lifecycle and statuses

| Status | Meaning |
|---|---|
| `available` | Free. Can be assigned. |
| `enroute` | Driving to an incident or to a staging point. |
| `searching` | Arrived at an approximate location, looking for the scene. |
| `onscene` | Working the incident. |
| `transport` | EMS unit driving a patient to a hospital. |
| `returning` | AI unit driving back to its station. Can be assigned again. |
| `busy` | Set by hand (CAD, field code `10-6`, `SetUnitStatus`). Not assignable. |
| `unavailable` | Set by hand (field code `10-7`, `SetUnitStatus`). Not assignable. |
| `offline` | AI unit off duty: `mode.units = 'players'`, or enough players of its department are on duty (`units.disableWhenPlayers`). |
| `resting` / `repair` | AI unit resting or in repair at its station (fleet module, `fleet.enabled`). |

AI units follow the road graph; air units fly straight. Player units are real players and their position is read from their ped.

Unit purposes (the `purpose` field): `incident`, `move` (staging point), `home`, `hospital`, `handover`, `patrol`.

### Dispatcher lifecycle

A dispatcher starts a shift at a console (`shiftStart` hook, then `shiftStarted`), works calls and incidents, and ends it (`shiftEnded`) with a summary: minutes, stats, a 1 to 5 star rating and pay. Leaving the server ends the shift without pay. A player on shift is never a field unit at the same time.

## The call router

`CreateCall`, `/911`, phone calls, the client `SendAlert` export and dispatch-script alerts in `call` mode all go through one router:

| Step | Check | Result | Returned |
|---|---|---|---|
| 1 | At least one dispatcher is on shift **and** `mode.calls` is not `generated` | The call rings at the console (`callCreate`, `callCreated`). | `callId, 'call'` |
| 2 | Otherwise, `mode.noDispatcher = 'auto'` | An incident is created at once and the nearest free units are auto-assigned. The caller gets a "units dispatched" notification. | `incidentId, 'incident'` |
| 3 | Otherwise, `mode.noDispatcher = 'mdt'`, koja-mdt is running and the call did not come from koja-mdt | The call is created as a koja-mdt dispatch call. | `mdtCallId, 'mdt'` |
| 4 | Anything else (`reject`, or the MDT is down) | Nothing is created. | `nil, 'no_dispatchers'` |

If a `callCreate` hook blocks the call in step 1, the router returns `nil` and the hook's reason (`cancelled` when the hook gave none). If an `incidentCreate` hook blocks the incident in step 2, the router returns `nil, 'cancelled'`.

:::hint{type="info" title="Exceptions"}
`CreateCall({ scenario = ... })` skips the router: it always rings a generated call, even with nobody on shift (it then counts as missed after `calls.patience`). `CreateIncident` also skips it: the incident is always created.
:::

Admins switch modes in game with `/dispatchadmin`. Defaults live in `Config.Modes` (`calls = 'mixed'`, `units = 'mixed'`, `noDispatcher = 'reject'`).

## Data formats

### Coordinates

Every `coords` parameter accepts:

| Form | Example |
|---|---|
| `vector3` / `vector4` | `vector3(24.5, -1347.3, 29.5)` |
| table with keys | `{ x = 24.5, y = -1347.3, z = 29.5 }` |
| table without `z` | `{ x = 24.5, y = -1347.3 }` (`z` becomes `0.0`) |
| array | `{ 24.5, -1347.3, 29.5 }` |

`x` and `y` must be within ±9000, otherwise the export returns `nil, 'invalid'`. `CreateCall` and `CreateIncident` also take `source = serverId` instead of `coords` to use that player's position.

### Needs

`need` is how many units of each department an incident requires:

```lua
need = { police = 2, ems = 1, fire = 0 }
```

Only `police`, `ems` and `fire` are read. Values are floored and capped at **8** per department; zero and negative values are dropped. An incident with an empty need gets `{ police = 1 }`. Unit types have a **power** (`Config.UnitTypes`): SWAT and ladder trucks count as 2.

An incident has two needs: `need` (the truth) and `known` (what the dispatcher knows). Exports set both to the same value; generated calls reveal the rest through questions or on scene.

### Categories

`robbery`, `burglary`, `assault`, `shooting`, `domestic`, `disturbance`, `suspicious`, `medical`, `overdose`, `fire`, `traffic`, `rescue`, `hostage`, `pursuit`, `officer_down`, `hazmat`.

The list is `Scenarios.Categories` in `shared/scenarios.lua`. An unknown category is ignored by `CreateCall` and `UpdateIncident` and becomes `suspicious` in `CreateIncident`.

### Priority

`1` (highest) to `3`. Values are clamped. Priority 1 incidents are announced as alerts on the radio.

### Unit references

Unit exports take a **unit reference**:

| Reference | Meaning |
|---|---|
| `'ai:3'` | AI unit 3 of `Config.AiUnits.fleet` (the index in the list). |
| `'ai:d12'` | AI unit with database row 12, when the fleet was edited in the in-game fleet editor. |
| `'p:12'` | The player unit of server id 12. |
| `12` (number) | The same as `'p:12'`: the player unit of that server id. |

Unit ids depend on the fleet configuration, so look them up with `GetUnits()` (for example by `callsign`) instead of hard-coding them.

## Return conventions

| Export kind | Success | Failure |
|---|---|---|
| Create (`CreateCall`, `CompatAlert`, `PhoneCall`, client `SendAlert`) | `id, route` | `nil, errorCode` (client exports: `false, errorCode`) |
| `CreateIncident`, `CreateBolo` | the id | `nil, errorCode` |
| `CloseBolo`, `SetSetting`, `RunFieldCode` | `true` | `false, errorCode` |
| Actions (`UpdateIncident`, `ResolveIncident`, `ReleaseUnit`, `SetCallsign`, `RegisterSetting`, ...) | `true` | `false` |
| Counters (`AssignUnits`, `AutoAssign`) | number of units assigned (`0` when nothing happened) | `0` |
| Lookups (`GetCall`, `GetUnit`, ...) | a table | `nil` |
| Lists (`GetUnits`, `GetIncidents`, ...) | an array | an empty array |

Returned tables are **copies**: changing them does not change the dispatcher. Exports never throw on bad input.

`route` is `'call'` (rings at the console), `'incident'` (created as an incident), `'mdt'` (sent to koja-mdt; the id is the MDT call id) or, for player calls added to an open line, `'update'`.

## Error codes

| Code | Returned by | Meaning |
|---|---|---|
| `invalid` | most exports | Missing or malformed input (no coordinates, unknown scenario, bad key, message too short). |
| `no_dispatchers` | call router | Nobody on shift and `mode.noDispatcher` did not create anything. |
| `cancelled` | call router, `CreateIncident`, `CompatAlert`, client `SendAlert`, shift start | A `before` hook blocked the call, incident or shift. A hook can return its own code instead (`return false, 'suspended'`); that code is passed on in place of `cancelled`. |
| `disabled` | `SendAlert`, `PhoneCall`, `CompatAlert`, `RunFieldCode` | The feature or setting is off (`api.clientAlerts`, `phones.enabled`, `compat.enabled`, `mdt_status.enabled`). |
| `no_response` | client exports | The server did not answer within 10 s, or the request hit the rate limit. |
| `rate` | `PhoneCall` | The same player called again within `Config.Phones.cooldown` (2 s). |
| `duplicate` | `CompatAlert` | An identical alert came in within `Config.Compat.dedupeSeconds` (20 s). |
| `not_field_unit` | `RunFieldCode` | The player is not a field unit or MDT officer on duty, or is a dispatcher. |
| `cooldown` | `RunFieldCode` | Backup or panic used again within `mdt_status.cooldown`. |
| `offline` / `busy` / `unavailable` / `no_channel` | `ConnectVoice` | Voice call cannot start: a player left, one of them is already in a voice call, no voice resource, or all channels are used. |
| `title_required` / `plate_required` / `invalid_plate` / `invalid_url` / `not_found` / `mdt_offline` / `mdt_failed` / `db` | `CreateBolo`, `CloseBolo` | BOLO input or storage problem, see [CreateBolo](/new/dispatcher/api/server-exports). |

## Hooks and events

Every lifecycle point is published twice:

1. As a **hook**: `RegisterHook(name, fn)`. `before` hooks (`callCreate`, `incidentCreate`, `unitAssign`, `shiftStart`) run first and can block the action (with an optional reason) or, for `incidentCreate`, change it. `after` hooks run when it happened.
2. As a **server-local event**: `koja-dispatcher:<name>` for every `after` hook, fired right after the hook functions, with the same payload. Listen with `AddEventHandler`.

Use a hook when you need to block or rewrite something, or want automatic cleanup when your resource stops. Use the event for simple listeners. See [Hooks](/new/dispatcher/api/hooks) and [Events](/new/dispatcher/api/events).

## Server and client rules

| Topic | Rule |
|---|---|
| Side | Server exports can only be called from server scripts. Client exports talk to the server through a rate-limited RPC. |
| Trust | Server resources are trusted: server exports do not check permissions. Validate player input (distance, job, cooldown) in your own net events before you call them. |
| Yielding | Server exports do not yield, except the ones that write the database (`SetSetting`, `CreateBolo`, `CloseBolo`) or talk to koja-mdt. Client exports `SendAlert` and `Call911` **yield** until the server answers; call them from a thread or an event handler. |
| Rate limits | Client RPCs are limited per player (`Config.Security.limits`). `SendAlert`: burst 3, then one every 5 s. `Call911`: burst 2, then one every 20 s. |

## Own code inside the resource

If you need to change koja-dispatcher itself, put your own files in `server/custom/*.lua` and `client/custom/*.lua`. Both folders are loaded automatically, after all built-in modules, so `KOJA.Server.*` / `KOJA.Client.*` are ready when your file runs. Do not add files to `server/features/`: the manifest lists those files one by one, so a new file there is not loaded. Keep a copy of your custom files when you update.

## Versioning

| Item | Value |
|---|---|
| Resource / export namespace | `koja-dispatcher` |
| Version | `version` in `fxmanifest.lua` (`1.1.0` at the time of writing) |
| Product key | `koja_resource 'koja-dispatch'` in `fxmanifest.lua`, used by koja-lib to look up the latest release |
| Database tables | `koja_dispatch_*` |

On start the server prints a banner and, when `Config.VersionCheck.enabled` is on and koja-lib is started, asks koja-lib for the release status: up to date, development build, update available or critical update (with the first changelog lines and a download link).

Feature-detect from your own resource instead of assuming a version:

```lua title="server.lua"
local state = GetResourceState('koja-dispatcher')
local version = GetResourceMetadata('koja-dispatcher', 'version', 0)
if state == 'started' then
    print(('koja-dispatcher %s is running'):format(version))
end

-- an export that may not exist in older versions
local ok, id = pcall(function() return exports['koja-dispatcher']:CreateBolo(data) end)
```

`/dispatchcheck` (server console or admins) runs a health check: koja-lib, oxmysql, framework, database tables, dispatcher jobs, AI fleet, web build, AI keys, settings, stations and the koja-mdt link.
