Koja Scripts

Hooks

Every koja-dispatcher hook, when it fires, its exact payload, what a before hook can change or cancel, and how to register and remove hooks.

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.

KindRunsCan do
beforeBefore the action happens.Return false (optionally with a reason) to cancel it. incidentCreate hooks can also return a table to change fields.
afterAfter the action happened.React. Return values are ignored. Every after hook is also fired as the server event koja-dispatcher:<name>.

Register and remove

server.lua
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)
RuleDetail
Return value of RegisterHookThe hook id, or nil when the name is unknown or fn is not a function.
OrderHooks 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.
ErrorsEach 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.
CleanupAll hooks of a resource are removed when that resource stops. You only need RemoveHook to stop listening while your resource keeps running.
CopiesA hook from another resource receives a serialized copy of the payload. Changing the table in place does nothing; return a table instead (incidentCreate).
SpeedHooks 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.

HookWhat the caller gets when cancelled
callCreateCreateCall, PhoneCall, CompatAlert (call mode), client SendAlert and Call911 return nil / false and your reason. CreateCall({ scenario }) returns only nil. Blocked generated calls are skipped.
incidentCreateCreateIncident returns nil and your reason. The auto fallback, dispatch-script alerts in incident mode and a dispatcher's dispatch report cancelled.
unitAssignThe unit is not assigned (internally false, reason). AssignUnits and AutoAssign do not count it; auto-assign moves on to the next unit.
shiftStartThe 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.

server.lua
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.

FieldTypeDescription
kindstring'script' (generated) or 'player'.
scenariostring?Scenario key for generated calls.
callerstringCaller name.
sourcenumber?Player server id of the caller (/911, phones, CreateCall({ source })). nil for generated calls, SendAlert, compat alerts and koja-mdt calls.
coordsvector3Real position of the call.
labelstring?Place label.
messagestringFirst line: the opening line of a generated call or the player's message.
originstring?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.

server.lua
-- 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.

FieldTypeDescription
categorystringCategory.
prioritynumber1 to 3 (clamped after the hook).
coordsvector3Where the incident is placed (the approximate point for unlocated calls).
realvector3?Real position when it differs from coords (dispatched generated calls).
locatedboolean?false when the caller did not give the location yet.
labelstring?Place label.
needtableReal need per department.
knowntableNeed known to the dispatcher.
notestring?Incident note (cut to 200).
sourcestring'call', 'player', 'external', 'manual' or 'mdt'.
originstring?Resource or system that created it.
ownernumber?Dispatcher server id when a dispatcher created it.
follownumber?Player server id the incident follows.
mdtIdnumber?Linked koja-mdt call id.
scenariostring?Scenario key for dispatched generated calls.
trainingboolean?true for a private training incident.
respond, fail, work, points, exposureRatenumber?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.

server.lua
-- 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).

FieldTypeDescription
unitstringUnit id ('ai:3', 'p:12').
srcnumber?Player server id for player units.
callsignstringCallsign.
incidentnumberIncident 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).

server.lua
-- 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.

FieldTypeDescription
sourcenumberPlayer server id.
consolestringStation id, or 'remote' for the remote CAD.
remotebooleantrue for the remote CAD.

Can change: nothing. Can cancel: yes, with a reason shown to the player as errors.<reason>.

server.lua
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

HookFires whenPayload
callCreatedA call starts ringing.The public call.
callAnsweredA dispatcher answers.{ id, kind, handler }. handler is the dispatcher's server id.
callEndedA 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.
callMissedA generated call rang longer than calls.patience.{ id, caller, scenario, label, waited }. waited in seconds.
callDroppedA dispatcher hung up on a generated call that was not a prank.{ id, caller, scenario, label, handler }
server.lua
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

HookFires whenPayload
incidentCreatedAn incident was created.The public incident.
incidentUpdatedUpdateIncident, a scenario event changed it, or a multi-stage scenario moved to the next stage.The public incident.
incidentEndedIt was resolved, failed or cancelled.The public incident with status, outcome and endedAt.
server.lua
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

HookFires whenPayload
unitAssignedA unit was assigned.{ unit, src, callsign, incident }
unitStatusAny unit status changed (AI and players).{ unit, src, callsign, status, previous }
unitAddedA player went on duty as a field unit.The public unit.
unitRemovedA player unit went off duty, left the server or started a dispatcher shift.{ unit, src, callsign }

src is nil for AI units.

server.lua
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

HookFires whenPayload
shiftStartedA dispatcher started a shift.{ source, name, console } (console is the station id or 'remote')
shiftEndedA shift ended (also on disconnect).{ source, name, minutes, stats, earned, rating }
settingChangedA setting changed (admin menu, SetSetting, reset).{ key, value, previous }
adminActionAn 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.

server.lua
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

HookFires whenPayload
pursuitStartedA pursuit incident got a live suspect vehicle.{ incident, x, y, category }
pursuitEndedThe pursuit ended.{ incident, outcome, seconds }, outcome is 'caught', 'lost' or 'cancelled'
mciStartedA mass casualty incident opened.{ incident, patients, x, y }
mciEndedThe MCI incident ended.{ incident, transported, treated, seconds, status }
server.lua
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

HookFires whenPayload
voiceConnectedCaller and dispatcher were put on a voice channel.{ call, channel, provider, caller, handler }. caller and handler are server ids.
voiceEndedThe 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.

server.lua
exports['koja-dispatcher']:RegisterHook('voiceConnected', function(v)
    exports['my-phone']:ShowCallScreen(v.caller, '911', v.channel)
end)

Reference

HookKindPayload
callCreatebefore{ kind, scenario, caller, source, coords, label, message, origin }
incidentCreatebeforecreation data (any field changeable)
unitAssignbefore{ unit, src, callsign, incident }
shiftStartbefore{ source, console, remote }
callCreatedafterpublic call
callAnsweredafter{ id, kind, handler }
callEndedafter{ id, kind, incident, handler }
callMissedafter{ id, caller, scenario, label, waited }
callDroppedafter{ id, caller, scenario, label, handler }
incidentCreatedafterpublic incident
incidentUpdatedafterpublic incident
incidentEndedafterpublic incident
unitAssignedafter{ unit, src, callsign, incident }
unitStatusafter{ unit, src, callsign, status, previous }
unitAddedafterpublic unit
unitRemovedafter{ unit, src, callsign }
shiftStartedafter{ source, name, console }
shiftEndedafter{ source, name, minutes, stats, earned, rating }
settingChangedafter{ key, value, previous }
adminActionafter{ source, name, text }
mciStartedafter{ incident, patients, x, y }
mciEndedafter{ incident, transported, treated, seconds, status }
pursuitStartedafter{ incident, x, y, category }
pursuitEndedafter{ incident, outcome, seconds }
voiceConnectedafter{ call, channel, provider, caller, handler }
voiceEndedafter{ call, channel, caller, handler, handlerKind, reason, duration }

boloCreated is an event only, not a hook: listen with AddEventHandler('koja-dispatcher:boloCreated', ...).