Hooks
Hooks let another server resource block, rewrite or react to dispatcher actions, wherever they come from: the CAD, /911, phones, koja-mdt, dispatch-script alerts or exports.
| Kind | Runs | Can do |
|---|---|---|
| before | Before the action happens. | Return false (optionally with a reason) to cancel it. incidentCreate hooks can also return a table to change fields. |
| after | After the action happened. | React. Return values are ignored. Every after hook is also fired as the server event koja-dispatcher:<name>. |
Register and remove
local hookId = exports['koja-dispatcher']:RegisterHook('incidentCreated', function(incident)
print(('#%d %s at %s'):format(incident.id, incident.category, incident.label or '?'))
end)
-- later
exports['koja-dispatcher']:RemoveHook(hookId)| Rule | Detail |
|---|---|
Return value of RegisterHook | The hook id, or nil when the name is unknown or fn is not a function. |
| Order | Hooks run in registration order. koja-dispatcher registers its own internal hooks at start, so they run before yours. The first before hook that returns false stops the chain. |
| Errors | Each hook runs in pcall. An error is printed (hook <name> from <resource> failed: ...) and the next hook still runs; a failing before hook does not cancel the action. |
| Cleanup | All hooks of a resource are removed when that resource stops. You only need RemoveHook to stop listening while your resource keeps running. |
| Copies | A hook from another resource receives a serialized copy of the payload. Changing the table in place does nothing; return a table instead (incidentCreate). |
| Speed | Hooks run synchronously inside the dispatcher's logic. Do not wait or query slow APIs in a before hook. |
Cancelling
Return false from a before hook to cancel. Return a short code as the second value to tell the caller why: return false, 'suspended'. Without a reason the caller gets cancelled.
| Hook | What the caller gets when cancelled |
|---|---|
callCreate | CreateCall, PhoneCall, CompatAlert (call mode), client SendAlert and Call911 return nil / false and your reason. CreateCall({ scenario }) returns only nil. Blocked generated calls are skipped. |
incidentCreate | CreateIncident returns nil and your reason. The auto fallback, dispatch-script alerts in incident mode and a dispatcher's dispatch report cancelled. |
unitAssign | The unit is not assigned (internally false, reason). AssignUnits and AutoAssign do not count it; auto-assign moves on to the next unit. |
shiftStart | The shift does not start. The CAD shows the locale text errors.<reason> (for example errors.cancelled); add your own reason to the locale files (game.errors) or reuse an existing key. |
Changing fields
Only incidentCreate applies returned changes: every key of the table you return is copied into the creation data before the incident is built. The other before hooks get a fresh payload table, so returned tables have no effect there.
exports['koja-dispatcher']:RegisterHook('incidentCreate', function(data)
if data.category == 'shooting' then
return { priority = 1, need = { police = 3, ems = 2 } }
end
end)Before hooks
callCreate
Fires when any call is about to ring: generated calls, player and API calls through the router, koja-mdt calls pulled into the CAD and private training calls.
| Field | Type | Description |
|---|---|---|
kind | string | 'script' (generated) or 'player'. |
scenario | string? | Scenario key for generated calls. |
caller | string | Caller name. |
source | number? | Player server id of the caller (/911, phones, CreateCall({ source })). nil for generated calls, SendAlert, compat alerts and koja-mdt calls. |
coords | vector3 | Real position of the call. |
label | string? | Place label. |
message | string | First line: the opening line of a generated call or the player's message. |
origin | string? | Where it came from: your resource name, 'api', 'player', 'client:<id>', 'compat:<script>', 'koja-mdt'. nil for generated calls. |
Can change: nothing. Can cancel: yes, with a reason.
-- no 911 calls from inside the prison
local prison = vector3(1690.0, 2600.0, 45.0)
exports['koja-dispatcher']:RegisterHook('callCreate', function(call)
if call.kind == 'player' and #(call.coords - prison) < 250.0 then
return false, 'no_signal'
end
end)incidentCreate
Fires before every incident is created: dispatched calls, the auto fallback, CreateIncident, manual CAD incidents, dispatch-script alerts in incident mode, koja-mdt calls in incidents mode, field backup and panic, and training incidents.
| Field | Type | Description |
|---|---|---|
category | string | Category. |
priority | number | 1 to 3 (clamped after the hook). |
coords | vector3 | Where the incident is placed (the approximate point for unlocated calls). |
real | vector3? | Real position when it differs from coords (dispatched generated calls). |
located | boolean? | false when the caller did not give the location yet. |
label | string? | Place label. |
need | table | Real need per department. |
known | table | Need known to the dispatcher. |
note | string? | Incident note (cut to 200). |
source | string | 'call', 'player', 'external', 'manual' or 'mdt'. |
origin | string? | Resource or system that created it. |
owner | number? | Dispatcher server id when a dispatcher created it. |
follow | number? | Player server id the incident follows. |
mdtId | number? | Linked koja-mdt call id. |
scenario | string? | Scenario key for dispatched generated calls. |
training | boolean? | true for a private training incident. |
respond, fail, work, points, exposureRate | number? | Timings, score and danger rate when set by the creator. |
Can change: any field. Set scenario to give an API incident the stages, escalation and timings of a scenario. Can cancel: yes, with a reason.
-- Fleeca robberies are always priority 1 with three police units
exports['koja-dispatcher']:RegisterHook('incidentCreate', function(data)
if data.training then return end
if data.category == 'robbery' and data.label and data.label:lower():find('fleeca') then
return { priority = 1, need = { police = 3 }, known = { police = 3 } }
end
end)unitAssign
Fires before a unit is assigned to an incident (CAD, AssignUnits, AutoAssign, koja-mdt attach, backup).
| Field | Type | Description |
|---|---|---|
unit | string | Unit id ('ai:3', 'p:12'). |
src | number? | Player server id for player units. |
callsign | string | Callsign. |
incident | number | Incident id. |
Can change: nothing. Can cancel: yes. With fleet.enabled, the internal fleet hook already blocks resting units and units waiting for repair (fleet_unavailable).
-- probationary officers cannot be sent to priority 1 calls
exports['koja-dispatcher']:RegisterHook('unitAssign', function(data)
if not data.src then return end
local incident = exports['koja-dispatcher']:GetIncident(data.incident)
if incident and incident.priority == 1 and Player(data.src).state.probation then
return false, 'probation'
end
end)shiftStart
Fires after the job, distance, station and capacity checks pass and right before the shift starts.
| Field | Type | Description |
|---|---|---|
source | number | Player server id. |
console | string | Station id, or 'remote' for the remote CAD. |
remote | boolean | true for the remote CAD. |
Can change: nothing. Can cancel: yes, with a reason shown to the player as errors.<reason>.
exports['koja-dispatcher']:RegisterHook('shiftStart', function(data)
if data.remote and not IsPlayerAceAllowed(data.source, 'dispatch.remote') then
return false, 'not_dispatcher' -- existing locale key: "not a dispatcher" message
end
end)After hooks
Every after hook below is also fired as the server event koja-dispatcher:<name> with the same payload (see Events). Private training calls and incidents do not fire call or incident after hooks.
Calls
| Hook | Fires when | Payload |
|---|---|---|
callCreated | A call starts ringing. | The public call. |
callAnswered | A dispatcher answers. | { id, kind, handler }. handler is the dispatcher's server id. |
callEnded | A call is done (dispatched, hung up, prank, missed, expired, closed in the MDT). | { id, kind, incident, handler }. incident is the new incident id when it was dispatched. |
callMissed | A generated call rang longer than calls.patience. | { id, caller, scenario, label, waited }. waited in seconds. |
callDropped | A dispatcher hung up on a generated call that was not a prank. | { id, caller, scenario, label, handler } |
exports['koja-dispatcher']:RegisterHook('callEnded', function(call)
if call.incident then
print(('call #%d became incident #%d'):format(call.id, call.incident))
end
end)Incidents
| Hook | Fires when | Payload |
|---|---|---|
incidentCreated | An incident was created. | The public incident. |
incidentUpdated | UpdateIncident, a scenario event changed it, or a multi-stage scenario moved to the next stage. | The public incident. |
incidentEnded | It was resolved, failed or cancelled. | The public incident with status, outcome and endedAt. |
exports['koja-dispatcher']:RegisterHook('incidentEnded', function(incident)
if incident.status == 'failed' then
print(('incident #%d failed after %ds'):format(incident.id, incident.endedAt - incident.createdAt))
end
end)Units
| Hook | Fires when | Payload |
|---|---|---|
unitAssigned | A unit was assigned. | { unit, src, callsign, incident } |
unitStatus | Any unit status changed (AI and players). | { unit, src, callsign, status, previous } |
unitAdded | A player went on duty as a field unit. | The public unit. |
unitRemoved | A player unit went off duty, left the server or started a dispatcher shift. | { unit, src, callsign } |
src is nil for AI units.
exports['koja-dispatcher']:RegisterHook('unitStatus', function(data)
if data.src and data.status == 'onscene' then
TriggerClientEvent('my-hud:onScene', data.src)
end
end)Shifts, settings and admin
| Hook | Fires when | Payload |
|---|---|---|
shiftStarted | A dispatcher started a shift. | { source, name, console } (console is the station id or 'remote') |
shiftEnded | A shift ended (also on disconnect). | { source, name, minutes, stats, earned, rating } |
settingChanged | A setting changed (admin menu, SetSetting, reset). | { key, value, previous } |
adminAction | An admin did something in /dispatchadmin. | { source, name, text } |
stats is { answered, calls, resolved, failed, missed, pranks, coded, instructions, score }. rating is 1 to 5 stars, earned the pay (0 when unpaid, too short or the player disconnected).
adminAction.text is a short log line, for example set mode.calls = real, reset all settings, added station station_2, moved station mrpd, created job dispatch, saved scenario my_bank.
exports['koja-dispatcher']:RegisterHook('shiftEnded', function(shift)
if shift.minutes >= 60 and shift.rating >= 4 then
exports['my-bonus']:Give(shift.source, 500)
end
end)Pursuits and mass casualty incidents
| Hook | Fires when | Payload |
|---|---|---|
pursuitStarted | A pursuit incident got a live suspect vehicle. | { incident, x, y, category } |
pursuitEnded | The pursuit ended. | { incident, outcome, seconds }, outcome is 'caught', 'lost' or 'cancelled' |
mciStarted | A mass casualty incident opened. | { incident, patients, x, y } |
mciEnded | The MCI incident ended. | { incident, transported, treated, seconds, status } |
exports['koja-dispatcher']:RegisterHook('pursuitStarted', function(p)
exports['my-news']:Breaking(('Police chase in progress near %.0f, %.0f'):format(p.x, p.y))
end)Voice
| Hook | Fires when | Payload |
|---|---|---|
voiceConnected | Caller and dispatcher were put on a voice channel. | { call, channel, provider, caller, handler }. caller and handler are server ids. |
voiceEnded | The voice call ended. | { call, channel, caller, handler, handlerKind, reason, duration } |
reason is ended, dispatched, dispatcher, caller, unit, dropped, shift, disabled or api. handlerKind is 'dispatcher', or 'unit' after a transfer.
exports['koja-dispatcher']:RegisterHook('voiceConnected', function(v)
exports['my-phone']:ShowCallScreen(v.caller, '911', v.channel)
end)Reference
| Hook | Kind | Payload |
|---|---|---|
callCreate | before | { kind, scenario, caller, source, coords, label, message, origin } |
incidentCreate | before | creation data (any field changeable) |
unitAssign | before | { unit, src, callsign, incident } |
shiftStart | before | { source, console, remote } |
callCreated | after | public call |
callAnswered | after | { id, kind, handler } |
callEnded | after | { id, kind, incident, handler } |
callMissed | after | { id, caller, scenario, label, waited } |
callDropped | after | { id, caller, scenario, label, handler } |
incidentCreated | after | public incident |
incidentUpdated | after | public incident |
incidentEnded | after | public incident |
unitAssigned | after | { unit, src, callsign, incident } |
unitStatus | after | { unit, src, callsign, status, previous } |
unitAdded | after | public unit |
unitRemoved | after | { unit, src, callsign } |
shiftStarted | after | { source, name, console } |
shiftEnded | after | { source, name, minutes, stats, earned, rating } |
settingChanged | after | { key, value, previous } |
adminAction | after | { source, name, text } |
mciStarted | after | { incident, patients, x, y } |
mciEnded | after | { incident, transported, treated, seconds, status } |
pursuitStarted | after | { incident, x, y, category } |
pursuitEnded | after | { incident, outcome, seconds } |
voiceConnected | after | { call, channel, provider, caller, handler } |
voiceEnded | after | { call, channel, caller, handler, handlerKind, reason, duration } |
boloCreated is an event only, not a hook: listen with AddEventHandler('koja-dispatcher:boloCreated', ...).