# Events

| Group | Side | Direction |
|---|---|---|
| [Dispatcher events](#dispatcher-events) | server | koja-dispatcher → your resource |
| [Events you can trigger](#events-you-can-trigger) | server / client | your resource → koja-dispatcher |
| [koja-mdt events](#koja-mdt-events) | server | koja-mdt → koja-dispatcher |
| [Client events](#client-events) | client | koja-dispatcher → the player's client |
| [Internal events](#internal-events) | both | do not use |

## Dispatcher events

Every [after hook](/new/dispatcher/api/hooks) is also fired as a **server-local** event `koja-dispatcher:<hook>`, with the same payload, right after the hook functions ran.

```lua title="server.lua"
AddEventHandler('koja-dispatcher:incidentEnded', function(incident)
    if incident.status == 'resolved' then
        print(('incident #%d closed: %s'):format(incident.id, incident.outcome))
    end
end)
```

:::hint{type="danger" title="AddEventHandler, never RegisterNetEvent"}
These events are triggered with `TriggerEvent` on the server. Listen with `AddEventHandler`. Do **not** call `RegisterNetEvent` for them: that would let any client fake them with `TriggerServerEvent`.
:::

| Event | Payload |
|---|---|
| `koja-dispatcher:callCreated` | public call |
| `koja-dispatcher:callAnswered` | `{ id, kind, handler }` |
| `koja-dispatcher:callEnded` | `{ id, kind, incident, handler }` |
| `koja-dispatcher:callMissed` | `{ id, caller, scenario, label, waited }` |
| `koja-dispatcher:callDropped` | `{ id, caller, scenario, label, handler }` |
| `koja-dispatcher:incidentCreated` | public incident |
| `koja-dispatcher:incidentUpdated` | public incident |
| `koja-dispatcher:incidentEnded` | public incident (`status`, `outcome`, `endedAt`) |
| `koja-dispatcher:unitAssigned` | `{ unit, src, callsign, incident }` |
| `koja-dispatcher:unitStatus` | `{ unit, src, callsign, status, previous }` |
| `koja-dispatcher:unitAdded` | public unit |
| `koja-dispatcher:unitRemoved` | `{ unit, src, callsign }` |
| `koja-dispatcher:shiftStarted` | `{ source, name, console }` |
| `koja-dispatcher:shiftEnded` | `{ source, name, minutes, stats, earned, rating }` |
| `koja-dispatcher:settingChanged` | `{ key, value, previous }` |
| `koja-dispatcher:adminAction` | `{ source, name, text }` |
| `koja-dispatcher:mciStarted` | `{ incident, patients, x, y }` |
| `koja-dispatcher:mciEnded` | `{ incident, transported, treated, seconds, status }` |
| `koja-dispatcher:pursuitStarted` | `{ incident, x, y, category }` |
| `koja-dispatcher:pursuitEnded` | `{ incident, outcome, seconds }` |
| `koja-dispatcher:voiceConnected` | `{ call, channel, provider, caller, handler }` |
| `koja-dispatcher:voiceEnded` | `{ call, channel, caller, handler, handlerKind, reason, duration }` |
| `koja-dispatcher:boloCreated` | internal BOLO (event only, no hook) |

Payload details are on the [Hooks](/new/dispatcher/api/hooks) page.

### `koja-dispatcher:boloCreated`

Fires when a BOLO is created in the dispatcher's own list (`mdt_tools.boloSource = 'internal'`, or `auto` while koja-mdt is not running), from the CAD or the `CreateBolo` export. BOLOs created in koja-mdt fire koja-mdt's own events instead.

```lua
{
    id = 'i12', source = 'internal', type = 'vehicle',
    title = 'Black Sultan, armed', description = nil,
    plate = 'ABC123', model = 'Sultan', colour = 'Black', priority = 1, image = nil,
    x = 215.8, y = -810.1, label = 'Legion Square', incident = 1004,
    createdBy = 'Jane Doe', createdAt = 1730000000,
}
```

```lua title="server.lua"
AddEventHandler('koja-dispatcher:boloCreated', function(bolo)
    if bolo.plate then exports['my-alpr']:Watch(bolo.plate) end
end)
```

---

## Events you can trigger

### `koja-dispatcher:phone:call` (net, client → server)

Generic 911 entry point for any phone resource. The text is handled like a call to the service number: it goes through `/911` logic, follow-up texts join the open call and dispatcher replies come back to the player (as phone messages when a supported phone is running, otherwise as normal notifications).

| Argument | Type | Description |
|---|---|---|
| `text` | string | Message (control characters removed, max 250). |

```lua title="client.lua"
TriggerServerEvent('koja-dispatcher:phone:call', 'My neighbour is screaming for help')
```

Only `phones.enabled` must be on; no supported phone is needed. The same player can call once every 2 seconds (`Config.Phones.cooldown`). When nobody can take the call, the player gets a "no dispatchers" message.

### `koja-dispatcher:compat:alert` (net, client → server)

Feeds a client-side alert in the format of another dispatch script.

| Argument | Type | Description |
|---|---|---|
| `name` | string | `'ps-dispatch'`, `'cd_dispatch'`, `'rcore_dispatch'`, `'qs-dispatch'`, `'core_dispatch'` or `'linden_outlawalert'`. |
| `data` | table | The alert in that script's format (see [`CompatAlert`](/new/dispatcher/api/server-exports)). |

Rate limited to 8 alerts per 10 seconds per player (`Config.Compat.rate`), coordinates more than 600 m from the player's ped are replaced by the ped position (`Config.Compat.maxDistance`), and identical alerts within 20 s are dropped.

```lua title="client.lua"
TriggerServerEvent('koja-dispatcher:compat:alert', 'ps-dispatch', {
    message = 'Store robbery', code = '10-90', priority = 1,
    coords = GetEntityCoords(PlayerPedId()), jobs = { 'police' },
})
```

### Dispatch-script events

While `compat.enabled` is on, koja-dispatcher answers the events of other dispatch scripts **only when that script is not running** (a shim from `compat/` counts as not running). Existing robbery and crime scripts keep working without edits.

| Event | Side | Script | Arguments |
|---|---|---|---|
| `ps-dispatch:server:notify` | server (net) | ps-dispatch | `data` table |
| `cd_dispatch:AddNotification` | server (net) | cd_dispatch | `data` table |
| `rcore_dispatch:server:sendAlert` | server (net) | rcore_dispatch | `data` table |
| `qs-dispatch:server:CreateDispatchCall` | server (net) | qs-dispatch | `data` table |
| `core_dispatch:addCall` | server (net) | core_dispatch | `code, title, extraInfo, coords, job, cooldown, sprite, color, priority` |
| `wf-alerts:svNotify` | server (net) | linden_outlawalert | `data` table |
| `wf-alerts:svNotify911` | server (net) | linden_outlawalert | `message, caller, coords` |
| `ps-dispatch:client:notify` | client | ps-dispatch | `data` table, forwarded to the server |
| `cd_dispatch:AddNotification` | client | cd_dispatch | `data` table, forwarded |
| `rcore_dispatch:server:sendAlert` | client | rcore_dispatch | `data` table, forwarded |
| `qs-dispatch:server:CreateDispatchCall` | client | qs-dispatch | `data` table, forwarded |

Server-side `TriggerEvent` (without a player) works too. Events sent by players use the same rate limit and 600 m clamp as `koja-dispatcher:compat:alert`.

### Phone events

With `phones.enabled` on, the dispatcher listens to the running phone:

| Phone | Listened to |
|---|---|
| lb-phone | `lb-phone:newCall` (calls to the service number or to a company in `Config.Phones.companies`), `lb-phone:newCompanyMessage`, `lb-phone:messages:messageSent` (texts to the service number) |
| npwd | `exports.npwd:onCall(number)` and `exports.npwd:onMessage(number)` for the service number |
| qs-smartphone-pro | net events `qs-smartphone:server:sendJobAlert` and `qs-smartphone-pro:server:sendJobAlert` |

These are the phones' own events. You do not trigger them yourself.

---

## koja-mdt events

With koja-mdt started and `mdt.enabled` on, koja-dispatcher listens to these server events. They are accepted **only when koja-mdt fires them**; the same events from other resources are ignored.

| Event | What koja-dispatcher does |
|---|---|
| `koja-mdt:dispatch:create` | Rings the MDT call at the console (`mdt.pullCalls = 'calls'`) or opens an incident (`'incidents'`), only while a dispatcher is on shift and `mode.calls` is not `generated`. Calls created by koja-dispatcher itself are skipped. |
| `koja-mdt:dispatch:close` | Ends the linked call and resolves the linked incident (`outcome = 'mdt'`). |
| `koja-mdt:dispatch:assign` | Assigns the officer's player unit to the linked incident (`mdt.syncStatus`). |
| `koja-mdt:dispatch:detach` | Releases the officer's unit (`mdt.syncStatus`). |
| `koja-mdt:unit:status` | Mirrors the MDT status (`available`, `busy`, `traffic`, `break`) to the player unit (`mdt.syncStatus`). |
| `koja-mdt:officer:duty` | Re-checks which players are field units. |
| `koja-mdt:bolo:create`, `koja-mdt:bolo:update`, `koja-mdt:bolo:close` | Refreshes the CAD BOLO list. |
| `koja-mdt:anpr:hit` | Alerts dispatchers when the read plate has a BOLO (`mdt_tools.alpr`). |

To create MDT calls or status changes from your own resource, use the koja-mdt exports. koja-dispatcher picks the result up through these events.

---

## Client events

koja-dispatcher sends these net events to the player's client. Other client scripts may listen to them, for example to show their own HUD. Register the name in your resource and add a handler; never trigger them yourself.

```lua title="client.lua"
RegisterNetEvent('koja-dispatcher:client:fieldAssign', function(incident)
    print(('assigned to #%d: %s at %s'):format(incident.id, incident.title, incident.label or '?'))
end)
```

| Event | Sent to | Payload |
|---|---|---|
| `koja-dispatcher:client:fieldAssign` | field unit | `{ id, category, title, priority, x, y, z, label, note, scene, located }`. Sent on assignment and again when the incident moves. |
| `koja-dispatcher:client:fieldMove` | field unit | `{ x, y }` staging point from the CAD. |
| `koja-dispatcher:client:fieldOnScene` | field unit | `{ id }` the unit arrived. |
| `koja-dispatcher:client:fieldDone` | field unit | `{ id, outcome }` the incident ended. |
| `koja-dispatcher:client:fieldClear` | field unit | `{}` the unit was released or recalled. |
| `koja-dispatcher:client:fieldStatus` | field unit | the new status string (`'available'`, `'enroute'`, ...). |
| `koja-dispatcher:client:fieldCode` | field unit | `{ code, status, units, mdt }` after a field code; `{ status = 'offline' }` when the unit goes off duty. |
| `koja-dispatcher:client:boloNotice` | on-duty police units | `{ title, plate, priority, label }` new internal BOLO (`mdt_tools.notifyUnits`). |
| `koja-dispatcher:client:voiceRole` | caller, dispatcher or unit in a voice call | role table `{ call, role, provider, elapsed, held, ... }`, or `false` when the voice call ended. |
| `koja-dispatcher:client:sync` | all players | shared client settings: `{ modules, consoles, cinematic, remote, fieldScenes, fieldWaypoint, clientAlerts }`. `modules` holds every setting registered with `client = true`. |
| `koja-dispatcher:client:modules` | local (client `TriggerEvent`) | the `modules` table after each sync. |

Dispatcher-only events (`koja-dispatcher:client:call`, `incident`, `unit`, `radio`, `roster`, ...) only reach players on shift and carry CAD data; their format can change between versions, so read the server exports instead.

---

## Internal events

These are part of koja-dispatcher's own client/server protocol. **Never trigger them from other resources.** Server handlers check the sender and rate limits, and their arguments change between versions.

| Event | Why |
|---|---|
| `koja-dispatcher:rpc:request`, `koja-dispatcher:rpc:response`, `koja-dispatcher:rpc:clientRequest`, `koja-dispatcher:rpc:clientResponse` | The CAD's RPC channel. Use the exports. |
| `koja-dispatcher:server:sync` | Requests the client settings. |
| `koja-dispatcher:server:voice` | AI caller speech upload from the dispatcher's microphone. |
| `koja-dispatcher:server:voiceTalking` | Voice call talking indicator. |
| `koja-dispatcher:server:shots`, `koja-dispatcher:server:crash` | Shots and crash sensors. Use `CreateCall` / `CreateIncident` for your own alerts. |
| `koja-dispatcher:server:avatar`, `koja-dispatcher:server:stationState`, `koja-dispatcher:server:terrainRows` | Avatar capture, station screens, 3D terrain scan. |
| `koja-dispatcher:client:*` | Sent by the server only. Triggering them locally only confuses that player's UI. |
