Koja Scripts

Server exports

Every koja-dispatcher server export grouped by domain, with signatures, parameter tables, return values, error codes, limits and Lua examples.

Server exports

All exports on this page are called from server scripts:

server.lua
local dispatch = exports['koja-dispatcher']

Read the overview first: it explains the call router, coordinate and need formats, unit references and the id, route / nil, errorCode convention.

Domains on this page: Calls ยท Incidents ยท Units ยท Dispatchers ยท Modes and settings ยท Radio ยท Hooks ยท Voice ยท Phones and dispatch-script compatibility ยท Discord ยท BOLO and field codes ยท Career

ExportDomainReturns
CreateCall(data) / AddCallCallsid, route or nil, error
GetCall(id)Callscall or nil
GetCalls()Callscall[]
CreateIncident(data) / AddIncidentIncidentsid or nil, error
GetIncident(id)Incidentsincident or nil
GetIncidents()Incidentsincident[]
UpdateIncident(id, patch)Incidentsboolean
ResolveIncident(id, outcome?)Incidentsboolean
CancelIncident(id)Incidentsboolean
GetUnits()Unitsunit[]
GetUnit(ref)Unitsunit or nil
GetPlayerUnit(src)Unitsunit or nil
AssignUnits(id, refs)Unitsnumber
AutoAssign(id)Unitsnumber
ReleaseUnit(ref)Unitsboolean
SetUnitStatus(ref, status)Unitsboolean
SetCallsign(ref, callsign)Unitsboolean
IsDispatcher(src)Dispatchersboolean
GetDispatchers()Dispatchersdispatcher[]
GetDispatcherCount()Dispatchersnumber
GetMode()Settings{ calls, units, noDispatcher }
GetSetting(key)Settingsvalue or nil
SetSetting(key, value)Settingstrue or false, error
RegisterSetting(key, kind, default, options?)Settingsboolean
SendRadio(text, kind?)Radioboolean
RegisterHook(name, fn)Hookshook id or nil
RemoveHook(id)Hooksboolean
ConnectVoice(callId)Voicetrue, channel or false, error
DisconnectVoice(callId)Voiceboolean
GetVoiceSession(callId)Voicesession or nil
PhoneCall(src, text)Phonesid, route or nil, error
CompatAlert(name, data)Compatibilityid, route or nil, error
DiscordLog(channel, title, ...)Discordboolean
CreateBolo(data, author?)BOLOBOLO id or nil, error
CloseBolo(id, author?)BOLOtrue or false, error
ReportPlate(plate, coords?, label?)BOLObolo[]
GetBolos()BOLObolo[]
RunFieldCode(src, code)Field codestrue or false, error
HasCareerPerk(src, perk)Careerboolean
GetCareerRank(src)Careerrank key or nil

Calls

Public call shape (returned by GetCall, GetCalls and the callCreated hook):

{
    id = 7, kind = 'player',            -- 'player' or 'script'
    state = 'ringing',                  -- 'ringing', 'talking' or 'done'
    caller = 'Store alarm', callerAvatar = nil,
    handler = nil, handlerName = nil,   -- dispatcher server id and name once answered
    ringingSince = 1730000000, patience = 45,
    transcript = { { who = 'caller', text = 'Silent alarm at the 24/7' } },
    asked = {}, known = { police = 1 },
    located = true, x = 24.5, y = -1347.3, radius = 0, label = 'Innocence Blvd',
    instruction = nil, instructionGood = nil,
    incident = nil,                     -- incident id after dispatch
    voice = nil, distress = nil, ai = nil, training = nil, dynamics = nil,
    risk = nil,                         -- career perk needed to answer ('pursuits', 'hostage', ...)
}

Generated calls that are not located yet return an approximate x, y and a radius (300 m) instead of the real position.

CreateCall(data)

Rings a 911 call at the dispatcher console, or routes it through the fallback when nobody is on shift (see the call router). AddCall is an alias.

FieldTypeRequiredDescription
coordsvector3 / tableyes, unless sourceWhere the call comes from.
sourcenumbernoPlayer server id. Used for the position when coords is missing, and as the caller: they get notifications (answered, questions, dispatcher replies) and an auto fallback incident follows them.
callerstringnoCaller name shown in the CAD (max 64). Default Anonymous.
messagestringnoOpening line of the transcript. Control characters become spaces, max 300.
labelstringnoPlace or street label shown in the CAD (max 96).
categorystringnoPreselected category for the dispatcher. Must be a known category, otherwise ignored.
prioritynumbernoPreselected priority 1 to 3.
needtablenoUnits the dispatcher already knows are needed ({ police = 2 }). In the auto fallback it becomes the incident need (default { police = 1 }).
scenariostringnoKey of a scenario in Scenarios.List. Rings a generated call with the full scripted conversation at coords. All other fields except coords, source and label are ignored.

