Server exports
All exports on this page are called from server scripts:
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
| Export | Domain | Returns |
|---|---|---|
CreateCall(data) / AddCall | Calls | id, route or nil, error |
GetCall(id) | Calls | call or nil |
GetCalls() | Calls | call[] |
CreateIncident(data) / AddIncident | Incidents | id or nil, error |
GetIncident(id) | Incidents | incident or nil |
GetIncidents() | Incidents | incident[] |
UpdateIncident(id, patch) | Incidents | boolean |
ResolveIncident(id, outcome?) | Incidents | boolean |
CancelIncident(id) | Incidents | boolean |
GetUnits() | Units | unit[] |
GetUnit(ref) | Units | unit or nil |
GetPlayerUnit(src) | Units | unit or nil |
AssignUnits(id, refs) | Units | number |
AutoAssign(id) | Units | number |
ReleaseUnit(ref) | Units | boolean |
SetUnitStatus(ref, status) | Units | boolean |
SetCallsign(ref, callsign) | Units | boolean |
IsDispatcher(src) | Dispatchers | boolean |
GetDispatchers() | Dispatchers | dispatcher[] |
GetDispatcherCount() | Dispatchers | number |
GetMode() | Settings | { calls, units, noDispatcher } |
GetSetting(key) | Settings | value or nil |
SetSetting(key, value) | Settings | true or false, error |
RegisterSetting(key, kind, default, options?) | Settings | boolean |
SendRadio(text, kind?) | Radio | boolean |
RegisterHook(name, fn) | Hooks | hook id or nil |
RemoveHook(id) | Hooks | boolean |
ConnectVoice(callId) | Voice | true, channel or false, error |
DisconnectVoice(callId) | Voice | boolean |
GetVoiceSession(callId) | Voice | session or nil |
PhoneCall(src, text) | Phones | id, route or nil, error |
CompatAlert(name, data) | Compatibility | id, route or nil, error |
DiscordLog(channel, title, ...) | Discord | boolean |
CreateBolo(data, author?) | BOLO | BOLO id or nil, error |
CloseBolo(id, author?) | BOLO | true or false, error |
ReportPlate(plate, coords?, label?) | BOLO | bolo[] |
GetBolos() | BOLO | bolo[] |
RunFieldCode(src, code) | Field codes | true or false, error |
HasCareerPerk(src, perk) | Career | boolean |
GetCareerRank(src) | Career | rank 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.
| Field | Type | Required | Description |
|---|---|---|---|
coords | vector3 / table | yes, unless source | Where the call comes from. |
source | number | no | Player 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. |
caller | string | no | Caller name shown in the CAD (max 64). Default Anonymous. |
message | string | no | Opening line of the transcript. Control characters become spaces, max 300. |
label | string | no | Place or street label shown in the CAD (max 96). |
category | string | no | Preselected category for the dispatcher. Must be a known category, otherwise ignored. |
priority | number | no | Preselected priority 1 to 3. |
need | table | no | Units the dispatcher already knows are needed ({ police = 2 }). In the auto fallback it becomes the incident need (default { police = 1 }). |
scenario | string | no | Key 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:
route | id 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.
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-- 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',
})-- 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
originis the name of your resource. It shows up inincidentCreated/incidentEndedpayloads when the call is dispatched. scenariocalls skip the router: they ring even when nobody is on shift ormode.calls = 'real', and count as missed aftercalls.patience.- A ringing call fires the
callCreate(before) andcallCreated(after) hooks.
GetCall(id)
| Param | Type | Required | Description |
|---|---|---|---|
id | number | yes | Call id. |
Returns the public call or nil. Calls are removed 4 s after they end.
local call = exports['koja-dispatcher']:GetCall(id)
if call and call.state == 'talking' then print(call.handlerName .. ' is on the line') endGetCalls()
Returns every call that is not done, as an array of public calls. Private training calls are not included.
local ringing = 0
for _, call in ipairs(exports['koja-dispatcher']:GetCalls()) do
if call.state == 'ringing' then ringing = ringing + 1 end
endIncidents
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.
| Field | Type | Required | Description |
|---|---|---|---|
coords | vector3 / table | yes, unless source | Scene position. |
source | number | no | Player server id whose position is used when coords is missing. |
category | string | no | One of the categories. Unknown or missing becomes suspicious. |
priority | number | no | 1 to 3, default 2. |
need | table | no | Units needed per department, default { police = 1 }. Also used as the known need. |
label | string | no | Place label (max 96). |
note | string | no | Incident note shown in the CAD and to field units (max 200). message is accepted as an alias. |
autoAssign | boolean | no | true assigns the nearest free units for the need at once. |
follow | number | no | Player server id: the incident moves with this player (when they are more than 60 m away) until the first unit is on scene. |
work | number | no | Seconds of fully covered work on scene to resolve it. Default 60. |
fail | number | no | Seconds of uncovered exposure until it fails. Default 480. |
respond | number | no | Expected 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.
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-- 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',
})-- 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
sourceof the incident is'external'andoriginis your resource name. With koja-mdt andmdt.pushIncidents = 'real'or'all'it is pushed to the MDT call list.- Fires
incidentCreate(before, any field can be changed) andincidentCreated(after). - Multi-stage scenarios, MCI and pursuits start from the category (
pursuitstarts a live chase whenpursuit.enabledis on and a slot is free).
GetIncident(id)
| Param | Type | Required | Description |
|---|---|---|---|
id | number | yes | Incident id. |
Returns the public incident or nil. Ended incidents stay readable for 20 s.
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))
endGetIncidents()
Returns every incident still in memory (active ones and those that ended less than 20 s ago). Training incidents are not included.
for _, incident in ipairs(exports['koja-dispatcher']:GetIncidents()) do
if incident.status == 'active' and incident.priority == 1 then print('P1: #' .. incident.id) end
endUpdateIncident(id, patch)
Changes an active incident and pushes it to the CAD. Fires incidentUpdated.
Field of patch | Type | Description |
|---|---|---|
priority | number | 1 to 3 (clamped, floored). |
note | string | Max 200. |
label | string | Max 96. |
category | string | Must be a known category, otherwise ignored. |
need | table | Replaces both the need and the known need. An empty need is ignored. |
coords | vector3 / table | Moves 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.
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).
| Param | Type | Required | Description |
|---|---|---|---|
id | number | yes | Incident id. |
outcome | string | no | Outcome label, max 24 chars. Default 'resolved'. |
Returns true, or false when it is not active.
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.
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).
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
endGetUnit(ref)
| Param | Type | Required | Description |
|---|---|---|---|
ref | string / number | yes | Unit id ('ai:3', 'p:12') or a player server id. |
Returns the public unit or nil.
local unit = exports['koja-dispatcher']:GetUnit('ai:5')GetPlayerUnit(src)
| Param | Type | Required | Description |
|---|---|---|---|
src | number | yes | Player 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').
local unit = exports['koja-dispatcher']:GetPlayerUnit(source)
if unit and unit.incident then print(unit.callsign .. ' is on incident #' .. unit.incident) endAssignUnits(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.
| Param | Type | Required | Description |
|---|---|---|---|
id | number | yes | Incident id. |
refs | table / string / number | yes | A 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.
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.
| Param | Type | Required | Description |
|---|---|---|---|
id | number | yes | Incident id. |
Returns the number of units assigned (0 when none are free or the incident is not active).
if exports['koja-dispatcher']:AutoAssign(incidentId) == 0 then
print('no free units')
endReleaseUnit(ref)
Detaches a unit from its incident. AI units drive back to their station (returning); player units become available and their waypoint is cleared.
| Param | Type | Required | Description |
|---|---|---|---|
ref | string / number | yes | Unit reference. |
Returns true, or false when the unit does not exist.
exports['koja-dispatcher']:ReleaseUnit(source)SetUnitStatus(ref, status)
| Param | Type | Required | Description |
|---|---|---|---|
ref | string / number | yes | Unit reference. |
status | string | yes | '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.
exports['koja-dispatcher']:SetUnitStatus(source, 'busy') -- processing a suspect
exports['koja-dispatcher']:SetUnitStatus(source, 'available') -- back in serviceSetCallsign(ref, callsign)
| Param | Type | Required | Description |
|---|---|---|---|
ref | string / number | yes | Unit reference. |
callsign | string | yes | Upper-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.
exports['koja-dispatcher']:SetCallsign(source, '1-ADAM-12')Dispatchers
IsDispatcher(src)
| Param | Type | Required | Description |
|---|---|---|---|
src | number | yes | Player server id. |
Returns true when the player is on shift as a dispatcher.
if exports['koja-dispatcher']:IsDispatcher(source) then return end -- dispatchers cannot rob storesGetDispatchers()
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()).
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)))
endGetDispatcherCount()
Returns the number of dispatchers on shift.
local staffed = exports['koja-dispatcher']:GetDispatcherCount() > 0Modes and settings
GetMode()
Returns { calls, units, noDispatcher }, the current values of mode.calls, mode.units and mode.noDispatcher.
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
endGetSetting(key)
| Param | Type | Required | Description |
|---|---|---|---|
key | string | yes | A settings key, see Settings. |
Returns the current value (saved value or default), or nil for an unknown key.
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.
| Param | Type | Required | Description |
|---|---|---|---|
key | string | yes | A settings key. |
value | boolean / number / string | yes | Booleans 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.
-- 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.
| Param | Type | Required | Description |
|---|---|---|---|
key | string | yes | group.name format: letters, digits and _, at least one dot ('myserver.robberyUnits'). |
kind | string | yes | 'bool', 'int', 'number' or 'enum'. |
default | any | yes | Default 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]. |
options | table | no | Fields below. |
| Option | Type | Description |
|---|---|---|
group | string | Admin menu group. Default: the part of the key before the first dot. |
min, max | number | Range for int and number. Defaults 0 and 100. |
step | number | Slider step for number. Default 0.1. |
values | string[] | Allowed values. Required (non-empty) for enum. |
client | boolean | true sends the value to every client with the module settings (koja-dispatcher:client:sync). |
onChange | function | Called 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.
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
RegisterSettingagain: it returnstrueand attaches your newonChange. 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.
| Param | Type | Required | Description |
|---|---|---|---|
text | string | yes | Control characters become spaces, max 160. |
kind | string | no | Colour 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).
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.
| Param | Type | Required | Description |
|---|---|---|---|
name | string | yes | A hook name (callCreate, incidentCreated, ...). |
fn | function | yes | Called 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.
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)
| Param | Type | Required | Description |
|---|---|---|---|
id | number | yes | The id returned by RegisterHook. |
Returns true when a hook was removed.
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.
| Param | Type | Required | Description |
|---|---|---|---|
callId | number | yes | Call 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).
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.
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,
}
local session = exports['koja-dispatcher']:GetVoiceSession(callId)
if session and session.handlerKind == 'unit' then print('transferred to ' .. session.unit) endPhones 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.
| Param | Type | Required | Description |
|---|---|---|---|
src | number | yes | Player server id. |
text | string | yes | Message, 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.
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.
| Param | Type | Required | Description |
|---|---|---|---|
name | string | yes | 'ps-dispatch', 'cd_dispatch', 'rcore_dispatch', 'qs-dispatch', 'core_dispatch' or 'linden_outlawalert'. |
data | table | yes | The alert in that script's format (see below). |
name | Fields read |
|---|---|
ps-dispatch | message, information, code, codeName, priority, coords, jobs, street |
cd_dispatch | title, message, coords, job_table, blip.text |
rcore_dispatch | text, code, type, default_priority, coords, job |
qs-dispatch | message, callLocation or coords, job, callCode.code, callCode.snippet |
core_dispatch | title, message, extraInfo, coords, job, code, priority |
linden_outlawalert | title, 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).
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).
| Param | Type | Required | Description |
|---|---|---|---|
channel | string | yes | 'shifts', 'incidents' or 'admin'. Anything else becomes 'incidents'. |
title | string | yes | Embed title (max 256). |
description | string | no | Embed text (max 2000). |
fields | table | no | Up to 10 { name, value, inline } entries (name max 256, value max 1024). |
color | number | no | Embed 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.
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.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | 'person', 'vehicle' or 'general'. |
title | string | yes | 3 to 96 characters. |
plate | string | for vehicle | 2 to 8 characters; upper-cased, only letters, digits and spaces. |
description | string | no | Max 1000 characters. |
model, colour | string | no | Max 48 each. |
priority | number | no | 1 to 3, default 2. |
image | string | no | http(s) URL, max 512. |
incident | number | no | Links an incident; its position and label are used. |
point | table | no | { x, y } last-seen position when no incident is given. |
label | string | no | Place label for point (max 128). |
| Param | Type | Required | Description |
|---|---|---|---|
author | string | no | Name 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).
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) endNotes 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?)
| Param | Type | Required | Description |
|---|---|---|---|
id | string | yes | BOLO id from CreateBolo or GetBolos ('i12', 'm5'). |
author | string | no | Name 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.
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.
| Param | Type | Required | Description |
|---|---|---|---|
plate | string | yes | Upper-cased, letters, digits and spaces kept, 2 to 8 characters. Spaces are ignored when matching. |
coords | table / vector3 | no | Where the plate was read (x, y). |
label | string | no | Camera 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).
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) endGetBolos()
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.
for _, bolo in ipairs(exports['koja-dispatcher']:GetBolos()) do
if bolo.type == 'vehicle' then print(bolo.plate, bolo.title) end
endRunFieldCode(src, code)
Runs a field status code for a player as if they pressed the key or typed /tencode.
| Param | Type | Required | Description |
|---|---|---|---|
src | number | yes | Player server id (online). |
code | string | yes | A code id from Config.MdtStatus.codes. Default codes below. |
| Code | Action |
|---|---|
'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.
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)
| Param | Type | Required | Description |
|---|---|---|---|
src | number | yes | Player server id. |
perk | string | yes | Perk 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).
if not exports['koja-dispatcher']:HasCareerPerk(source, 'supervise') then
return print('supervisors only')
endGetCareerRank(src)
Returns the rank key ('trainee', 'dispatcher', 'senior', 'supervisor', 'chief' by default), or nil when career.enabled is off.
local rank = exports['koja-dispatcher']:GetCareerRank(source)