Returns id, route:

routeid is
'call'the call id (rings at the console)
'incident'the incident id (mode.noDispatcher = 'auto')
'mdt'the koja-mdt dispatch call id (mode.noDispatcher = 'mdt')

With scenario it returns only the call id, or nil (without a reason) when a hook blocked it.

Errors invalid (no table, no usable coordinates, unknown scenario), no_dispatchers, cancelled or the reason a callCreate hook returned. When an incidentCreate hook blocks the auto fallback, the error is cancelled.

Alarm call
local id, route = exports['koja-dispatcher']:CreateCall({
    coords = vector3(-47.3, -1757.5, 29.4),
    caller = 'Fleeca alarm company',
    message = 'Silent alarm triggered at Fleeca Bank, Hawick Ave',
    label = 'Fleeca Bank, Hawick Ave',
    category = 'robbery',
    priority = 1,
    need = { police = 2 },
})
if not id then print('call failed: ' .. route) end
From a player
-- uses the player's position, the player gets the dispatcher's replies
local id, route = exports['koja-dispatcher']:CreateCall({
    source = source,
    caller = GetPlayerName(source),
    message = 'Someone is breaking into my car',
})
Generated scenario
-- full conversation (questions, answers, instructions) at your coordinates
local callId = exports['koja-dispatcher']:CreateCall({
    scenario = 'store_robbery',
    coords = vector3(25.7, -1347.3, 29.5),
    label = '24/7 Innocence Blvd',
})

Notes

  • The call's origin is the name of your resource. It shows up in incidentCreated / incidentEnded payloads when the call is dispatched.
  • scenario calls skip the router: they ring even when nobody is on shift or mode.calls = 'real', and count as missed after calls.patience.
  • A ringing call fires the callCreate (before) and callCreated (after) hooks.

GetCall(id)

ParamTypeRequiredDescription
idnumberyesCall id.

Returns the public call or nil. Calls are removed 4 s after they end.

server.lua
local call = exports['koja-dispatcher']:GetCall(id)
if call and call.state == 'talking' then print(call.handlerName .. ' is on the line') end

GetCalls()

Returns every call that is not done, as an array of public calls. Private training calls are not included.

server.lua
local ringing = 0
for _, call in ipairs(exports['koja-dispatcher']:GetCalls()) do
    if call.state == 'ringing' then ringing = ringing + 1 end
end

Incidents

Public incident shape (returned by GetIncident, GetIncidents and the incident hooks):

{
    id = 1004, category = 'robbery', priority = 1,
    x = -47.3, y = -1757.5, label = 'Fleeca Bank, Hawick Ave', located = true,
    status = 'active',                  -- 'active', 'resolved', 'failed' or 'cancelled'
    outcome = nil,                      -- set when it ends ('resolved', 'failed', 'cancelled', 'prank', 'code4', 'mdt', 'lost' or your own)
    createdAt = 1730000000, endedAt = nil,
    known = { police = 2 },             -- need known to the dispatcher
    units = { 'ai:1', 'p:12' },         -- assigned unit ids
    onScene = { ['ai:1'] = true },
    progress = 40,                      -- 0..100
    danger = 15,                        -- 0..100, fails at 100
    escalated = false,
    owner = 'Jane Doe',                 -- name of the dispatcher who created it, if any
    note = 'Silent alarm',
    source = 'external',                -- 'call', 'player', 'external', 'manual' or 'mdt'
    origin = 'my-robberies',            -- resource or system that created it
    mdt = 55,                           -- koja-mdt call id when linked
    transport = false, training = nil,
    stage = nil,                        -- { index, total, name } for multi-stage scenarios
    mci = nil,                          -- mass casualty board when it is an MCI
}

CreateIncident(data)

Skips the call and creates an incident on the map straight away, whether or not a dispatcher is on shift. AddIncident is an alias.

FieldTypeRequiredDescription
coordsvector3 / tableyes, unless sourceScene position.
sourcenumbernoPlayer server id whose position is used when coords is missing.
categorystringnoOne of the categories. Unknown or missing becomes suspicious.
prioritynumberno1 to 3, default 2.
needtablenoUnits needed per department, default { police = 1 }. Also used as the known need.
labelstringnoPlace label (max 96).
notestringnoIncident note shown in the CAD and to field units (max 200). message is accepted as an alias.
autoAssignbooleannotrue assigns the nearest free units for the need at once.
follownumbernoPlayer server id: the incident moves with this player (when they are more than 60 m away) until the first unit is on scene.
worknumbernoSeconds of fully covered work on scene to resolve it. Default 60.
failnumbernoSeconds of uncovered exposure until it fails. Default 480.
respondnumbernoExpected response time in seconds, stored on the incident (default 180). Not used by the simulation at the moment.

Returns the incident id. Errors invalid (no table or no usable coordinates), cancelled or the reason an incidentCreate hook returned.

Robbery with auto-assign
local incidentId, err = exports['koja-dispatcher']:CreateIncident({
    category = 'shooting',
    coords = vector3(127.4, -1298.6, 29.2),
    priority = 1,
    need = { police = 2, ems = 1 },
    label = 'Vanilla Unicorn',
    note = 'Shots fired inside the club',
    autoAssign = true,
})
if not incidentId then print('incident failed: ' .. err) end
Follow a player
-- the scene follows the victim until the first unit arrives
exports['koja-dispatcher']:CreateIncident({
    source = victimId,
    follow = victimId,
    category = 'assault',
    need = { police = 1, ems = 1 },
    note = 'Victim is running from the attacker',
})
Long job
-- a slow hazmat job: 4 minutes of work, 15 minutes before it fails
exports['koja-dispatcher']:CreateIncident({
    category = 'hazmat', coords = coords, priority = 1,
    need = { fire = 2, police = 1 }, work = 240, fail = 900,
})

Notes

  • source of the incident is 'external' and origin is your resource name. With koja-mdt and mdt.pushIncidents = 'real' or 'all' it is pushed to the MDT call list.
  • Fires incidentCreate (before, any field can be changed) and incidentCreated (after).
  • Multi-stage scenarios, MCI and pursuits start from the category (pursuit starts a live chase when pursuit.enabled is on and a slot is free).

GetIncident(id)

ParamTypeRequiredDescription
idnumberyesIncident id.

Returns the public incident or nil. Ended incidents stay readable for 20 s.

server.lua
local incident = exports['koja-dispatcher']:GetIncident(incidentId)
if incident and incident.status == 'active' then
    print(('#%d %d%% done, %d unit(s)'):format(incident.id, incident.progress, #incident.units))
end

GetIncidents()

Returns every incident still in memory (active ones and those that ended less than 20 s ago). Training incidents are not included.

server.lua
for _, incident in ipairs(exports['koja-dispatcher']:GetIncidents()) do
    if incident.status == 'active' and incident.priority == 1 then print('P1: #' .. incident.id) end
end

UpdateIncident(id, patch)

Changes an active incident and pushes it to the CAD. Fires incidentUpdated.

Field of patchTypeDescription
prioritynumber1 to 3 (clamped, floored).
notestringMax 200.
labelstringMax 96.
categorystringMust be a known category, otherwise ignored.
needtableReplaces both the need and the known need. An empty need is ignored.
coordsvector3 / tableMoves the incident. It becomes located, and assigned units are re-routed (player units get a new waypoint).

Returns true, or false when the incident does not exist, is not active or patch is not a table.

server.lua
exports['koja-dispatcher']:UpdateIncident(incidentId, {
    priority = 1,
    category = 'hostage',
    need = { police = 4, ems = 1 },
    note = 'Suspects took two hostages inside the vault',
})

ResolveIncident(id, outcome?)

Ends an active incident as resolved. Units are released (AI units drive home; EMS units on scene of a transport incident drive to a hospital).

ParamTypeRequiredDescription
idnumberyesIncident id.
outcomestringnoOutcome label, max 24 chars. Default 'resolved'.

Returns true, or false when it is not active.

server.lua
exports['koja-dispatcher']:ResolveIncident(incidentId, 'arrested')

CancelIncident(id)

Ends an active incident as cancelled (outcome 'cancelled', or 'prank' for a prank scenario). Returns true, or false when it is not active.

server.lua
exports['koja-dispatcher']:CancelIncident(incidentId)

Units

Public unit shape (returned by GetUnits, GetUnit, GetPlayerUnit and the unitAdded hook):

{
    id = 'ai:1', kind = 'ai',                -- 'ai' or 'player'
    dept = 'police', type = 'patrol',        -- type is 'player' for player units
    callsign = '1-ADAM-12', name = 'Patrol', -- AI: unit type label, player: character name
    status = 'enroute',
    x = 441.0, y = -982.0, h = 90.0,
    incident = 1004,                         -- assigned incident id or nil
    target = { x = -47.3, y = -1757.5 },     -- where it is driving to
    purpose = 'incident',                    -- 'incident', 'move', 'home', 'hospital', 'handover' or 'patrol'
    home = 'Mission Row PD',                 -- station label (AI)
    avatar = nil,                            -- avatar key (players)
    route = { { 441.0, -982.0 }, ... },      -- remaining road route (AI)
    -- with fleet.enabled, AI units also carry:
    fatigue = 35, damage = nil, fleetUntil = nil, fleetPending = nil,
}

GetUnits()

Returns every unit (AI and player, including offline AI units).

server.lua
local free = {}
for _, unit in ipairs(exports['koja-dispatcher']:GetUnits()) do
    if unit.dept == 'ems' and unit.status == 'available' then free[#free + 1] = unit.callsign end
end

GetUnit(ref)

ParamTypeRequiredDescription
refstring / numberyesUnit id ('ai:3', 'p:12') or a player server id.

Returns the public unit or nil.

server.lua
local unit = exports['koja-dispatcher']:GetUnit('ai:5')

GetPlayerUnit(src)

ParamTypeRequiredDescription
srcnumberyesPlayer server id.

Returns the player's unit, or nil when the player is not a field unit (wrong job, off duty in koja-mdt, on shift as a dispatcher, or mode.units = 'ai').

server.lua
local unit = exports['koja-dispatcher']:GetPlayerUnit(source)
if unit and unit.incident then print(unit.callsign .. ' is on incident #' .. unit.incident) end

AssignUnits(id, refs)

Assigns units to an active incident. Each unit leaves its previous incident, gets enroute, and player units get the assignment with a waypoint.

ParamTypeRequiredDescription
idnumberyesIncident id.
refstable / string / numberyesA list of unit references ({ 'ai:1', 12, 'p:15' }) or a single one.

Returns the number of units assigned. 0 when the incident is not active. Offline units, unknown references and assignments blocked by a unitAssign hook (for example a resting unit with fleet.enabled) are skipped; the hook's reason is not returned by this export.

server.lua
local count = exports['koja-dispatcher']:AssignUnits(incidentId, { 'ai:1', source })

AutoAssign(id)

Assigns the nearest free units until the incident's known need is covered (unit power counts, AI distance is weighted by speed). Units blocked by a unitAssign hook are skipped and the next nearest unit is tried.

ParamTypeRequiredDescription
idnumberyesIncident id.

Returns the number of units assigned (0 when none are free or the incident is not active).

server.lua
if exports['koja-dispatcher']:AutoAssign(incidentId) == 0 then
    print('no free units')
end

ReleaseUnit(ref)

Detaches a unit from its incident. AI units drive back to their station (returning); player units become available and their waypoint is cleared.

ParamTypeRequiredDescription
refstring / numberyesUnit reference.

Returns true, or false when the unit does not exist.

server.lua
exports['koja-dispatcher']:ReleaseUnit(source)

SetUnitStatus(ref, status)

ParamTypeRequiredDescription
refstring / numberyesUnit reference.
statusstringyes'available', 'busy' or 'unavailable'.

available works like ReleaseUnit. For player units the change is announced on the radio and synced to koja-mdt (mdt.syncStatus). Fires unitStatus.

Returns true, or false for an unknown unit or another status.

server.lua
exports['koja-dispatcher']:SetUnitStatus(source, 'busy')        -- processing a suspect
exports['koja-dispatcher']:SetUnitStatus(source, 'available')   -- back in service

SetCallsign(ref, callsign)

ParamTypeRequiredDescription
refstring / numberyesUnit reference.
callsignstringyesUpper-cased; only letters, digits and - are kept; max 16, min 2 characters.

For player units the callsign is saved in the database by character identifier and reused on the next duty (unless mdt.useCallsigns takes the MDT callsign). Returns true, or false for an unknown unit or a callsign shorter than 2 characters.

server.lua
exports['koja-dispatcher']:SetCallsign(source, '1-ADAM-12')

Dispatchers

IsDispatcher(src)

ParamTypeRequiredDescription
srcnumberyesPlayer server id.

Returns true when the player is on shift as a dispatcher.

server.lua
if exports['koja-dispatcher']:IsDispatcher(source) then return end -- dispatchers cannot rob stores

GetDispatchers()

Returns the dispatchers on shift: { src, name, console, since, avatar }[]. console is the station label (or the remote console label), since is the shift start (os.time()).

server.lua
for _, d in ipairs(exports['koja-dispatcher']:GetDispatchers()) do
    print(('%s at %s since %s'):format(d.name, d.console, os.date('%H:%M', d.since)))
end

GetDispatcherCount()

Returns the number of dispatchers on shift.

server.lua
local staffed = exports['koja-dispatcher']:GetDispatcherCount() > 0

Modes and settings

GetMode()

Returns { calls, units, noDispatcher }, the current values of mode.calls, mode.units and mode.noDispatcher.

server.lua
local mode = exports['koja-dispatcher']:GetMode()
if mode.noDispatcher == 'reject' and exports['koja-dispatcher']:GetDispatcherCount() == 0 then
    -- 911 calls will not go anywhere right now
end

GetSetting(key)

ParamTypeRequiredDescription
keystringyesA settings key, see Settings.

Returns the current value (saved value or default), or nil for an unknown key.

server.lua
local maxRinging = exports['koja-dispatcher']:GetSetting('calls.maxRinging')

SetSetting(key, value)

Changes a setting exactly like the admin menu: the value is validated, applied, saved in koja_dispatch_settings and announced with settingChanged.

ParamTypeRequiredDescription
keystringyesA settings key.
valueboolean / number / stringyesBooleans must be real booleans. Numbers are clamped to the key's range (ints are rounded). Enums must match one of the values exactly.

Returns true (also when the value did not change), or false, 'invalid' for an unknown key or a value of the wrong type.

server.lua
-- no dispatchers at night: dispatch AI units automatically
local ok, err = exports['koja-dispatcher']:SetSetting('mode.noDispatcher', 'auto')

Notes SetSetting writes the database (yields). Changing mode.units or units.disableWhenPlayers re-applies unit membership at once; mdt.* keys refresh the koja-mdt link; client-side keys are pushed to all players.

RegisterSetting(key, kind, default, options?)

Adds your own key to the dispatcher settings. It shows up on the Modules page of /dispatchadmin and works with GetSetting, SetSetting and settingChanged like a built-in key. Values admins save are kept across restarts.

ParamTypeRequiredDescription
keystringyesgroup.name format: letters, digits and _, at least one dot ('myserver.robberyUnits').
kindstringyes'bool', 'int', 'number' or 'enum'.
defaultanyyesDefault value. For bool anything but true means false; for int / number a non-number becomes min; for enum a value not in values becomes values[1].
optionstablenoFields below.
OptionTypeDescription
groupstringAdmin menu group. Default: the part of the key before the first dot.
min, maxnumberRange for int and number. Defaults 0 and 100.
stepnumberSlider step for number. Default 0.1.
valuesstring[]Allowed values. Required (non-empty) for enum.
clientbooleantrue sends the value to every client with the module settings (koja-dispatcher:client:sync).
onChangefunctionCalled with (value, previous) when the value changes.

Returns true when registered, or when the key was already registered through RegisterSetting (then only onChange is replaced). false when the key is badly formed, kind is unknown, an enum has no values, or the key belongs to a built-in setting or one registered inside koja-dispatcher.

server.lua
local dispatch = exports['koja-dispatcher']
dispatch:RegisterSetting('myserver.robberyAlerts', 'bool', true)
dispatch:RegisterSetting('myserver.robberyUnits', 'int', 2, {
    min = 1, max = 8,
    onChange = function(value, previous) print(('robbery units %s -> %s'):format(previous, value)) end,
})
dispatch:RegisterSetting('myserver.robberyRoute', 'enum', 'incident', { values = { 'call', 'incident' } })

Notes

  • Restarts: saved values of keys that are not registered yet are kept when koja-dispatcher loads its settings and applied the moment you register the key, so registration order does not matter. When your resource restarts, call RegisterSetting again: it returns true and attaches your new onChange. Type, range, default and group stay as first registered until koja-dispatcher restarts.
  • Labels come from the locale files: ui.admin.module.<group>, ui.admin.set.<group>.<name> and <name>_hint.

Radio

SendRadio(text, kind?)

Adds a line to the radio log in every dispatcher's CAD.

ParamTypeRequiredDescription
textstringyesControl characters become spaces, max 160.
kindstringnoColour and style: 'dispatch' (default), 'unit', 'alert', 'ok', 'incident', 'call', 'ems'. Anything else becomes 'dispatch'.

Returns true, or false for an empty text. The log keeps the last 40 lines (Config.Incidents.maxLog).

server.lua
exports['koja-dispatcher']:SendRadio('AIR-1 is grounded for maintenance', 'alert')

Hooks

RegisterHook(name, fn)

Registers a hook function. See Hooks for every name and payload.

ParamTypeRequiredDescription
namestringyesA hook name (callCreate, incidentCreated, ...).
fnfunctionyesCalled with the payload. Before hooks return false (optionally with a reason) to block; incidentCreate hooks may return a table to change fields.

Returns the hook id (number), or nil for an unknown name or a non-function.

server.lua
local hookId = exports['koja-dispatcher']:RegisterHook('incidentCreated', function(incident)
    print(('new incident #%d (%s)'):format(incident.id, incident.category))
end)

Hooks are removed automatically when the resource that registered them stops.

RemoveHook(id)

ParamTypeRequiredDescription
idnumberyesThe id returned by RegisterHook.

Returns true when a hook was removed.

server.lua
exports['koja-dispatcher']:RemoveHook(hookId)

Voice

When a dispatcher answers a player call, the caller and the dispatcher are put on a private call channel (pma-voice setPlayerCall, or mumble-voip through the client). Channels are voice.channelBase + 0..899 (Config.Voice.channelRange). Without a voice resource, or with voice.enabled off, calls stay text.

ConnectVoice(callId)

Opens the voice channel for a player call that is in talking state. Normally this happens automatically on answer.

ParamTypeRequiredDescription
callIdnumberyesCall id.

Returns true, channel (also when the session already exists), or false, error.

Errors invalid (unknown call, not a player call, not answered, caller is the dispatcher), offline (caller or dispatcher left), busy (one of them is already in a voice call), unavailable (no voice resource or voice.enabled off), no_channel (all 900 channels used).

server.lua
local ok, channelOrError = exports['koja-dispatcher']:ConnectVoice(callId)

DisconnectVoice(callId)

Takes both players off the channel. Fires voiceEnded with reason = 'api'. Returns true, or false when there is no session.

server.lua
exports['koja-dispatcher']:DisconnectVoice(callId)

GetVoiceSession(callId)

Returns the session or nil:

{
    call = 7, channel = 91103, provider = 'pma-voice',
    caller = 'John Doe',          -- caller name
    callerSrc = 12,               -- caller server id
    handler = 3,                  -- dispatcher (or unit after a transfer) server id
    handlerKind = 'dispatcher',   -- 'dispatcher' or 'unit'
    unit = nil,                   -- unit id after a transfer
    startedAt = 1730000000, muted = false, held = false, detached = false, talking = false,
}
server.lua
local session = exports['koja-dispatcher']:GetVoiceSession(callId)
if session and session.handlerKind == 'unit' then print('transferred to ' .. session.unit) end

Phones and dispatch-script compatibility

PhoneCall(src, text)

Sends a 911 call or text for a player from any phone resource. It uses the /911 logic: a second message within calls.playerReplyWindow is added to the open call. When a supported phone (lb-phone, qs-smartphone-pro, npwd, ...) is running, dispatcher replies come back as phone messages from the service number; otherwise they arrive as normal notifications.

ParamTypeRequiredDescription
srcnumberyesPlayer server id.
textstringyesMessage, control characters removed, max 250.

Returns id, route (route is nil when the text was added to an open call or to the incident that follows the player), or nil, error. Called from the server console (no invoking resource) it returns false.

Errors invalid (player offline, empty text), disabled (phones.enabled off), rate (same player within 2 s), no_dispatchers (the player also gets a "nobody is available" notification or phone message), cancelled or a hook reason.

server.lua
local id, route = exports['koja-dispatcher']:PhoneCall(source, 'There is a fire in my kitchen')

CompatAlert(name, data)

Feeds an alert in the format of another dispatch script. This is what the shim resources in compat/ call.

ParamTypeRequiredDescription
namestringyes'ps-dispatch', 'cd_dispatch', 'rcore_dispatch', 'qs-dispatch', 'core_dispatch' or 'linden_outlawalert'.
datatableyesThe alert in that script's format (see below).
nameFields read
ps-dispatchmessage, information, code, codeName, priority, coords, jobs, street
cd_dispatchtitle, message, coords, job_table, blip.text
rcore_dispatchtext, code, type, default_priority, coords, job
qs-dispatchmessage, callLocation or coords, job, callCode.code, callCode.snippet
core_dispatchtitle, message, extraInfo, coords, job, code, priority
linden_outlawalerttitle, message, coords or pos, recipientList, code

The category comes from the 10-code (10-90 robbery, 10-71 shooting, 10-80 pursuit, 10-99 officer down, ...) or keywords in the text; default suspicious. Priority comes from the alert or the category default. Jobs containing police/sheriff, ambulance/ems or fire pick the departments; otherwise the category default need is used.

Returns id, route. With compat.mode = 'call' the alert goes through the call router; with 'incident' it becomes an incident (route = 'incident', auto-assigned when compat.autoAssign is on).

Errors invalid (unknown name, bad data, no coordinates), disabled (compat.enabled off), duplicate (same alert within 20 s), no_dispatchers, cancelled (in call mode a callCreate hook's own reason is passed on).

server.lua
local id, route = exports['koja-dispatcher']:CompatAlert('ps-dispatch', {
    message = 'Suspicious vehicle', code = '10-66', priority = 2,
    coords = vector3(215.8, -810.1, 30.7), jobs = { 'police' },
})

Discord

DiscordLog(channel, title, description?, fields?, color?)

Queues an embed on one of the dispatcher's webhooks. Webhook URLs come only from the server convars koja_dispatcher_webhook_<channel> (falling back to koja_dispatcher_webhook_default).

ParamTypeRequiredDescription
channelstringyes'shifts', 'incidents' or 'admin'. Anything else becomes 'incidents'.
titlestringyesEmbed title (max 256).
descriptionstringnoEmbed text (max 2000).
fieldstablenoUp to 10 { name, value, inline } entries (name max 256, value max 1024).
colornumbernoEmbed colour, for example 0x5865F2.

Returns true when queued, false when title is not a string or discord.enabled is off. Embeds are sent in batches of up to 10 every 2 s per webhook; a channel without a URL is dropped silently.

server.lua
exports['koja-dispatcher']:DiscordLog('incidents', 'Bank job finished', 'Pacific Standard was cleared.', {
    { name = 'Crew', value = '4' },
    { name = 'Loot', value = '$1,250,000' },
}, 0x5865F2)

BOLO and field codes

BOLO shape (GetBolos, ReportPlate, koja-dispatcher:boloCreated):

{
    id = 'i12',                        -- 'i<id>' internal, 'm<id>' koja-mdt
    source = 'internal',               -- 'internal' or 'mdt'
    type = 'vehicle',                  -- 'person', 'vehicle' or 'general'
    title = 'Black Sultan, armed', description = '...',
    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,
    department = nil,                  -- MDT BOLOs only
}

Where BOLOs are stored depends on mdt_tools.boloSource: auto uses koja-mdt while it runs and the dispatcher's own list (koja_dispatch_bolos) otherwise; mdt only uses koja-mdt; internal only the dispatcher's list.

CreateBolo(data, author?)

Creates a BOLO exactly like the CAD's MDT workspace: in koja-mdt (with location, colour and linked incident kept in koja_dispatch_bolos) or in the internal list, depending on mdt_tools.boloSource. Every dispatcher sees it at once.

FieldTypeRequiredDescription
typestringyes'person', 'vehicle' or 'general'.
titlestringyes3 to 96 characters.
platestringfor vehicle2 to 8 characters; upper-cased, only letters, digits and spaces.
descriptionstringnoMax 1000 characters.
model, colourstringnoMax 48 each.
prioritynumberno1 to 3, default 2.
imagestringnohttp(s) URL, max 512.
incidentnumbernoLinks an incident; its position and label are used.
pointtableno{ x, y } last-seen position when no incident is given.
labelstringnoPlace label for point (max 128).
ParamTypeRequiredDescription
authorstringnoName stored as the creator (max 128). Default: your resource name.

Returns the BOLO id: 'm<id>' when it was created in koja-mdt, 'i<id>' for the internal list. Or nil, error.

Errors invalid (not a table, unknown type), title_required, plate_required, invalid_plate, invalid_url, mdt_offline (boloSource = 'mdt' while koja-mdt is down), mdt_failed or a koja-mdt error code, db (internal list without a database).

server.lua
local boloId, err = exports['koja-dispatcher']:CreateBolo({
    type = 'vehicle',
    title = 'Black Sultan, armed occupants',
    plate = 'ABC123',
    model = 'Sultan', colour = 'Black',
    description = 'Fled the Pacific Standard robbery northbound',
    priority = 1,
    point = { x = 235.0, y = 216.0 },
    label = 'Pacific Standard Bank',
}, 'Pacific Standard security')
if not boloId then print('BOLO failed: ' .. err) end

Notes A radio alert announces the BOLO. Internal BOLOs also fire koja-dispatcher:boloCreated and notify on-duty police field units when mdt_tools.notifyUnits is on. Writes the database (yields).

CloseBolo(id, author?)

ParamTypeRequiredDescription
idstringyesBOLO id from CreateBolo or GetBolos ('i12', 'm5').
authorstringnoName passed to koja-mdt as the closer. Default: your resource name.

Returns true, or false, error. Errors invalid (bad id format), not_found (unknown internal BOLO), mdt_offline, mdt_failed or a koja-mdt error code.

server.lua
exports['koja-dispatcher']:CloseBolo(boloId)

ReportPlate(plate, coords?, label?)

For third-party ALPR and camera scripts. Checks a plate against the active BOLO list; a match alerts every dispatcher like an ALPR hit (radio line and CAD alert) and moves the BOLO's last-seen position to coords.

ParamTypeRequiredDescription
platestringyesUpper-cased, letters, digits and spaces kept, 2 to 8 characters. Spaces are ignored when matching.
coordstable / vector3noWhere the plate was read (x, y).
labelstringnoCamera or street label (max 128).

Returns the matching BOLOs (an empty table when none match, the plate is invalid or mdt_tools.alpr is off).

server.lua
local hits = exports['koja-dispatcher']:ReportPlate('ABC 123', GetEntityCoords(GetPlayerPed(src)), 'Camera 14, Del Perro Fwy')
if #hits > 0 then print('BOLO hit: ' .. hits[1].title) end

GetBolos()

Returns the active BOLO list as the CAD sees it, sorted by priority then newest first. With koja-mdt (mdt_tools.boloSource auto or mdt) MDT BOLOs are included. The list refreshes about 0.3 s after a change.

server.lua
for _, bolo in ipairs(exports['koja-dispatcher']:GetBolos()) do
    if bolo.type == 'vehicle' then print(bolo.plate, bolo.title) end
end

RunFieldCode(src, code)

Runs a field status code for a player as if they pressed the key or typed /tencode.

ParamTypeRequiredDescription
srcnumberyesPlayer server id (online).
codestringyesA code id from Config.MdtStatus.codes. Default codes below.
CodeAction
'10-8'available
'10-6'busy
'10-97'en route
'10-23'on scene (counts as arrival when the unit drives to an incident)
'10-7'unavailable (MDT break)
'10-15'transport (MDT busy)
'backup'priority 1 officer_down incident at the officer, assigns them plus mdt_status.backupUnits nearest units; with koja-mdt also a 10-78 MDT call
'10-99'koja-mdt SendPanic; without koja-mdt a local priority 1 incident (mdt_status.panicFallback)

Returns true, or false, error. Errors invalid (offline player, unknown code), disabled (mdt_status.enabled off, or panic without MDT and panicFallback off), not_field_unit (not a unit or MDT officer, or a dispatcher), cooldown (backup or panic within mdt_status.cooldown), cancelled.

server.lua
local ok, err = exports['koja-dispatcher']:RunFieldCode(source, '10-6')

Career

Dispatcher ranks, XP and unlocks are configured in shared/config/career.lua.

HasCareerPerk(src, perk)

ParamTypeRequiredDescription
srcnumberyesPlayer server id.
perkstringyesPerk name. Default perks: pursuits, hostage, mci, supervise.

Returns true when the player's rank (or a lower one) unlocks the perk. Returns true for everybody when career.enabled is off. May read the database the first time it is asked about a player (yields).

server.lua
if not exports['koja-dispatcher']:HasCareerPerk(source, 'supervise') then
    return print('supervisors only')
end

GetCareerRank(src)

Returns the rank key ('trainee', 'dispatcher', 'senior', 'supervisor', 'chief' by default), or nil when career.enabled is off.

server.lua
local rank = exports['koja-dispatcher']:GetCareerRank(source)